2021-02-18 19:45:39 +01:00
|
|
|
/* eslint-disable */
|
2021-02-18 03:53:25 +01:00
|
|
|
import videojs from 'video.js';
|
2021-02-18 19:45:39 +01:00
|
|
|
import { version as VERSION } from './package.json';
|
2021-02-18 03:53:25 +01:00
|
|
|
import ConcreteButton from './ConcreteButton';
|
|
|
|
import ConcreteMenuItem from './ConcreteMenuItem';
|
2022-04-20 20:40:21 +02:00
|
|
|
import * as QUALITY_OPTIONS from 'constants/player';
|
2021-02-18 03:53:25 +01:00
|
|
|
|
|
|
|
// Default options for the plugin.
|
|
|
|
const defaults = {};
|
|
|
|
|
|
|
|
// Cross-compatibility for Video.js 5 and 6.
|
|
|
|
const registerPlugin = videojs.registerPlugin || videojs.plugin;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* VideoJS HLS Quality Selector Plugin class.
|
|
|
|
*/
|
|
|
|
class HlsQualitySelectorPlugin {
|
|
|
|
/**
|
|
|
|
* Plugin Constructor.
|
|
|
|
*
|
|
|
|
* @param {Player} player - The videojs player instance.
|
|
|
|
* @param {Object} options - The plugin options.
|
|
|
|
*/
|
|
|
|
constructor(player, options) {
|
|
|
|
this.player = player;
|
|
|
|
this.config = options;
|
|
|
|
|
2021-02-18 04:32:05 +01:00
|
|
|
// Ensure dependencies are met
|
2021-02-18 03:53:25 +01:00
|
|
|
if (!this.player.qualityLevels) {
|
2021-02-18 04:32:05 +01:00
|
|
|
console.error(`Error: Missing video.js quality levels plugin (required) - videojs-hls-quality-selector`);
|
2021-02-18 03:53:25 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setupPlugin();
|
|
|
|
}
|
|
|
|
|
|
|
|
setupPlugin() {
|
|
|
|
// Create the quality button.
|
|
|
|
this.createQualityButton();
|
|
|
|
|
2021-03-08 12:35:04 +01:00
|
|
|
// Hide quality selector by default
|
|
|
|
this._qualityButton.hide();
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
// Bind event listeners
|
|
|
|
this.bindPlayerEvents();
|
|
|
|
|
|
|
|
// Listen for source changes
|
|
|
|
this.player.on('loadedmetadata', (e) => {
|
|
|
|
this.updatePlugin();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
updatePlugin() {
|
2022-04-12 12:02:13 +02:00
|
|
|
if (this.player.claimSrcVhs || this.player.isLivestream) {
|
2021-02-18 03:53:25 +01:00
|
|
|
this._qualityButton.show();
|
|
|
|
} else {
|
|
|
|
this._qualityButton.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-09 01:33:56 +01:00
|
|
|
* Deprecated, returns VHS plugin
|
2021-02-18 03:53:25 +01:00
|
|
|
*
|
2021-03-09 01:33:56 +01:00
|
|
|
* @return {*} - videojs-http-streaming plugin.
|
2021-02-18 03:53:25 +01:00
|
|
|
*/
|
|
|
|
getHls() {
|
2022-04-11 12:20:50 +02:00
|
|
|
console.warn('hls-quality-selector: WARN: Using getHls options is deprecated. Use getVhs instead.');
|
2021-03-09 01:33:56 +01:00
|
|
|
return this.getVhs();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns VHS Plugin
|
|
|
|
*
|
|
|
|
* @return {*} - videojs-http-streaming plugin.
|
|
|
|
*/
|
|
|
|
getVhs() {
|
2021-03-09 01:19:24 +01:00
|
|
|
return this.player.tech({ IWillNotUseThisInPlugins: true }).vhs;
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds listener for quality level changes.
|
|
|
|
*/
|
|
|
|
bindPlayerEvents() {
|
|
|
|
this.player.qualityLevels().on('addqualitylevel', this.onAddQualityLevel.bind(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds the quality menu button to the player control bar.
|
|
|
|
*/
|
|
|
|
createQualityButton() {
|
|
|
|
const player = this.player;
|
|
|
|
|
|
|
|
this._qualityButton = new ConcreteButton(player);
|
|
|
|
|
|
|
|
const placementIndex = player.controlBar.children().length - 2;
|
2021-02-18 19:45:39 +01:00
|
|
|
const concreteButtonInstance = player.controlBar.addChild(
|
|
|
|
this._qualityButton,
|
|
|
|
{ componentClass: 'qualitySelector' },
|
|
|
|
this.config.placementIndex || placementIndex
|
|
|
|
);
|
2021-02-18 03:53:25 +01:00
|
|
|
|
|
|
|
concreteButtonInstance.addClass('vjs-quality-selector');
|
|
|
|
if (!this.config.displayCurrentQuality) {
|
|
|
|
const icon = ` ${this.config.vjsIconClass || 'vjs-icon-hd'}`;
|
|
|
|
|
2021-02-18 19:45:39 +01:00
|
|
|
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += icon;
|
2021-02-18 03:53:25 +01:00
|
|
|
} else {
|
2022-04-19 18:55:32 +02:00
|
|
|
this.setButtonInnerText(QUALITY_OPTIONS.AUTO);
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
|
|
|
concreteButtonInstance.removeClass('vjs-hidden');
|
|
|
|
}
|
|
|
|
|
2022-04-11 12:20:50 +02:00
|
|
|
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.]--');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
/**
|
|
|
|
*Set inner button text.
|
|
|
|
*
|
|
|
|
* @param {string} text - the text to display in the button.
|
|
|
|
*/
|
|
|
|
setButtonInnerText(text) {
|
2022-04-11 12:20:50 +02:00
|
|
|
let str;
|
|
|
|
switch (text) {
|
2022-04-19 18:55:32 +02:00
|
|
|
case QUALITY_OPTIONS.AUTO:
|
2022-04-22 13:35:30 +02:00
|
|
|
str = QUALITY_OPTIONS.AUTO;
|
2022-04-11 12:20:50 +02:00
|
|
|
break;
|
2022-04-19 18:55:32 +02:00
|
|
|
case QUALITY_OPTIONS.ORIGINAL:
|
2022-04-11 12:20:50 +02:00
|
|
|
str = this.resolveOriginalQualityLabel(true, false);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
str = text;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._qualityButton.menuButton_.$('.vjs-icon-placeholder').innerHTML = str;
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds individual quality menu items.
|
|
|
|
*
|
|
|
|
* @param {Object} item - Individual quality menu item.
|
|
|
|
* @return {ConcreteMenuItem} - Menu item
|
|
|
|
*/
|
|
|
|
getQualityMenuItem(item) {
|
|
|
|
const player = this.player;
|
|
|
|
|
|
|
|
return new ConcreteMenuItem(player, item, this._qualityButton, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executed when a quality level is added from HLS playlist.
|
|
|
|
*/
|
2022-04-20 20:38:18 +02:00
|
|
|
onAddQualityLevel(e, qualityOption) {
|
2021-02-18 03:53:25 +01:00
|
|
|
const player = this.player;
|
2022-04-20 20:38:18 +02:00
|
|
|
const defaultQuality = qualityOption || this.config.defaultQuality;
|
2021-02-18 03:53:25 +01:00
|
|
|
const qualityList = player.qualityLevels();
|
|
|
|
const levels = qualityList.levels_ || [];
|
2022-04-19 18:55:32 +02:00
|
|
|
|
|
|
|
let levelItems = [];
|
|
|
|
let nextLowestQualityItem;
|
|
|
|
let nextLowestQualityItemObj;
|
2021-02-18 03:53:25 +01:00
|
|
|
|
|
|
|
for (let i = 0; i < levels.length; ++i) {
|
2022-04-19 18:55:32 +02:00
|
|
|
const currentHeight = levels[i].height;
|
|
|
|
|
|
|
|
if (!levelItems.filter((_existingItem) => _existingItem.item?.value === currentHeight).length) {
|
|
|
|
const heightStr = currentHeight + 'p';
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
const levelItem = this.getQualityMenuItem.call(this, {
|
2022-04-19 18:55:32 +02:00
|
|
|
label: heightStr,
|
|
|
|
value: currentHeight,
|
|
|
|
selected: defaultQuality ? currentHeight === defaultQuality : undefined,
|
2021-02-18 03:53:25 +01:00
|
|
|
});
|
|
|
|
|
2022-04-26 00:04:35 +02:00
|
|
|
const isLiveOriginal = defaultQuality && defaultQuality === QUALITY_OPTIONS.ORIGINAL && player.isLivestream;
|
|
|
|
const shouldCheckHeight =
|
|
|
|
defaultQuality && !nextLowestQualityItem && (currentHeight <= defaultQuality || isLiveOriginal);
|
|
|
|
|
|
|
|
if (shouldCheckHeight) {
|
2022-04-19 18:55:32 +02:00
|
|
|
nextLowestQualityItem = levelItem;
|
|
|
|
nextLowestQualityItemObj = {
|
|
|
|
label: heightStr,
|
|
|
|
value: currentHeight,
|
|
|
|
selected: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
levelItems.push(levelItem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-19 18:55:32 +02:00
|
|
|
if (nextLowestQualityItem) {
|
|
|
|
levelItems = levelItems.map((item) =>
|
|
|
|
item === nextLowestQualityItem ? this.getQualityMenuItem.call(this, nextLowestQualityItemObj) : item
|
|
|
|
);
|
2022-04-22 13:35:30 +02:00
|
|
|
this._currentQuality = nextLowestQualityItemObj.value;
|
2022-04-19 18:55:32 +02:00
|
|
|
}
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
levelItems.sort((current, next) => {
|
2021-02-18 19:45:39 +01:00
|
|
|
if (typeof current !== 'object' || typeof next !== 'object') {
|
2021-02-18 03:53:25 +01:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (current.item.value < next.item.value) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (current.item.value > next.item.value) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
2022-04-12 12:02:13 +02:00
|
|
|
if (!player.isLivestream) {
|
|
|
|
levelItems.push(
|
|
|
|
this.getQualityMenuItem.call(this, {
|
|
|
|
label: this.resolveOriginalQualityLabel(false, true),
|
2022-04-19 18:55:32 +02:00
|
|
|
value: QUALITY_OPTIONS.ORIGINAL,
|
|
|
|
selected: defaultQuality ? defaultQuality === QUALITY_OPTIONS.ORIGINAL : false,
|
2022-04-12 12:02:13 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2022-04-11 12:20:50 +02:00
|
|
|
|
|
|
|
levelItems.push(
|
|
|
|
this.getQualityMenuItem.call(this, {
|
2022-04-22 13:35:30 +02:00
|
|
|
label: QUALITY_OPTIONS.AUTO,
|
2022-04-19 18:55:32 +02:00
|
|
|
value: QUALITY_OPTIONS.AUTO,
|
|
|
|
selected: !defaultQuality ? true : defaultQuality === QUALITY_OPTIONS.AUTO,
|
2021-02-18 19:45:39 +01:00
|
|
|
})
|
|
|
|
);
|
2021-02-18 03:53:25 +01:00
|
|
|
|
2022-04-19 20:08:24 +02:00
|
|
|
this.setButtonInnerText(
|
|
|
|
nextLowestQualityItemObj ? nextLowestQualityItemObj.label : defaultQuality || QUALITY_OPTIONS.AUTO
|
|
|
|
);
|
2022-04-19 18:55:32 +02:00
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
if (this._qualityButton) {
|
2021-02-18 19:45:39 +01:00
|
|
|
this._qualityButton.createItems = function () {
|
2021-02-18 03:53:25 +01:00
|
|
|
return levelItems;
|
|
|
|
};
|
|
|
|
this._qualityButton.update();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-19 18:55:32 +02:00
|
|
|
swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) {
|
2022-04-12 12:02:13 +02:00
|
|
|
const currentTime = this.player.currentTime();
|
|
|
|
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal);
|
|
|
|
this.player.load();
|
|
|
|
this.player.currentTime(currentTime);
|
|
|
|
|
2022-04-19 18:55:32 +02:00
|
|
|
console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input');
|
2022-04-12 12:02:13 +02:00
|
|
|
}
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
/**
|
|
|
|
* Sets quality (based on media height)
|
|
|
|
*
|
|
|
|
* @param {number} height - A number representing HLS playlist.
|
|
|
|
*/
|
|
|
|
setQuality(height) {
|
|
|
|
const qualityList = this.player.qualityLevels();
|
2022-04-25 15:28:36 +02:00
|
|
|
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);
|
|
|
|
}
|
2021-02-18 03:53:25 +01:00
|
|
|
|
|
|
|
// Set quality on plugin
|
|
|
|
this._currentQuality = height;
|
|
|
|
|
|
|
|
if (this.config.displayCurrentQuality) {
|
2022-04-19 18:55:32 +02:00
|
|
|
this.setButtonInnerText(
|
|
|
|
height === QUALITY_OPTIONS.AUTO
|
|
|
|
? QUALITY_OPTIONS.AUTO
|
|
|
|
: height === QUALITY_OPTIONS.ORIGINAL
|
|
|
|
? QUALITY_OPTIONS.ORIGINAL
|
|
|
|
: `${height}p`
|
|
|
|
);
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < qualityList.length; ++i) {
|
|
|
|
const quality = qualityList[i];
|
2022-04-19 18:55:32 +02:00
|
|
|
quality.enabled =
|
|
|
|
quality.height === height || height === QUALITY_OPTIONS.AUTO || height === QUALITY_OPTIONS.ORIGINAL;
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
2022-04-11 12:20:50 +02:00
|
|
|
|
2022-04-19 18:55:32 +02:00
|
|
|
if (height === QUALITY_OPTIONS.ORIGINAL) {
|
2022-04-12 12:02:13 +02:00
|
|
|
if (this.player.currentSrc() !== this.player.claimSrcOriginal.src) {
|
2022-04-19 18:55:32 +02:00
|
|
|
setTimeout(() => this.swapSrcTo(QUALITY_OPTIONS.ORIGINAL));
|
2022-04-12 12:02:13 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!this.player.isLivestream && this.player.currentSrc() !== this.player.claimSrcVhs.src) {
|
|
|
|
setTimeout(() => this.swapSrcTo('vhs'));
|
2022-04-20 20:38:18 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
this.onAddQualityLevel(undefined, height);
|
|
|
|
}, 1000);
|
|
|
|
}
|
2022-04-12 12:02:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-18 03:53:25 +01:00
|
|
|
this._qualityButton.unpressButton();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the current set quality or 'auto'
|
|
|
|
*
|
|
|
|
* @return {string} the currently set quality
|
|
|
|
*/
|
|
|
|
getCurrentQuality() {
|
2022-04-19 18:55:32 +02:00
|
|
|
return this._currentQuality || QUALITY_OPTIONS.AUTO;
|
2021-02-18 03:53:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function to invoke when the player is ready.
|
|
|
|
*
|
|
|
|
* This is a great place for your plugin to initialize itself. When this
|
|
|
|
* function is called, the player will have its DOM and child components
|
|
|
|
* in place.
|
|
|
|
*
|
|
|
|
* @function onPlayerReady
|
|
|
|
* @param {Player} player
|
|
|
|
* A Video.js player object.
|
|
|
|
*
|
|
|
|
* @param {Object} [options={}]
|
|
|
|
* A plain object containing options for the plugin.
|
|
|
|
*/
|
|
|
|
const onPlayerReady = (player, options) => {
|
|
|
|
player.addClass('vjs-hls-quality-selector');
|
|
|
|
player.hlsQualitySelector = new HlsQualitySelectorPlugin(player, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A video.js plugin.
|
|
|
|
*
|
|
|
|
* In the plugin function, the value of `this` is a video.js `Player`
|
|
|
|
* instance. You cannot rely on the player being in a "ready" state here,
|
|
|
|
* depending on how the plugin is invoked. This may or may not be important
|
|
|
|
* to you; if not, remove the wait for "ready"!
|
|
|
|
*
|
|
|
|
* @function hlsQualitySelector
|
|
|
|
* @param {Object} [options={}]
|
|
|
|
* An object of options left to the plugin author to define.
|
|
|
|
*/
|
2021-02-18 19:45:39 +01:00
|
|
|
const hlsQualitySelector = function (options) {
|
2021-02-18 03:53:25 +01:00
|
|
|
this.ready(() => {
|
|
|
|
onPlayerReady(this, videojs.mergeOptions(defaults, options));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Register the plugin with video.js.
|
|
|
|
registerPlugin('hlsQualitySelector', hlsQualitySelector);
|
|
|
|
|
|
|
|
// Include the version number.
|
|
|
|
hlsQualitySelector.VERSION = VERSION;
|
|
|
|
|
|
|
|
export default hlsQualitySelector;
|
2021-02-18 19:45:39 +01:00
|
|
|
/* eslint-enable */
|