Store quality option and use it on all videos #1356
This commit is contained in:
commit
d8ab1eb960
15 changed files with 203 additions and 65 deletions
|
@ -1671,7 +1671,9 @@
|
||||||
"Theater Mode (t)": "Theater Mode (t)",
|
"Theater Mode (t)": "Theater Mode (t)",
|
||||||
"Default Mode (t)": "Default Mode (t)",
|
"Default Mode (t)": "Default Mode (t)",
|
||||||
"Quality": "Quality",
|
"Quality": "Quality",
|
||||||
|
"Auto": "Auto",
|
||||||
"Auto --[Video quality. Short form]--": "Auto",
|
"Auto --[Video quality. Short form]--": "Auto",
|
||||||
|
"Auto %quality% --[Video quality popup. Long form.]--": "Auto %quality%",
|
||||||
"Orig (%quality%) --[Video quality popup. Short form.]--": "Orig (%quality%)",
|
"Orig (%quality%) --[Video quality popup. Short form.]--": "Orig (%quality%)",
|
||||||
"Original (%quality%) --[Video quality popup. Long form.]--": "Original (%quality%)",
|
"Original (%quality%) --[Video quality popup. Long form.]--": "Original (%quality%)",
|
||||||
"Original --[Video quality button. Abbreviate to fit space.]--": "Original",
|
"Original --[Video quality button. Abbreviate to fit space.]--": "Original",
|
||||||
|
@ -2246,5 +2248,9 @@
|
||||||
"Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.",
|
"Attach images by pasting or drag-and-drop.": "Attach images by pasting or drag-and-drop.",
|
||||||
"There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.": "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.",
|
"There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.": "There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.",
|
||||||
"Sports": "Sports",
|
"Sports": "Sports",
|
||||||
|
"Default Video Quality": "Default Video Quality",
|
||||||
|
"Set a default quality for video playback. If the default choice is not available, the next lowest will be used when playback starts.": "Set a default quality for video playback. If the default choice is not available, the next lowest will be used when playback starts.",
|
||||||
|
"Original": "Original",
|
||||||
|
"Enable default quality setting": "Enable default quality setting",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Card from 'component/common/card';
|
||||||
import { FormField, FormFieldPrice } from 'component/common/form';
|
import { FormField, FormFieldPrice } from 'component/common/form';
|
||||||
import MaxPurchasePrice from 'component/maxPurchasePrice';
|
import MaxPurchasePrice from 'component/maxPurchasePrice';
|
||||||
import SettingsRow from 'component/settingsRow';
|
import SettingsRow from 'component/settingsRow';
|
||||||
|
import SettingDefaultQuality from 'component/settingDefaultQuality';
|
||||||
|
|
||||||
type Price = {
|
type Price = {
|
||||||
currency: string,
|
currency: string,
|
||||||
|
@ -119,6 +120,10 @@ export default function SettingContent(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow title={__('Default Video Quality')} subtitle={__(HELP.DEFAULT_VIDEO_QUALITY)}>
|
||||||
|
<SettingDefaultQuality />
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
{!SIMPLE_SITE && (
|
{!SIMPLE_SITE && (
|
||||||
<>
|
<>
|
||||||
{/*
|
{/*
|
||||||
|
@ -240,4 +245,5 @@ const HELP = {
|
||||||
MAX_PURCHASE_PRICE: 'This will prevent you from purchasing any content over a certain cost, as a safety measure.',
|
MAX_PURCHASE_PRICE: 'This will prevent you from purchasing any content over a certain cost, as a safety measure.',
|
||||||
ONLY_CONFIRM_OVER_AMOUNT: '', // [feel redundant. Disable for now] "When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount.",
|
ONLY_CONFIRM_OVER_AMOUNT: '', // [feel redundant. Disable for now] "When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount.",
|
||||||
PUBLISH_PREVIEW: 'Show preview and confirmation dialog before publishing content.',
|
PUBLISH_PREVIEW: 'Show preview and confirmation dialog before publishing content.',
|
||||||
|
DEFAULT_VIDEO_QUALITY: 'Set a default quality for video playback. If the default choice is not available, the next lowest will be used when playback starts.',
|
||||||
};
|
};
|
||||||
|
|
15
ui/component/settingDefaultQuality/index.js
Normal file
15
ui/component/settingDefaultQuality/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { doSetDefaultVideoQuality } from 'redux/actions/settings';
|
||||||
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import SettingDefaultQuality from './view';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
defaultQuality: selectClientSetting(state, SETTINGS.DEFAULT_VIDEO_QUALITY),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doSetDefaultVideoQuality,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(select, perform)(SettingDefaultQuality);
|
45
ui/component/settingDefaultQuality/view.jsx
Normal file
45
ui/component/settingDefaultQuality/view.jsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import { VIDEO_QUALITY_OPTIONS } from 'constants/player';
|
||||||
|
import { toCapitalCase } from 'util/string';
|
||||||
|
|
||||||
|
const OPTION_DISABLED = 'Disabled';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultQuality: ?string,
|
||||||
|
doSetDefaultVideoQuality: (value: ?string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingDefaultQuality(props: Props) {
|
||||||
|
const { defaultQuality, doSetDefaultVideoQuality } = props;
|
||||||
|
|
||||||
|
const valueRef = React.useRef();
|
||||||
|
const dropdownOptions = [OPTION_DISABLED, ...VIDEO_QUALITY_OPTIONS];
|
||||||
|
|
||||||
|
function handleSetQuality(e) {
|
||||||
|
const { value } = e.target;
|
||||||
|
|
||||||
|
doSetDefaultVideoQuality(value === OPTION_DISABLED ? null : value);
|
||||||
|
valueRef.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
name="default_video_quality"
|
||||||
|
type="select"
|
||||||
|
onChange={handleSetQuality}
|
||||||
|
value={defaultQuality || valueRef.current}
|
||||||
|
>
|
||||||
|
{dropdownOptions.map((option) => {
|
||||||
|
const qualityStr = typeof option === 'number' ? option + 'p' : toCapitalCase(option);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<option key={'quality' + qualityStr} value={option}>
|
||||||
|
{__(qualityStr)}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||||
import { selectDaemonSettings, selectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
import { selectDaemonSettings, selectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||||
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { search } = props.location;
|
const { search } = props.location;
|
||||||
|
@ -74,6 +75,7 @@ const select = (state, props) => {
|
||||||
videoTheaterMode: selectClientSetting(state, SETTINGS.VIDEO_THEATER_MODE),
|
videoTheaterMode: selectClientSetting(state, SETTINGS.VIDEO_THEATER_MODE),
|
||||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, getChannelIdFromClaim(claim)),
|
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, getChannelIdFromClaim(claim)),
|
||||||
isLivestreamClaim: isStreamPlaceholderClaim(claim),
|
isLivestreamClaim: isStreamPlaceholderClaim(claim),
|
||||||
|
defaultQuality: selectClientSetting(state, SETTINGS.DEFAULT_VIDEO_QUALITY),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,6 +103,7 @@ const perform = (dispatch) => ({
|
||||||
),
|
),
|
||||||
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
|
doToast: (props) => dispatch(doToast(props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(VideoViewer));
|
export default withRouter(connect(select, perform)(VideoViewer));
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
import { toCapitalCase } from 'util/string';
|
||||||
|
|
||||||
const VideoJsButtonClass = videojs.getComponent('MenuButton');
|
const VideoJsButtonClass = videojs.getComponent('MenuButton');
|
||||||
const VideoJsMenuClass = videojs.getComponent('Menu');
|
const VideoJsMenuClass = videojs.getComponent('Menu');
|
||||||
const VideoJsComponent = videojs.getComponent('Component');
|
const VideoJsComponent = videojs.getComponent('Component');
|
||||||
const Dom = videojs.dom;
|
const Dom = videojs.dom;
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert string to title case.
|
|
||||||
*
|
|
||||||
* @param {string} string - the string to convert
|
|
||||||
* @return {string} the returned titlecase string
|
|
||||||
*/
|
|
||||||
function toTitleCase(string) {
|
|
||||||
if (typeof string !== 'string') {
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend vjs button class for quality button.
|
* Extend vjs button class for quality button.
|
||||||
*/
|
*/
|
||||||
|
@ -59,7 +46,7 @@ export default class ConcreteButton extends VideoJsButtonClass {
|
||||||
if (this.options_.title) {
|
if (this.options_.title) {
|
||||||
const titleEl = Dom.createEl('li', {
|
const titleEl = Dom.createEl('li', {
|
||||||
className: 'vjs-menu-title',
|
className: 'vjs-menu-title',
|
||||||
innerHTML: toTitleCase(this.options_.title),
|
innerHTML: toCapitalCase(this.options_.title),
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
});
|
});
|
||||||
const titleComponent = new VideoJsComponent(this.player_, { el: titleEl });
|
const titleComponent = new VideoJsComponent(this.player_, { el: titleEl });
|
||||||
|
|
|
@ -3,6 +3,7 @@ import videojs from 'video.js';
|
||||||
import { version as VERSION } from './package.json';
|
import { version as VERSION } from './package.json';
|
||||||
import ConcreteButton from './ConcreteButton';
|
import ConcreteButton from './ConcreteButton';
|
||||||
import ConcreteMenuItem from './ConcreteMenuItem';
|
import ConcreteMenuItem from './ConcreteMenuItem';
|
||||||
|
import * as QUALITY_OPTIONS from 'constants/player';
|
||||||
|
|
||||||
// Default options for the plugin.
|
// Default options for the plugin.
|
||||||
const defaults = {};
|
const defaults = {};
|
||||||
|
@ -104,7 +105,7 @@ class HlsQualitySelectorPlugin {
|
||||||
|
|
||||||
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += icon;
|
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += icon;
|
||||||
} else {
|
} else {
|
||||||
this.setButtonInnerText('auto');
|
this.setButtonInnerText(QUALITY_OPTIONS.AUTO);
|
||||||
}
|
}
|
||||||
concreteButtonInstance.removeClass('vjs-hidden');
|
concreteButtonInstance.removeClass('vjs-hidden');
|
||||||
}
|
}
|
||||||
|
@ -134,10 +135,10 @@ class HlsQualitySelectorPlugin {
|
||||||
setButtonInnerText(text) {
|
setButtonInnerText(text) {
|
||||||
let str;
|
let str;
|
||||||
switch (text) {
|
switch (text) {
|
||||||
case 'auto':
|
case QUALITY_OPTIONS.AUTO:
|
||||||
str = __('Auto --[Video quality. Short form]--');
|
str = QUALITY_OPTIONS.AUTO;
|
||||||
break;
|
break;
|
||||||
case 'original':
|
case QUALITY_OPTIONS.ORIGINAL:
|
||||||
str = this.resolveOriginalQualityLabel(true, false);
|
str = this.resolveOriginalQualityLabel(true, false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -163,27 +164,52 @@ class HlsQualitySelectorPlugin {
|
||||||
/**
|
/**
|
||||||
* Executed when a quality level is added from HLS playlist.
|
* Executed when a quality level is added from HLS playlist.
|
||||||
*/
|
*/
|
||||||
onAddQualityLevel() {
|
onAddQualityLevel(e, qualityOption) {
|
||||||
const player = this.player;
|
const player = this.player;
|
||||||
|
const defaultQuality = qualityOption || this.config.defaultQuality;
|
||||||
const qualityList = player.qualityLevels();
|
const qualityList = player.qualityLevels();
|
||||||
const levels = qualityList.levels_ || [];
|
const levels = qualityList.levels_ || [];
|
||||||
const levelItems = [];
|
|
||||||
|
let levelItems = [];
|
||||||
|
let nextLowestQualityItem;
|
||||||
|
let nextLowestQualityItemObj;
|
||||||
|
|
||||||
for (let i = 0; i < levels.length; ++i) {
|
for (let i = 0; i < levels.length; ++i) {
|
||||||
if (
|
const currentHeight = levels[i].height;
|
||||||
!levelItems.filter((_existingItem) => {
|
|
||||||
return _existingItem.item && _existingItem.item.value === levels[i].height;
|
if (!levelItems.filter((_existingItem) => _existingItem.item?.value === currentHeight).length) {
|
||||||
}).length
|
const heightStr = currentHeight + 'p';
|
||||||
) {
|
|
||||||
const levelItem = this.getQualityMenuItem.call(this, {
|
const levelItem = this.getQualityMenuItem.call(this, {
|
||||||
label: levels[i].height + 'p',
|
label: heightStr,
|
||||||
value: levels[i].height,
|
value: currentHeight,
|
||||||
|
selected: defaultQuality ? currentHeight === defaultQuality : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isLiveOriginal = defaultQuality && defaultQuality === QUALITY_OPTIONS.ORIGINAL && player.isLivestream;
|
||||||
|
const shouldCheckHeight =
|
||||||
|
defaultQuality && !nextLowestQualityItem && (currentHeight <= defaultQuality || isLiveOriginal);
|
||||||
|
|
||||||
|
if (shouldCheckHeight) {
|
||||||
|
nextLowestQualityItem = levelItem;
|
||||||
|
nextLowestQualityItemObj = {
|
||||||
|
label: heightStr,
|
||||||
|
value: currentHeight,
|
||||||
|
selected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
levelItems.push(levelItem);
|
levelItems.push(levelItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextLowestQualityItem) {
|
||||||
|
levelItems = levelItems.map((item) =>
|
||||||
|
item === nextLowestQualityItem ? this.getQualityMenuItem.call(this, nextLowestQualityItemObj) : item
|
||||||
|
);
|
||||||
|
this._currentQuality = nextLowestQualityItemObj.value;
|
||||||
|
}
|
||||||
|
|
||||||
levelItems.sort((current, next) => {
|
levelItems.sort((current, next) => {
|
||||||
if (typeof current !== 'object' || typeof next !== 'object') {
|
if (typeof current !== 'object' || typeof next !== 'object') {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -201,20 +227,24 @@ class HlsQualitySelectorPlugin {
|
||||||
levelItems.push(
|
levelItems.push(
|
||||||
this.getQualityMenuItem.call(this, {
|
this.getQualityMenuItem.call(this, {
|
||||||
label: this.resolveOriginalQualityLabel(false, true),
|
label: this.resolveOriginalQualityLabel(false, true),
|
||||||
value: 'original',
|
value: QUALITY_OPTIONS.ORIGINAL,
|
||||||
selected: false,
|
selected: defaultQuality ? defaultQuality === QUALITY_OPTIONS.ORIGINAL : false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
levelItems.push(
|
levelItems.push(
|
||||||
this.getQualityMenuItem.call(this, {
|
this.getQualityMenuItem.call(this, {
|
||||||
label: __('Auto --[Video quality. Short form]--'),
|
label: QUALITY_OPTIONS.AUTO,
|
||||||
value: 'auto',
|
value: QUALITY_OPTIONS.AUTO,
|
||||||
selected: true,
|
selected: !defaultQuality ? true : defaultQuality === QUALITY_OPTIONS.AUTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.setButtonInnerText(
|
||||||
|
nextLowestQualityItemObj ? nextLowestQualityItemObj.label : defaultQuality || QUALITY_OPTIONS.AUTO
|
||||||
|
);
|
||||||
|
|
||||||
if (this._qualityButton) {
|
if (this._qualityButton) {
|
||||||
this._qualityButton.createItems = function () {
|
this._qualityButton.createItems = function () {
|
||||||
return levelItems;
|
return levelItems;
|
||||||
|
@ -223,13 +253,13 @@ class HlsQualitySelectorPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swapSrcTo(mode = 'original') {
|
swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) {
|
||||||
const currentTime = this.player.currentTime();
|
const currentTime = this.player.currentTime();
|
||||||
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal);
|
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal);
|
||||||
this.player.load();
|
this.player.load();
|
||||||
this.player.currentTime(currentTime);
|
this.player.currentTime(currentTime);
|
||||||
|
|
||||||
console.assert(mode === 'vhs' || mode === 'original', 'Unexpected input');
|
console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -239,22 +269,39 @@ class HlsQualitySelectorPlugin {
|
||||||
*/
|
*/
|
||||||
setQuality(height) {
|
setQuality(height) {
|
||||||
const qualityList = this.player.qualityLevels();
|
const qualityList = this.player.qualityLevels();
|
||||||
|
const { initialQualityChange, setInitialQualityChange, doToast } = this.config;
|
||||||
|
|
||||||
|
if (!initialQualityChange) {
|
||||||
|
doToast({
|
||||||
|
message: __('You can also change your default quality on settings.'),
|
||||||
|
linkText: __('Settings'),
|
||||||
|
linkTarget: '/settings',
|
||||||
|
});
|
||||||
|
setInitialQualityChange(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Set quality on plugin
|
// Set quality on plugin
|
||||||
this._currentQuality = height;
|
this._currentQuality = height;
|
||||||
|
|
||||||
if (this.config.displayCurrentQuality) {
|
if (this.config.displayCurrentQuality) {
|
||||||
this.setButtonInnerText(height === 'auto' ? 'auto' : height === 'original' ? 'original' : `${height}p`);
|
this.setButtonInnerText(
|
||||||
|
height === QUALITY_OPTIONS.AUTO
|
||||||
|
? QUALITY_OPTIONS.AUTO
|
||||||
|
: height === QUALITY_OPTIONS.ORIGINAL
|
||||||
|
? QUALITY_OPTIONS.ORIGINAL
|
||||||
|
: `${height}p`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < qualityList.length; ++i) {
|
for (let i = 0; i < qualityList.length; ++i) {
|
||||||
const quality = qualityList[i];
|
const quality = qualityList[i];
|
||||||
quality.enabled = quality.height === height || height === 'auto' || height === 'original';
|
quality.enabled =
|
||||||
|
quality.height === height || height === QUALITY_OPTIONS.AUTO || height === QUALITY_OPTIONS.ORIGINAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height === 'original') {
|
if (height === QUALITY_OPTIONS.ORIGINAL) {
|
||||||
if (this.player.currentSrc() !== this.player.claimSrcOriginal.src) {
|
if (this.player.currentSrc() !== this.player.claimSrcOriginal.src) {
|
||||||
setTimeout(() => this.swapSrcTo('original'));
|
setTimeout(() => this.swapSrcTo(QUALITY_OPTIONS.ORIGINAL));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this.player.isLivestream && this.player.currentSrc() !== this.player.claimSrcVhs.src) {
|
if (!this.player.isLivestream && this.player.currentSrc() !== this.player.claimSrcVhs.src) {
|
||||||
|
@ -265,7 +312,10 @@ class HlsQualitySelectorPlugin {
|
||||||
// Until we have "persistent quality" implemented, we need to do this
|
// Until we have "persistent quality" implemented, we need to do this
|
||||||
// because the VHS internals default to "auto" when initialized,
|
// because the VHS internals default to "auto" when initialized,
|
||||||
// causing a GUI mismatch.
|
// causing a GUI mismatch.
|
||||||
setTimeout(() => this.setQuality(height), 1000);
|
setTimeout(() => {
|
||||||
|
this.setQuality(height);
|
||||||
|
this.onAddQualityLevel(undefined, height);
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,7 +329,7 @@ class HlsQualitySelectorPlugin {
|
||||||
* @return {string} the currently set quality
|
* @return {string} the currently set quality
|
||||||
*/
|
*/
|
||||||
getCurrentQuality() {
|
getCurrentQuality() {
|
||||||
return this._currentQuality || 'auto';
|
return this._currentQuality || QUALITY_OPTIONS.AUTO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -205,19 +205,10 @@ export const lastBandwidthSelector = function() {
|
||||||
const hlsQualitySelector = player.hlsQualitySelector;
|
const hlsQualitySelector = player.hlsQualitySelector;
|
||||||
const originalHeight = hlsQualitySelector.config.originalHeight;
|
const originalHeight = hlsQualitySelector.config.originalHeight;
|
||||||
|
|
||||||
if (originalHeight && hlsQualitySelector) {
|
if (hlsQualitySelector?.getCurrentQuality() === 'auto') {
|
||||||
if (hlsQualitySelector.getCurrentQuality() === 'auto') {
|
hlsQualitySelector._qualityButton.menuButton_.$('.vjs-icon-placeholder').innerHTML = __('Auto %quality% --[Video quality popup. Long form.]--', { quality: selectedBandwidth.attributes.RESOLUTION.height + 'p' });
|
||||||
if (selectedBandwidth.attributes.RESOLUTION.height === originalHeight) {
|
|
||||||
if (player.claimSrcOriginal && player.currentSrc() !== player.claimSrcOriginal?.src) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentTime = player.currentTime();
|
|
||||||
player.src(player.claimSrcOriginal);
|
|
||||||
player.currentTime(currentTime);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return selectedBandwidth;
|
return selectedBandwidth;
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ import recsys from './plugins/videojs-recsys/plugin';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import { platform } from 'util/platform';
|
import { platform } from 'util/platform';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
const canAutoplay = require('./plugins/canAutoplay');
|
const canAutoplay = require('./plugins/canAutoplay');
|
||||||
|
|
||||||
|
@ -84,6 +85,7 @@ type Props = {
|
||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
userId: ?number,
|
userId: ?number,
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
|
defaultQuality: ?string,
|
||||||
onPlayerReady: (Player, any) => void,
|
onPlayerReady: (Player, any) => void,
|
||||||
playNext: () => void,
|
playNext: () => void,
|
||||||
playPrevious: () => void,
|
playPrevious: () => void,
|
||||||
|
@ -96,6 +98,7 @@ type Props = {
|
||||||
isLivestreamClaim: boolean,
|
isLivestreamClaim: boolean,
|
||||||
userClaimId: ?string,
|
userClaimId: ?string,
|
||||||
activeLivestreamForChannel: any,
|
activeLivestreamForChannel: any,
|
||||||
|
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
||||||
|
@ -144,6 +147,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
startMuted,
|
startMuted,
|
||||||
userId,
|
userId,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
|
defaultQuality,
|
||||||
onPlayerReady,
|
onPlayerReady,
|
||||||
playNext,
|
playNext,
|
||||||
playPrevious,
|
playPrevious,
|
||||||
|
@ -156,8 +160,16 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
userClaimId,
|
userClaimId,
|
||||||
isLivestreamClaim,
|
isLivestreamClaim,
|
||||||
activeLivestreamForChannel,
|
activeLivestreamForChannel,
|
||||||
|
doToast,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// used to notify about default quality setting
|
||||||
|
// if already has a quality set, no need to notify
|
||||||
|
const [initialQualityChange, setInitialQualityChange] = usePersistedState(
|
||||||
|
'initial-quality-change',
|
||||||
|
Boolean(defaultQuality)
|
||||||
|
);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const playerRef = useRef();
|
const playerRef = useRef();
|
||||||
|
@ -281,6 +293,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
player.hlsQualitySelector({
|
player.hlsQualitySelector({
|
||||||
displayCurrentQuality: true,
|
displayCurrentQuality: true,
|
||||||
originalHeight: claimValues?.video?.height,
|
originalHeight: claimValues?.video?.height,
|
||||||
|
defaultQuality,
|
||||||
|
initialQualityChange,
|
||||||
|
setInitialQualityChange,
|
||||||
|
doToast,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,6 @@ import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming
|
||||||
// const PLAY_TIMEOUT_LIMIT = 2000;
|
// const PLAY_TIMEOUT_LIMIT = 2000;
|
||||||
const PLAY_POSITION_SAVE_INTERVAL_MS = 15000;
|
const PLAY_POSITION_SAVE_INTERVAL_MS = 15000;
|
||||||
|
|
||||||
const USE_ORIGINAL_STREAM_FOR_OPTIMIZED_AUTO = false;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position: number,
|
position: number,
|
||||||
changeVolume: (number) => void,
|
changeVolume: (number) => void,
|
||||||
|
@ -71,6 +69,8 @@ type Props = {
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
isLivestreamClaim: boolean,
|
isLivestreamClaim: boolean,
|
||||||
activeLivestreamForChannel: any,
|
activeLivestreamForChannel: any,
|
||||||
|
defaultQuality: ?string,
|
||||||
|
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -115,6 +115,8 @@ function VideoViewer(props: Props) {
|
||||||
isMarkdownOrComment,
|
isMarkdownOrComment,
|
||||||
isLivestreamClaim,
|
isLivestreamClaim,
|
||||||
activeLivestreamForChannel,
|
activeLivestreamForChannel,
|
||||||
|
defaultQuality,
|
||||||
|
doToast,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const permanentUrl = claim && claim.permanent_url;
|
const permanentUrl = claim && claim.permanent_url;
|
||||||
|
@ -385,16 +387,14 @@ function VideoViewer(props: Props) {
|
||||||
// re-factoring.
|
// re-factoring.
|
||||||
player.on('loadedmetadata', () => restorePlaybackRate(player));
|
player.on('loadedmetadata', () => restorePlaybackRate(player));
|
||||||
|
|
||||||
// Override "auto" to use non-vhs url when the quality matches.
|
// Override the "auto" algorithm to post-process the result
|
||||||
if (USE_ORIGINAL_STREAM_FOR_OPTIMIZED_AUTO) {
|
player.on('loadedmetadata', () => {
|
||||||
player.on('loadedmetadata', () => {
|
const vhs = player.tech(true).vhs;
|
||||||
const vhs = player.tech(true).vhs;
|
if (vhs) {
|
||||||
if (vhs) {
|
// https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884
|
||||||
// https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884
|
vhs.selectPlaylist = lastBandwidthSelector;
|
||||||
vhs.selectPlaylist = lastBandwidthSelector;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for tracking buffering for watchman
|
// used for tracking buffering for watchman
|
||||||
player.on('tracking:buffered', doTrackingBuffered);
|
player.on('tracking:buffered', doTrackingBuffered);
|
||||||
|
@ -512,6 +512,8 @@ function VideoViewer(props: Props) {
|
||||||
userClaimId={claim && claim.signing_channel && claim.signing_channel.claim_id}
|
userClaimId={claim && claim.signing_channel && claim.signing_channel.claim_id}
|
||||||
isLivestreamClaim={isLivestreamClaim}
|
isLivestreamClaim={isLivestreamClaim}
|
||||||
activeLivestreamForChannel={activeLivestreamForChannel}
|
activeLivestreamForChannel={activeLivestreamForChannel}
|
||||||
|
defaultQuality={defaultQuality}
|
||||||
|
doToast={doToast}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1 +1,7 @@
|
||||||
export const VIDEO_ALMOST_FINISHED_THRESHOLD = 0.8;
|
export const VIDEO_ALMOST_FINISHED_THRESHOLD = 0.8;
|
||||||
|
|
||||||
|
// Quality Options
|
||||||
|
export const AUTO = 'auto';
|
||||||
|
export const ORIGINAL = 'original';
|
||||||
|
|
||||||
|
export const VIDEO_QUALITY_OPTIONS = [AUTO, ORIGINAL, 144, 240, 360, 480, 720, 1080];
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const TILE_LAYOUT = 'tile_layout';
|
||||||
export const VIDEO_THEATER_MODE = 'video_theater_mode';
|
export const VIDEO_THEATER_MODE = 'video_theater_mode';
|
||||||
export const VIDEO_PLAYBACK_RATE = 'video_playback_rate';
|
export const VIDEO_PLAYBACK_RATE = 'video_playback_rate';
|
||||||
export const PREFERRED_CURRENCY = 'preferred_currency';
|
export const PREFERRED_CURRENCY = 'preferred_currency';
|
||||||
|
export const DEFAULT_VIDEO_QUALITY = 'default_video_quality';
|
||||||
|
|
||||||
export const SETTINGS_GRP = {
|
export const SETTINGS_GRP = {
|
||||||
APPEARANCE: 'appearance',
|
APPEARANCE: 'appearance',
|
||||||
|
|
|
@ -493,3 +493,6 @@ export function toggleAutoplayNext() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const doSetDefaultVideoQuality = (value) => (dispatch) =>
|
||||||
|
dispatch(doSetClientSetting(SETTINGS.DEFAULT_VIDEO_QUALITY, value, true));
|
||||||
|
|
|
@ -76,6 +76,7 @@ const defaultState = {
|
||||||
[SETTINGS.AUTO_DOWNLOAD]: true,
|
[SETTINGS.AUTO_DOWNLOAD]: true,
|
||||||
[SETTINGS.HIDE_REPOSTS]: false,
|
[SETTINGS.HIDE_REPOSTS]: false,
|
||||||
[SETTINGS.HIDE_SCHEDULED_LIVESTREAMS]: false,
|
[SETTINGS.HIDE_SCHEDULED_LIVESTREAMS]: false,
|
||||||
|
[SETTINGS.DEFAULT_VIDEO_QUALITY]: null,
|
||||||
|
|
||||||
// OS
|
// OS
|
||||||
[SETTINGS.AUTO_LAUNCH]: true,
|
[SETTINGS.AUTO_LAUNCH]: true,
|
||||||
|
|
|
@ -311,6 +311,12 @@ button.vjs-big-play-button {
|
||||||
}
|
}
|
||||||
.vjs-quality-selector {
|
.vjs-quality-selector {
|
||||||
order: 13 !important;
|
order: 13 !important;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.vjs-icon-placeholder {
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.vjs-button--theater-mode {
|
.vjs-button--theater-mode {
|
||||||
order: 14 !important;
|
order: 14 !important;
|
||||||
|
|
Loading…
Reference in a new issue