Store quality option and use it on all videos #1356

This commit is contained in:
Rafael 2022-04-26 22:39:02 +08:00 committed by infinite-persistence
commit d8ab1eb960
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
15 changed files with 203 additions and 65 deletions

View file

@ -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--"
} }

View file

@ -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.',
}; };

View 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);

View 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>
);
}

View file

@ -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));

View file

@ -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 });

View file

@ -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;
} }
} }

View file

@ -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;
}; };

View file

@ -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,
}); });
} }

View file

@ -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,8 +387,7 @@ 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) {
@ -394,7 +395,6 @@ function VideoViewer(props: Props) {
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>
); );

View file

@ -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];

View file

@ -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',

View file

@ -493,3 +493,6 @@ export function toggleAutoplayNext() {
); );
}; };
} }
export const doSetDefaultVideoQuality = (value) => (dispatch) =>
dispatch(doSetClientSetting(SETTINGS.DEFAULT_VIDEO_QUALITY, value, true));

View file

@ -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,

View file

@ -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;