Add ability to store quality settings as default
This commit is contained in:
parent
7390c464ac
commit
73f27cc67e
12 changed files with 179 additions and 46 deletions
|
@ -12,6 +12,7 @@ import Card from 'component/common/card';
|
|||
import { FormField, FormFieldPrice } from 'component/common/form';
|
||||
import MaxPurchasePrice from 'component/maxPurchasePrice';
|
||||
import SettingsRow from 'component/settingsRow';
|
||||
import SettingDefaultQuality from 'component/settingDefaultQuality';
|
||||
|
||||
type Price = {
|
||||
currency: string,
|
||||
|
@ -119,6 +120,10 @@ export default function SettingContent(props: Props) {
|
|||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow title={__('Default Video Quality')} subtitle={__(HELP.DEFAULT_VIDEO_QUALITY)}>
|
||||
<SettingDefaultQuality />
|
||||
</SettingsRow>
|
||||
|
||||
{!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.',
|
||||
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.',
|
||||
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);
|
70
ui/component/settingDefaultQuality/view.jsx
Normal file
70
ui/component/settingDefaultQuality/view.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { VIDEO_QUALITY_OPTIONS } from 'constants/video';
|
||||
import { toCapitalCase } from 'util/string';
|
||||
|
||||
type Props = {
|
||||
defaultQuality: ?string,
|
||||
doSetDefaultVideoQuality: (value: ?string) => void,
|
||||
};
|
||||
|
||||
export default function SettingDefaultQuality(props: Props) {
|
||||
const { defaultQuality, doSetDefaultVideoQuality } = props;
|
||||
|
||||
const [enabled, setEnabled] = React.useState<boolean>(Boolean(defaultQuality));
|
||||
|
||||
const valueRef = React.useRef(VIDEO_QUALITY_OPTIONS[0]);
|
||||
|
||||
function handleEnable() {
|
||||
if (enabled) {
|
||||
setEnabled(false);
|
||||
// From enabled to disabled -> clear the setting
|
||||
doSetDefaultVideoQuality(null);
|
||||
} else {
|
||||
setEnabled(true);
|
||||
// From to disabled to enabled -> set the current shown value
|
||||
doSetDefaultVideoQuality(valueRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetQuality(e) {
|
||||
const { value } = e.target;
|
||||
doSetDefaultVideoQuality(value);
|
||||
valueRef.current = value;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset-section>
|
||||
<FormField
|
||||
name="default_video_quality"
|
||||
type="select"
|
||||
onChange={handleSetQuality}
|
||||
disabled={!enabled}
|
||||
value={defaultQuality}
|
||||
>
|
||||
{VIDEO_QUALITY_OPTIONS.map((quality) => {
|
||||
const qualityStr = typeof quality === 'number' ? quality + 'p' : toCapitalCase(quality);
|
||||
|
||||
return (
|
||||
<option key={'quality' + qualityStr} value={quality}>
|
||||
{qualityStr}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</FormField>
|
||||
</fieldset-section>
|
||||
|
||||
<fieldset-section>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="enable_default_quality"
|
||||
onChange={handleEnable}
|
||||
checked={enabled}
|
||||
label={__('Enable default quality setting')}
|
||||
/>
|
||||
</fieldset-section>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -23,7 +23,12 @@ import VideoViewer from './view';
|
|||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { selectDaemonSettings, selectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
||||
import {
|
||||
toggleVideoTheaterMode,
|
||||
toggleAutoplayNext,
|
||||
doSetClientSetting,
|
||||
doSetDefaultVideoQuality,
|
||||
} from 'redux/actions/settings';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -74,6 +79,7 @@ const select = (state, props) => {
|
|||
videoTheaterMode: selectClientSetting(state, SETTINGS.VIDEO_THEATER_MODE),
|
||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, getChannelIdFromClaim(claim)),
|
||||
isLivestreamClaim: isStreamPlaceholderClaim(claim),
|
||||
defaultQuality: selectClientSetting(state, SETTINGS.DEFAULT_VIDEO_QUALITY),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -101,6 +107,7 @@ const perform = (dispatch) => ({
|
|||
),
|
||||
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
doSetDefaultVideoQuality: (value) => dispatch(doSetDefaultVideoQuality(value)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(VideoViewer));
|
||||
|
|
|
@ -1,24 +1,11 @@
|
|||
import videojs from 'video.js';
|
||||
import { toCapitalCase } from 'util/string';
|
||||
|
||||
const VideoJsButtonClass = videojs.getComponent('MenuButton');
|
||||
const VideoJsMenuClass = videojs.getComponent('Menu');
|
||||
const VideoJsComponent = videojs.getComponent('Component');
|
||||
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.
|
||||
*/
|
||||
|
@ -59,7 +46,7 @@ export default class ConcreteButton extends VideoJsButtonClass {
|
|||
if (this.options_.title) {
|
||||
const titleEl = Dom.createEl('li', {
|
||||
className: 'vjs-menu-title',
|
||||
innerHTML: toTitleCase(this.options_.title),
|
||||
innerHTML: toCapitalCase(this.options_.title),
|
||||
tabIndex: -1,
|
||||
});
|
||||
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 ConcreteButton from './ConcreteButton';
|
||||
import ConcreteMenuItem from './ConcreteMenuItem';
|
||||
import * as QUALITY_OPTIONS from 'constants/video';
|
||||
|
||||
// Default options for the plugin.
|
||||
const defaults = {};
|
||||
|
@ -104,7 +105,7 @@ class HlsQualitySelectorPlugin {
|
|||
|
||||
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += icon;
|
||||
} else {
|
||||
this.setButtonInnerText('auto');
|
||||
this.setButtonInnerText(QUALITY_OPTIONS.AUTO);
|
||||
}
|
||||
concreteButtonInstance.removeClass('vjs-hidden');
|
||||
}
|
||||
|
@ -134,10 +135,10 @@ class HlsQualitySelectorPlugin {
|
|||
setButtonInnerText(text) {
|
||||
let str;
|
||||
switch (text) {
|
||||
case 'auto':
|
||||
case QUALITY_OPTIONS.AUTO:
|
||||
str = __('Auto --[Video quality. Short form]--');
|
||||
break;
|
||||
case 'original':
|
||||
case QUALITY_OPTIONS.ORIGINAL:
|
||||
str = this.resolveOriginalQualityLabel(true, false);
|
||||
break;
|
||||
default:
|
||||
|
@ -165,25 +166,45 @@ class HlsQualitySelectorPlugin {
|
|||
*/
|
||||
onAddQualityLevel() {
|
||||
const player = this.player;
|
||||
const { defaultQuality } = this.config;
|
||||
const qualityList = player.qualityLevels();
|
||||
const levels = qualityList.levels_ || [];
|
||||
const levelItems = [];
|
||||
|
||||
let levelItems = [];
|
||||
let nextLowestQualityItem;
|
||||
let nextLowestQualityItemObj;
|
||||
|
||||
for (let i = 0; i < levels.length; ++i) {
|
||||
if (
|
||||
!levelItems.filter((_existingItem) => {
|
||||
return _existingItem.item && _existingItem.item.value === levels[i].height;
|
||||
}).length
|
||||
) {
|
||||
const currentHeight = levels[i].height;
|
||||
|
||||
if (!levelItems.filter((_existingItem) => _existingItem.item?.value === currentHeight).length) {
|
||||
const heightStr = currentHeight + 'p';
|
||||
|
||||
const levelItem = this.getQualityMenuItem.call(this, {
|
||||
label: levels[i].height + 'p',
|
||||
value: levels[i].height,
|
||||
label: heightStr,
|
||||
value: currentHeight,
|
||||
selected: defaultQuality ? currentHeight === defaultQuality : undefined,
|
||||
});
|
||||
|
||||
if (defaultQuality && !nextLowestQualityItem && currentHeight <= defaultQuality) {
|
||||
nextLowestQualityItem = levelItem;
|
||||
nextLowestQualityItemObj = {
|
||||
label: heightStr,
|
||||
value: currentHeight,
|
||||
selected: true,
|
||||
};
|
||||
}
|
||||
|
||||
levelItems.push(levelItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextLowestQualityItem) {
|
||||
levelItems = levelItems.map((item) =>
|
||||
item === nextLowestQualityItem ? this.getQualityMenuItem.call(this, nextLowestQualityItemObj) : item
|
||||
);
|
||||
}
|
||||
|
||||
levelItems.sort((current, next) => {
|
||||
if (typeof current !== 'object' || typeof next !== 'object') {
|
||||
return -1;
|
||||
|
@ -201,8 +222,8 @@ class HlsQualitySelectorPlugin {
|
|||
levelItems.push(
|
||||
this.getQualityMenuItem.call(this, {
|
||||
label: this.resolveOriginalQualityLabel(false, true),
|
||||
value: 'original',
|
||||
selected: false,
|
||||
value: QUALITY_OPTIONS.ORIGINAL,
|
||||
selected: defaultQuality ? defaultQuality === QUALITY_OPTIONS.ORIGINAL : false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -210,11 +231,15 @@ class HlsQualitySelectorPlugin {
|
|||
levelItems.push(
|
||||
this.getQualityMenuItem.call(this, {
|
||||
label: __('Auto --[Video quality. Short form]--'),
|
||||
value: 'auto',
|
||||
selected: true,
|
||||
value: QUALITY_OPTIONS.AUTO,
|
||||
selected: !defaultQuality ? true : defaultQuality === QUALITY_OPTIONS.AUTO,
|
||||
})
|
||||
);
|
||||
|
||||
if (nextLowestQualityItemObj || defaultQuality) {
|
||||
this.setButtonInnerText(nextLowestQualityItemObj ? nextLowestQualityItemObj.label : defaultQuality);
|
||||
}
|
||||
|
||||
if (this._qualityButton) {
|
||||
this._qualityButton.createItems = function () {
|
||||
return levelItems;
|
||||
|
@ -223,13 +248,13 @@ class HlsQualitySelectorPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
swapSrcTo(mode = 'original') {
|
||||
swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) {
|
||||
const currentTime = this.player.currentTime();
|
||||
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal);
|
||||
this.player.load();
|
||||
this.player.currentTime(currentTime);
|
||||
|
||||
console.assert(mode === 'vhs' || mode === 'original', 'Unexpected input');
|
||||
console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,35 +263,36 @@ class HlsQualitySelectorPlugin {
|
|||
* @param {number} height - A number representing HLS playlist.
|
||||
*/
|
||||
setQuality(height) {
|
||||
const { doSetDefaultVideoQuality } = this.config;
|
||||
const qualityList = this.player.qualityLevels();
|
||||
|
||||
// Set quality on plugin
|
||||
this._currentQuality = height;
|
||||
doSetDefaultVideoQuality(height);
|
||||
|
||||
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) {
|
||||
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) {
|
||||
setTimeout(() => this.swapSrcTo('original'));
|
||||
setTimeout(() => this.swapSrcTo(QUALITY_OPTIONS.ORIGINAL));
|
||||
}
|
||||
} else {
|
||||
if (!this.player.isLivestream && this.player.currentSrc() !== this.player.claimSrcVhs.src) {
|
||||
setTimeout(() => this.swapSrcTo('vhs'));
|
||||
|
||||
if (height !== 'auto') {
|
||||
// -- Re-select quality --
|
||||
// Until we have "persistent quality" implemented, we need to do this
|
||||
// because the VHS internals default to "auto" when initialized,
|
||||
// causing a GUI mismatch.
|
||||
setTimeout(() => this.setQuality(height), 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +305,7 @@ class HlsQualitySelectorPlugin {
|
|||
* @return {string} the currently set quality
|
||||
*/
|
||||
getCurrentQuality() {
|
||||
return this._currentQuality || 'auto';
|
||||
return this._currentQuality || QUALITY_OPTIONS.AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,8 @@ type Props = {
|
|||
startMuted: boolean,
|
||||
userId: ?number,
|
||||
videoTheaterMode: boolean,
|
||||
defaultQuality: ?string,
|
||||
doSetDefaultVideoQuality: (value: ?string) => void,
|
||||
onPlayerReady: (Player, any) => void,
|
||||
playNext: () => void,
|
||||
playPrevious: () => void,
|
||||
|
@ -144,6 +146,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
startMuted,
|
||||
userId,
|
||||
videoTheaterMode,
|
||||
defaultQuality,
|
||||
doSetDefaultVideoQuality,
|
||||
onPlayerReady,
|
||||
playNext,
|
||||
playPrevious,
|
||||
|
@ -281,6 +285,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.hlsQualitySelector({
|
||||
displayCurrentQuality: true,
|
||||
originalHeight: claimValues?.video?.height,
|
||||
defaultQuality,
|
||||
doSetDefaultVideoQuality,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,8 @@ type Props = {
|
|||
claimRewards: () => void,
|
||||
isLivestreamClaim: boolean,
|
||||
activeLivestreamForChannel: any,
|
||||
defaultQuality: ?string,
|
||||
doSetDefaultVideoQuality: (value: ?string) => void,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -115,6 +117,8 @@ function VideoViewer(props: Props) {
|
|||
isMarkdownOrComment,
|
||||
isLivestreamClaim,
|
||||
activeLivestreamForChannel,
|
||||
defaultQuality,
|
||||
doSetDefaultVideoQuality,
|
||||
} = props;
|
||||
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
|
@ -512,6 +516,8 @@ function VideoViewer(props: Props) {
|
|||
userClaimId={claim && claim.signing_channel && claim.signing_channel.claim_id}
|
||||
isLivestreamClaim={isLivestreamClaim}
|
||||
activeLivestreamForChannel={activeLivestreamForChannel}
|
||||
defaultQuality={defaultQuality}
|
||||
doSetDefaultVideoQuality={doSetDefaultVideoQuality}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -43,6 +43,7 @@ export const TILE_LAYOUT = 'tile_layout';
|
|||
export const VIDEO_THEATER_MODE = 'video_theater_mode';
|
||||
export const VIDEO_PLAYBACK_RATE = 'video_playback_rate';
|
||||
export const PREFERRED_CURRENCY = 'preferred_currency';
|
||||
export const DEFAULT_VIDEO_QUALITY = 'default_video_quality';
|
||||
|
||||
export const SETTINGS_GRP = {
|
||||
APPEARANCE: 'appearance',
|
||||
|
|
5
ui/constants/video.js
Normal file
5
ui/constants/video.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Quality Options
|
||||
export const AUTO = 'auto';
|
||||
export const ORIGINAL = 'original';
|
||||
|
||||
export const VIDEO_QUALITY_OPTIONS = [AUTO, ORIGINAL, 144, 240, 360, 480, 720, 1080];
|
|
@ -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.HIDE_REPOSTS]: false,
|
||||
[SETTINGS.HIDE_SCHEDULED_LIVESTREAMS]: false,
|
||||
[SETTINGS.DEFAULT_VIDEO_QUALITY]: null,
|
||||
|
||||
// OS
|
||||
[SETTINGS.AUTO_LAUNCH]: true,
|
||||
|
|
Loading…
Reference in a new issue