Video: add original quality selection (#1328)
This commit is contained in:
commit
feb499b786
8 changed files with 419 additions and 42 deletions
|
@ -1,2 +1,3 @@
|
||||||
**/plugins/inline-attachment/**
|
**/plugins/inline-attachment/**
|
||||||
|
**/plugins/videojs-http-streaming--override/playlist-selectors.js
|
||||||
**/ui/constants/errors.js
|
**/ui/constants/errors.js
|
||||||
|
|
|
@ -1672,6 +1672,11 @@
|
||||||
"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 --[Video quality. Short form]--": "Auto",
|
||||||
|
"Orig (%quality%) --[Video quality popup. Short form.]--": "Orig (%quality%)",
|
||||||
|
"Original (%quality%) --[Video quality popup. Long form.]--": "Original (%quality%)",
|
||||||
|
"Original --[Video quality button. Abbreviate to fit space.]--": "Original",
|
||||||
|
"Original --[Video quality button. Long form.]--": "Original",
|
||||||
"Autoplay Next On": "Autoplay Next On",
|
"Autoplay Next On": "Autoplay Next On",
|
||||||
"Autoplay Next is on.": "Autoplay Next is on.",
|
"Autoplay Next is on.": "Autoplay Next is on.",
|
||||||
"Autoplay Next Off": "Autoplay Next Off",
|
"Autoplay Next Off": "Autoplay Next Off",
|
||||||
|
|
|
@ -50,12 +50,9 @@ class HlsQualitySelectorPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlugin() {
|
updatePlugin() {
|
||||||
// If there is the VHS tech
|
if (this.player.claimSrcVhs || this.player.isLivestream) {
|
||||||
if (this.getVhs()) {
|
|
||||||
// Show quality selector
|
|
||||||
this._qualityButton.show();
|
this._qualityButton.show();
|
||||||
} else {
|
} else {
|
||||||
// Hide quality selector
|
|
||||||
this._qualityButton.hide();
|
this._qualityButton.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +63,7 @@ class HlsQualitySelectorPlugin {
|
||||||
* @return {*} - videojs-http-streaming plugin.
|
* @return {*} - videojs-http-streaming plugin.
|
||||||
*/
|
*/
|
||||||
getHls() {
|
getHls() {
|
||||||
console.warn('hls-quality-selector: WARN: Using getHls options is deprecated. Use getVhs instead.')
|
console.warn('hls-quality-selector: WARN: Using getHls options is deprecated. Use getVhs instead.');
|
||||||
return this.getVhs();
|
return this.getVhs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,13 +109,43 @@ class HlsQualitySelectorPlugin {
|
||||||
concreteButtonInstance.removeClass('vjs-hidden');
|
concreteButtonInstance.removeClass('vjs-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveOriginalQualityLabel(abbreviatedForm, includeResolution) {
|
||||||
|
if (includeResolution && this.config.originalHeight) {
|
||||||
|
return abbreviatedForm
|
||||||
|
? __('Orig (%quality%) --[Video quality popup. Short form.]--', { quality: this.config.originalHeight + 'p' })
|
||||||
|
: __('Original (%quality%) --[Video quality popup. Long form.]--', {
|
||||||
|
quality: this.config.originalHeight + 'p',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// The allocated space for the button is fixed and happened to fit
|
||||||
|
// "Original", so we don't abbreviate for English. But it will most likely
|
||||||
|
// not fit for other languages, hence the 2 strings.
|
||||||
|
return abbreviatedForm
|
||||||
|
? __('Original --[Video quality button. Abbreviate to fit space.]--')
|
||||||
|
: __('Original --[Video quality button. Long form.]--');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*Set inner button text.
|
*Set inner button text.
|
||||||
*
|
*
|
||||||
* @param {string} text - the text to display in the button.
|
* @param {string} text - the text to display in the button.
|
||||||
*/
|
*/
|
||||||
setButtonInnerText(text) {
|
setButtonInnerText(text) {
|
||||||
this._qualityButton.menuButton_.$('.vjs-icon-placeholder').innerHTML = text;
|
let str;
|
||||||
|
switch (text) {
|
||||||
|
case 'auto':
|
||||||
|
str = __('Auto --[Video quality. Short form]--');
|
||||||
|
break;
|
||||||
|
case 'original':
|
||||||
|
str = this.resolveOriginalQualityLabel(true, false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
str = text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._qualityButton.menuButton_.$('.vjs-icon-placeholder').innerHTML = str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,9 +197,19 @@ class HlsQualitySelectorPlugin {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!player.isLivestream) {
|
||||||
|
levelItems.push(
|
||||||
|
this.getQualityMenuItem.call(this, {
|
||||||
|
label: this.resolveOriginalQualityLabel(false, true),
|
||||||
|
value: 'original',
|
||||||
|
selected: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
levelItems.push(
|
levelItems.push(
|
||||||
this.getQualityMenuItem.call(this, {
|
this.getQualityMenuItem.call(this, {
|
||||||
label: player.localize('Auto'),
|
label: __('Auto --[Video quality. Short form]--'),
|
||||||
value: 'auto',
|
value: 'auto',
|
||||||
selected: true,
|
selected: true,
|
||||||
})
|
})
|
||||||
|
@ -186,6 +223,15 @@ class HlsQualitySelectorPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swapSrcTo(mode = '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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets quality (based on media height)
|
* Sets quality (based on media height)
|
||||||
*
|
*
|
||||||
|
@ -198,14 +244,32 @@ class HlsQualitySelectorPlugin {
|
||||||
this._currentQuality = height;
|
this._currentQuality = height;
|
||||||
|
|
||||||
if (this.config.displayCurrentQuality) {
|
if (this.config.displayCurrentQuality) {
|
||||||
this.setButtonInnerText(height === 'auto' ? height : `${height}p`);
|
this.setButtonInnerText(height === 'auto' ? 'auto' : height === 'original' ? '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 === 'auto';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (height === 'original') {
|
||||||
|
if (this.player.currentSrc() !== this.player.claimSrcOriginal.src) {
|
||||||
|
setTimeout(() => this.swapSrcTo('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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._qualityButton.unpressButton();
|
this._qualityButton.unpressButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default {
|
||||||
|
GOAL_BUFFER_LENGTH: 30,
|
||||||
|
MAX_GOAL_BUFFER_LENGTH: 60,
|
||||||
|
GOAL_BUFFER_LENGTH_RATE: 1,
|
||||||
|
// 0.5 MB/s
|
||||||
|
INITIAL_BANDWIDTH: 4194304,
|
||||||
|
// A fudge factor to apply to advertised playlist bitrates to account for
|
||||||
|
// temporary flucations in client bandwidth
|
||||||
|
BANDWIDTH_VARIANCE: 1.2,
|
||||||
|
// How much of the buffer must be filled before we consider upswitching
|
||||||
|
BUFFER_LOW_WATER_LINE: 0,
|
||||||
|
MAX_BUFFER_LOW_WATER_LINE: 30,
|
||||||
|
BUFFER_LOW_WATER_LINE_RATE: 1,
|
||||||
|
};
|
|
@ -0,0 +1,223 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import window from 'global/window';
|
||||||
|
import Config from './config';
|
||||||
|
import Playlist from './playlist';
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CSS value for the specified property on an element
|
||||||
|
* using `getComputedStyle`. Firefox has a long-standing issue where
|
||||||
|
* getComputedStyle() may return null when running in an iframe with
|
||||||
|
* `display: none`.
|
||||||
|
*
|
||||||
|
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
||||||
|
* @param {HTMLElement} el the htmlelement to work on
|
||||||
|
* @param {string} the proprety to get the style for
|
||||||
|
*/
|
||||||
|
const safeGetComputedStyle = function(el, property) {
|
||||||
|
if (!el) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = window.getComputedStyle(el);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[property];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuable stable sort function
|
||||||
|
*
|
||||||
|
* @param {Playlists} array
|
||||||
|
* @param {Function} sortFn Different comparators
|
||||||
|
* @function stableSort
|
||||||
|
*/
|
||||||
|
const stableSort = function(array, sortFn) {
|
||||||
|
const newArray = array.slice();
|
||||||
|
|
||||||
|
array.sort(function(left, right) {
|
||||||
|
const cmp = sortFn(left, right);
|
||||||
|
|
||||||
|
if (cmp === 0) {
|
||||||
|
return newArray.indexOf(left) - newArray.indexOf(right);
|
||||||
|
}
|
||||||
|
return cmp;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses the appropriate media playlist based on bandwidth and player size
|
||||||
|
*
|
||||||
|
* @param {Object} master
|
||||||
|
* Object representation of the master manifest
|
||||||
|
* @param {number} playerBandwidth
|
||||||
|
* Current calculated bandwidth of the player
|
||||||
|
* @param {number} playerWidth
|
||||||
|
* Current width of the player element (should account for the device
|
||||||
|
* pixel ratio)
|
||||||
|
* @param {number} playerHeight
|
||||||
|
* Current height of the player element (should account for the device
|
||||||
|
* pixel ratio)
|
||||||
|
* @param {boolean} limitRenditionByPlayerDimensions
|
||||||
|
* True if the player width and height should be used during the
|
||||||
|
* selection, false otherwise
|
||||||
|
* @return {Playlist} the highest bitrate playlist less than the
|
||||||
|
* currently detected bandwidth, accounting for some amount of
|
||||||
|
* bandwidth variance
|
||||||
|
*/
|
||||||
|
export const simpleSelector = function(
|
||||||
|
master,
|
||||||
|
playerBandwidth,
|
||||||
|
playerWidth,
|
||||||
|
playerHeight,
|
||||||
|
limitRenditionByPlayerDimensions
|
||||||
|
) {
|
||||||
|
// convert the playlists to an intermediary representation to make comparisons easier
|
||||||
|
let sortedPlaylistReps = master.playlists.map((playlist) => {
|
||||||
|
let bandwidth;
|
||||||
|
const width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
|
||||||
|
const height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
|
||||||
|
|
||||||
|
bandwidth = playlist.attributes.BANDWIDTH;
|
||||||
|
|
||||||
|
bandwidth = bandwidth || window.Number.MAX_VALUE;
|
||||||
|
|
||||||
|
return {
|
||||||
|
bandwidth,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
playlist
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth);
|
||||||
|
|
||||||
|
// filter out any playlists that have been excluded due to
|
||||||
|
// incompatible configurations
|
||||||
|
sortedPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isIncompatible(rep.playlist));
|
||||||
|
|
||||||
|
// filter out any playlists that have been disabled manually through the representations
|
||||||
|
// api or blacklisted temporarily due to playback errors.
|
||||||
|
let enabledPlaylistReps = sortedPlaylistReps.filter((rep) => Playlist.isEnabled(rep.playlist));
|
||||||
|
|
||||||
|
if (!enabledPlaylistReps.length) {
|
||||||
|
// if there are no enabled playlists, then they have all been blacklisted or disabled
|
||||||
|
// by the user through the representations api. In this case, ignore blacklisting and
|
||||||
|
// fallback to what the user wants by using playlists the user has not disabled.
|
||||||
|
enabledPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isDisabled(rep.playlist));
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out any variant that has greater effective bitrate
|
||||||
|
// than the current estimated bandwidth
|
||||||
|
const bandwidthPlaylistReps = enabledPlaylistReps.filter((rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth);
|
||||||
|
|
||||||
|
let highestRemainingBandwidthRep =
|
||||||
|
bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
|
||||||
|
|
||||||
|
// get all of the renditions with the same (highest) bandwidth
|
||||||
|
// and then taking the very first element
|
||||||
|
const bandwidthBestRep = bandwidthPlaylistReps.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
||||||
|
|
||||||
|
// if we're not going to limit renditions by player size, make an early decision.
|
||||||
|
if (limitRenditionByPlayerDimensions === false) {
|
||||||
|
const chosenRep = (
|
||||||
|
bandwidthBestRep ||
|
||||||
|
enabledPlaylistReps[0] ||
|
||||||
|
sortedPlaylistReps[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
return chosenRep ? chosenRep.playlist : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out playlists without resolution information
|
||||||
|
const haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height);
|
||||||
|
|
||||||
|
// sort variants by resolution
|
||||||
|
stableSort(haveResolution, (left, right) => left.width - right.width);
|
||||||
|
|
||||||
|
// if we have the exact resolution as the player use it
|
||||||
|
const resolutionBestRepList = haveResolution.filter((rep) => rep.width === playerWidth && rep.height === playerHeight);
|
||||||
|
|
||||||
|
highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
|
||||||
|
// ensure that we pick the highest bandwidth variant that have exact resolution
|
||||||
|
const resolutionBestRep = resolutionBestRepList.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
||||||
|
|
||||||
|
let resolutionPlusOneList;
|
||||||
|
let resolutionPlusOneSmallest;
|
||||||
|
let resolutionPlusOneRep;
|
||||||
|
|
||||||
|
// find the smallest variant that is larger than the player
|
||||||
|
// if there is no match of exact resolution
|
||||||
|
if (!resolutionBestRep) {
|
||||||
|
resolutionPlusOneList = haveResolution.filter((rep) => rep.width > playerWidth || rep.height > playerHeight);
|
||||||
|
|
||||||
|
// find all the variants have the same smallest resolution
|
||||||
|
resolutionPlusOneSmallest = resolutionPlusOneList.filter((rep) => rep.width === resolutionPlusOneList[0].width &&
|
||||||
|
rep.height === resolutionPlusOneList[0].height);
|
||||||
|
|
||||||
|
// ensure that we also pick the highest bandwidth variant that
|
||||||
|
// is just-larger-than the video player
|
||||||
|
highestRemainingBandwidthRep =
|
||||||
|
resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
|
||||||
|
resolutionPlusOneRep = resolutionPlusOneSmallest.filter((rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback chain of variants
|
||||||
|
const chosenRep = (
|
||||||
|
resolutionPlusOneRep ||
|
||||||
|
resolutionBestRep ||
|
||||||
|
bandwidthBestRep ||
|
||||||
|
enabledPlaylistReps[0] ||
|
||||||
|
sortedPlaylistReps[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
return chosenRep ? chosenRep.playlist : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Playlist Selectors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses the appropriate media playlist based on the most recent
|
||||||
|
* bandwidth estimate and the player size.
|
||||||
|
*
|
||||||
|
* Expects to be called within the context of an instance of VhsHandler
|
||||||
|
*
|
||||||
|
* @return {Playlist} the highest bitrate playlist less than the
|
||||||
|
* currently detected bandwidth, accounting for some amount of
|
||||||
|
* bandwidth variance
|
||||||
|
*/
|
||||||
|
export const lastBandwidthSelector = function() {
|
||||||
|
const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
|
||||||
|
|
||||||
|
const selectedBandwidth = simpleSelector(
|
||||||
|
this.playlists.master,
|
||||||
|
this.systemBandwidth,
|
||||||
|
parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio,
|
||||||
|
parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio,
|
||||||
|
this.limitRenditionByPlayerDimensions
|
||||||
|
);
|
||||||
|
|
||||||
|
const player = this.player_;
|
||||||
|
const hlsQualitySelector = player.hlsQualitySelector;
|
||||||
|
const originalHeight = hlsQualitySelector.config.originalHeight;
|
||||||
|
|
||||||
|
if (originalHeight && hlsQualitySelector) {
|
||||||
|
if (hlsQualitySelector.getCurrentQuality() === 'auto') {
|
||||||
|
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;
|
||||||
|
};
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* @file playlist.js
|
||||||
|
*
|
||||||
|
* Playlist related utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the playlist is blacklisted or not.
|
||||||
|
*
|
||||||
|
* @param {Object} playlist the media playlist object
|
||||||
|
* @return {boolean} whether the playlist is blacklisted or not
|
||||||
|
* @function isBlacklisted
|
||||||
|
*/
|
||||||
|
export const isBlacklisted = function(playlist) {
|
||||||
|
return playlist.excludeUntil && playlist.excludeUntil > Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the playlist is compatible with current playback configuration or has
|
||||||
|
* been blacklisted permanently for being incompatible.
|
||||||
|
*
|
||||||
|
* @param {Object} playlist the media playlist object
|
||||||
|
* @return {boolean} whether the playlist is incompatible or not
|
||||||
|
* @function isIncompatible
|
||||||
|
*/
|
||||||
|
export const isIncompatible = function(playlist) {
|
||||||
|
return playlist.excludeUntil && playlist.excludeUntil === Infinity;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the playlist is enabled or not.
|
||||||
|
*
|
||||||
|
* @param {Object} playlist the media playlist object
|
||||||
|
* @return {boolean} whether the playlist is enabled or not
|
||||||
|
* @function isEnabled
|
||||||
|
*/
|
||||||
|
export const isEnabled = function(playlist) {
|
||||||
|
const blacklisted = isBlacklisted(playlist);
|
||||||
|
|
||||||
|
return (!playlist.disabled && !blacklisted);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the playlist has been manually disabled through the representations api.
|
||||||
|
*
|
||||||
|
* @param {Object} playlist the media playlist object
|
||||||
|
* @return {boolean} whether the playlist is disabled manually or not
|
||||||
|
* @function isDisabled
|
||||||
|
*/
|
||||||
|
export const isDisabled = function(playlist) {
|
||||||
|
return playlist.disabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isEnabled,
|
||||||
|
isDisabled,
|
||||||
|
isBlacklisted,
|
||||||
|
isIncompatible,
|
||||||
|
};
|
|
@ -30,10 +30,17 @@ require('@silvermine/videojs-chromecast')(videojs);
|
||||||
require('@silvermine/videojs-airplay')(videojs);
|
require('@silvermine/videojs-airplay')(videojs);
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
|
// -- custom --
|
||||||
|
claimSrcOriginal: ?{ src: string, type: string },
|
||||||
|
claimSrcVhs: ?{ src: string, type: string },
|
||||||
|
isLivestream?: boolean,
|
||||||
|
// -- original --
|
||||||
controlBar: { addChild: (string, any) => void },
|
controlBar: { addChild: (string, any) => void },
|
||||||
loadingSpinner: any,
|
loadingSpinner: any,
|
||||||
autoplay: (any) => boolean,
|
autoplay: (any) => boolean,
|
||||||
chromecast: (any) => void,
|
chromecast: (any) => void,
|
||||||
|
hlsQualitySelector: ?any,
|
||||||
|
tech: (?boolean) => { vhs: ?any },
|
||||||
currentTime: (?number) => number,
|
currentTime: (?number) => number,
|
||||||
dispose: () => void,
|
dispose: () => void,
|
||||||
duration: () => number,
|
duration: () => number,
|
||||||
|
@ -51,6 +58,8 @@ export type Player = {
|
||||||
playbackRate: (?number) => number,
|
playbackRate: (?number) => number,
|
||||||
readyState: () => number,
|
readyState: () => number,
|
||||||
requestFullscreen: () => boolean,
|
requestFullscreen: () => boolean,
|
||||||
|
src: ({ src: string, type: string }) => ?string,
|
||||||
|
currentSrc: () => string,
|
||||||
userActive: (?boolean) => boolean,
|
userActive: (?boolean) => boolean,
|
||||||
volume: (?number) => number,
|
volume: (?number) => number,
|
||||||
};
|
};
|
||||||
|
@ -276,7 +285,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add quality selector to player
|
// Add quality selector to player
|
||||||
if (showQualitySelector) player.hlsQualitySelector({ displayCurrentQuality: true });
|
if (showQualitySelector) {
|
||||||
|
player.hlsQualitySelector({
|
||||||
|
displayCurrentQuality: true,
|
||||||
|
originalHeight: claimValues?.video?.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add recsys plugin
|
// Add recsys plugin
|
||||||
if (shareTelemetry) {
|
if (shareTelemetry) {
|
||||||
|
@ -333,19 +347,17 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
// }
|
// }
|
||||||
// }, [showQualitySelector]);
|
// }, [showQualitySelector]);
|
||||||
|
|
||||||
/** instantiate videoJS and dispose of it when done with code **/
|
|
||||||
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
// test if perms to play video are available
|
|
||||||
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000, inline: true });
|
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000, inline: true });
|
||||||
|
|
||||||
canAutoplayVideo = canAutoplayVideo.result === true;
|
canAutoplayVideo = canAutoplayVideo.result === true;
|
||||||
|
|
||||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||||
|
|
||||||
// Initialize Video.js
|
|
||||||
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo);
|
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo);
|
||||||
|
if (!vjsPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add reference to player to global scope
|
// Add reference to player to global scope
|
||||||
window.player = vjsPlayer;
|
window.player = vjsPlayer;
|
||||||
|
@ -355,50 +367,35 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
|
|
||||||
window.addEventListener('keydown', curried_function(playerRef, containerRef));
|
window.addEventListener('keydown', curried_function(playerRef, containerRef));
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
const controlBar = document.querySelector('.vjs-control-bar');
|
const controlBar = document.querySelector('.vjs-control-bar');
|
||||||
if (controlBar) controlBar.style.setProperty('opacity', '1', 'important');
|
if (controlBar) {
|
||||||
|
controlBar.style.setProperty('opacity', '1', 'important');
|
||||||
|
}
|
||||||
|
|
||||||
if (isLivestreamClaim && userClaimId) {
|
if (isLivestreamClaim && userClaimId) {
|
||||||
// $FlowFixMe
|
vjsPlayer.isLivestream = true;
|
||||||
vjsPlayer.addClass('livestreamPlayer');
|
vjsPlayer.addClass('livestreamPlayer');
|
||||||
|
vjsPlayer.src({ type: 'application/x-mpegURL', src: livestreamVideoUrl });
|
||||||
// $FlowFixMe
|
|
||||||
vjsPlayer.src({
|
|
||||||
type: 'application/x-mpegURL',
|
|
||||||
src: livestreamVideoUrl,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// $FlowFixMe
|
vjsPlayer.isLivestream = false;
|
||||||
vjsPlayer.removeClass('livestreamPlayer');
|
vjsPlayer.removeClass('livestreamPlayer');
|
||||||
|
|
||||||
// change to m3u8 if applicable
|
// change to m3u8 if applicable
|
||||||
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
|
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
|
||||||
|
|
||||||
playerServerRef.current = response.headers.get('x-powered-by');
|
playerServerRef.current = response.headers.get('x-powered-by');
|
||||||
|
vjsPlayer.claimSrcOriginal = { type: sourceType, src: source };
|
||||||
|
|
||||||
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
|
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
|
||||||
// use m3u8 source
|
vjsPlayer.claimSrcVhs = { type: 'application/x-mpegURL', src: response.url };
|
||||||
// $FlowFixMe
|
vjsPlayer.src(vjsPlayer.claimSrcVhs);
|
||||||
vjsPlayer.src({
|
|
||||||
type: 'application/x-mpegURL',
|
|
||||||
src: response.url,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// use original mp4 source
|
vjsPlayer.src(vjsPlayer.claimSrcOriginal);
|
||||||
// $FlowFixMe
|
|
||||||
vjsPlayer.src({
|
|
||||||
type: sourceType,
|
|
||||||
src: source,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load video once source setup
|
|
||||||
// $FlowFixMe
|
|
||||||
vjsPlayer.load();
|
vjsPlayer.load();
|
||||||
|
|
||||||
// fix invisible vidcrunch overlay on IOS
|
// fix invisible vidcrunch overlay on IOS << TODO: does not belong here. Move to ads.jsx (#739)
|
||||||
if (IS_IOS) {
|
if (IS_IOS) {
|
||||||
// ads video player
|
// ads video player
|
||||||
const adsClaimDiv = document.querySelector('.ads__claim-item');
|
const adsClaimDiv = document.querySelector('.ads__claim-item');
|
||||||
|
|
|
@ -27,11 +27,14 @@ import type { HomepageCat } from 'util/buildHomepage';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
||||||
import useInterval from 'effects/use-interval';
|
import useInterval from 'effects/use-interval';
|
||||||
|
import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors';
|
||||||
|
|
||||||
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||||
// 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,
|
||||||
|
@ -382,6 +385,17 @@ 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.
|
||||||
|
if (USE_ORIGINAL_STREAM_FOR_OPTIMIZED_AUTO) {
|
||||||
|
player.on('loadedmetadata', () => {
|
||||||
|
const vhs = player.tech(true).vhs;
|
||||||
|
if (vhs) {
|
||||||
|
// https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue