UNPKG

videojs-max-quality-selector

Version:

A Videojs Plugin to help you list out resolutions and bit-rates from Live, Adaptive and Progressive streams.

662 lines (637 loc) 20.6 kB
/*! @name videojs-max-quality-selector @version 1.0.0 @license MIT */ 'use strict'; var videojs = require('video.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); const MenuButton = videojs__default["default"].getComponent('MenuButton'); const MenuItem = videojs__default["default"].getComponent('MenuItem'); const Menu = videojs__default["default"].getComponent('Menu'); const Dom = videojs__default["default"].dom; // Default options for the plugin. const defaults$1 = { parent: null }; class MaxQualityButton extends MenuButton { /** * QualityButton constructor * * @param {Player} player - videojs player instance * @param {Object} options - component options */ constructor(player, options) { super(player, options); this.options = videojs__default["default"].mergeOptions(defaults$1, options); this.parent = this.options.parent; this.items = []; this.addClass('vjs-max-quality-selector-button'); } handleMenuItemClick(e) { const selectedIndex = parseInt(e.currentTarget.dataset.id, 10); this.parent.changeLevel(selectedIndex); } handleSubmenuKeyPress(e) { if (e.currentTarget.dataset.id === undefined) { return; } const selectedIndex = parseInt(e.currentTarget.dataset.id, 10); this.parent.changeLevel(selectedIndex); } createButton(menu, cssClass, text, id) { const buttonEl = Dom.createEl('li', { className: cssClass, innerHTML: text, tabIndex: -1 }, { 'data-id': id }); const menuItem = new MenuItem(this.player_, { el: buttonEl }); menuItem.on('click', this.handleMenuItemClick.bind(this)); menu.addItem(menuItem); } createMenu() { const menu = new Menu(this.player_, { menuButton: this }); const uniqueEntries = []; const uniqueHeights = []; if (this.items) { if (!this.parent.autoMode && !this.parent.options.disableAuto) { this.createButton(menu, 'vjs-menu-item', this.parent.options.autoLabel, -1); } for (let i = 0; i < this.items.length; i++) { const quality = this.items[i]; if (this.parent.options.filterDuplicates && uniqueEntries.includes(quality.uniqueId)) { continue; } else { uniqueEntries.push(quality.uniqueId); } if (this.parent.options.filterDuplicateHeights && uniqueHeights.includes(quality.height)) { continue; } else { uniqueHeights.push(quality.height); } let elClass = 'vjs-menu-item'; elClass += quality.isCurrent ? ' vjs-selected' : ''; this.createButton(menu, elClass, this.parent.getQualityDisplayString(quality), quality.id); } if (!this.parent.options.showSingleItemMenu && menu.children_.length === 1) { return new Menu(this.player_, { menuButton: this }); } } return menu; } } videojs__default["default"].registerComponent('MaxQualityButton', MaxQualityButton); var version = "1.0.0"; const Plugin = videojs__default["default"].getPlugin('plugin'); /** * @constant * @kind class * @alias DefaultOptions */ const defaults = { /** * This option helps you position the button in the VideoJS control bar. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'index': -2 // Put the button before the closed-captioning button. * }); * * @member {number} * @default -1 */ index: -1, /** * This option lets you rename the string value that represents the auto bitrate selection system. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'autoLabel': 'ABR' // Change the label from 'Auto' (default) to 'ABR'. * }); * * @member {string} * @default 'Auto' */ autoLabel: 'Auto', /** * This option lets you control which level of quality is selected first. * * 0: Default Behaviour (The default from playlist) * 1: Lowest (Start the video with the lowest quality stream selected) * 2: Highest (Start the video with the highest quality stream selected) * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'defaultQuality': 2 // Make the video start playing at the highest quality possible * }); * * @member {number} * @default 0 */ defaultQuality: 0, /** * This option lets you control how the default quality level is displayed to the screen. * (Note: This option is ignored if you override the quality level with a label in {@link DefaultOptions.labels}) * * 0: Both (Includes both the resolution, in height, and the quality marketing name) * 1: Resolution (Include just the resolution, in height) * 2: Name (Include just the quality marketing name) * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'displayMode': 1 // Only render out the height name of the video in the quality button and list * }); * * @member {number} * @default 0 */ displayMode: 0, /** * This options lets you specify the minimum height resolution to show in the menu. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'minHeight': 480 // Do not list any resolutions smaller than 480p. * }); * * @member {number} * @default 0 */ minHeight: 0, /** * This options lets you specify the maximum height resolution to show in the menu. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'maxHeight': 1080 // Do not list any resolutions larger than 1080p. * }); * * @member {number} * @default 0 */ maxHeight: 0, /** * This options lets you override the name of the listed quality levels. * * Tip: Use {@link MaxQualitySelector#getLevelNames} output to find the ID to overwrite. * * @example * var player = videojs('my-video'); * * // Quick and useful if only a few contiguous quality levels * var labelsArray = [ 'High', 'Low' ]; * * // Useful if you need to specify labels in a sparce list * var labelsObject = { 0: 'High', 8: 'Medium', 16: 'Low', 24: 'Super Low' }; * * player.maxQualitySelector({ * 'labels': labelsArray | labelsObject * }); * * @member {Array|Object} * @default [] */ labels: [], /** * This option disables the auto bitrate selection system and focuses on a single quality level * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'disableAuto': true // Turn off the auto bitrate selection system * }); * * @member {boolean} * @default false */ disableAuto: false, /** * This option enabled the filtering of duplicate quality levels when their *width*, *height*, *bitrate* all match. * * Tip: This is useful if you want to avoid showing different endpoints to users. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'filterDuplicates': false // Turn off filtering of duplicate quality levels * }); * * @member {boolean} * @default true */ filterDuplicates: true, /** * This option enabled the filtering of duplicate quality levels when their *height* all match. * * Tip: This is useful if you want to avoid showing different bitrates to users. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'filterDuplicateHeights': false // Turn off filtering of duplicate quality levels with different bitrates * }); * * @member {boolean} * @default true */ filterDuplicateHeights: true, /** * This option enabled to show the menu even if there is only one quality level. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'showSingleItemMenu': true // Turn off hidding menu if there is only one quality level. * }); * * @member {boolean} * @default false */ showSingleItemMenu: false, /** * This option enables showing the bitrate in the button and menu. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'showBitrates': true // Turn on showing bitrates in the button and menu. * }); * * @member {boolean} * @default false */ showBitrates: false, /** * This option enables sorting the quality levels in the menu. * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'sortEnabled': false // List the quality levels as they have been specified. * }); * * @member {boolean} * @default true */ sortEnabled: true, /** * This option enables sorting direction the quality levels in the menu. * * 0: Descending (Qualities are listed from highest to lowest top down by *height*) * 1: Ascending (Qualities are listed from lowest to highest top down by *height*) * * @example * var player = videojs('my-video'); * player.maxQualitySelector({ * 'sort': 1 // List the qualities from lowest to highest top down. * }); * * @member {number} * @default 0 */ sort: 0 }; /** * A Videojs Plugin to help you list out resolutions and bit-rates from Live, Adaptive and Progressive streams. * * GitHub: {@link https://github.com/FoxCouncil/videojs-max-quality-selector} */ class MaxQualitySelector extends Plugin { /** * Create a MaxQualitySelector plugin instance. You generally should not ever need to call this manually, * however, if you do, make sure you pass a working player! * * @param {Player} player * A Video.js Player instance. * * @param {Object} [options] * An optional options object. See the {@link DefaultOptions} */ constructor(player, options) { // the parent class will add player under this.player super(player, options); this.defaults = defaults; this.options = videojs__default["default"].mergeOptions(defaults, options); this.log = videojs__default["default"].log.createLogger('MaxQualitySelector'); this.autoMode = true; this.qualityLevels = []; this.player.on('loadstart', this.handleMediaChange.bind(this)); if (this.player.qualityLevels !== undefined) { this.qlInternal = this.player.qualityLevels(); this.qlInternal.on('addqualitylevel', this.handleQualityLevel.bind(this)); this.qlInternal.on('change', this.handleChange.bind(this)); const buttonIndex = this.options.index < 0 ? player.controlBar.children().length + this.options.index : this.options.index; this.button = player.controlBar.addChild('MaxQualityButton', { parent: this }, buttonIndex); } this.player.ready(() => { this.player.addClass('vjs-max-quality-selector'); }); } /** * Run this to update the visual display of the plugin button with the current state. */ update() { const self = this; const enabledLevels = []; this.qualityLevels.forEach(function (obj, idx) { obj.isCurrent = false; if (self.qlInternal.levels_[obj.id].enabled) { enabledLevels.push(obj.id); } }); this.autoMode = enabledLevels.length === this.qualityLevels.length; const selQuality = this.qualityLevels.find(function (level) { return level.id === self.selectedIndex; }); if (selQuality === undefined) { this.button.hide(); return; } if (this.autoMode && this.options.disableAuto) { this.autoMode = false; this.changeLevel(selQuality.id); } selQuality.isCurrent = true; if (this.options.filterDuplicates) { this.qualityLevels.forEach(function (obj, idx) { if (obj.uniqueId === selQuality.uniqueId) { obj.isCurrent = true; } }); } if (this.options.filterDuplicateHeights) { this.qualityLevels.forEach(function (obj, idx) { if (obj.height === selQuality.height) { obj.isCurrent = true; } }); } this.button.$('.vjs-icon-placeholder').innerHTML = this.getQualityDisplayString(selQuality); this.button.show(); let qualityItems = this.qualityLevels; if (this.options.sortEnabled) { if (this.options.sort === 0) { qualityItems = this.qualityLevels.sort(function (a, b) { return b.uniqueId - a.uniqueId; }); } else { qualityItems = this.qualityLevels.sort(function (a, b) { return a.uniqueId - b.uniqueId; }); } } else { qualityItems = this.qualityLevels.sort(function (a, b) { return a.id - b.id; }); } this.button.items = qualityItems; this.button.update(); } /** * Change the current quality level to a new one. * * @param {number} levelIndex The numeric index of the quality level to be chosen. */ changeLevel(levelIndex) { const self = this; if (levelIndex < 0) { // Selecting AUTO this.qlInternal.levels_.forEach(function (obj, idx) { if (self.options.minHeight !== 0 && obj.height < self.options.minHeight || self.options.maxHeight !== 0 && obj.height > self.options.maxHeight) { obj.enabled = false; } else { obj.enabled = true; } }); this.update(); return; } const selectedQuality = this.qualityLevels.find(x => x.id === levelIndex); this.qlInternal.levels_.forEach(function (obj, idx) { const qual = self.qualityLevels.find(x => x.id === idx); if (qual !== undefined) { obj.enabled = idx === levelIndex || self.options.filterDuplicates && qual.uniqueId === selectedQuality.uniqueId || self.options.filterDuplicateHeights && qual.height === selectedQuality.height; } }); if (this.autoMode) { this.update(); } } /** * Called by VideoJS when the player's source has changed. * * @param {Event} e The event object returned by VideoJS. */ handleMediaChange(e) { this.log.debug('Handling media change:', this.player.src(), this.player.currentType()); this.qualityLevels = []; this.update(); if (this.options.defaultQuality !== 0) { this.firstRun = true; } } /** * Called by VideoJS-Contrib-Quality when the player's quality level has changed. * * @param {Event} e The event object returned by VideoJS-Contrib-Quality. */ handleChange(e) { this.log.debug(`Handling quality change: ${e.selectedIndex}`); if (this.firstRun && this.options.defaultQuality !== 0) { this.firstRun = false; const levelPref = this.options.defaultQuality; if (levelPref === 1) { const quality = this.qualityLevels.reduce(function (res, obj) { return obj.uniqueId < res.uniqueId ? obj : res; }); this.selectedIndex = quality.id; this.changeLevel(quality.id); this.update(); } else { const quality = this.qualityLevels.reduce(function (res, obj) { return obj.uniqueId > res.uniqueId ? obj : res; }); this.selectedIndex = quality.id; this.changeLevel(quality.id); this.update(); } } else { this.selectedIndex = e.selectedIndex; this.update(); } } /** * Called by VideoJS-Contrib-Quality when a new quality level has been added. * * @param {Event} e The event object returned by VideoJS-Contrib-Quality. */ handleQualityLevel(e) { const ql = e.qualityLevel; if (ql.width === undefined || ql.height === undefined || ql.bitrate === undefined) { return; } if (this.options.minHeight !== 0 && ql.height < this.options.minHeight || this.options.maxHeight !== 0 && ql.height > this.options.maxHeight) { ql.enabled = false; return; } const uniqueId = ql.width + ql.height + ql.bitrate; const quality = { id: this.qlInternal.levels_.indexOf(ql), uniqueId, width: ql.width, height: ql.height, dimension: ql.width + 'x' + ql.height, dimensionEnglishName: this.getDimensionEnglishName(ql.width, ql.height), dimensionMarketingName: this.getDimensionMarketingName(ql.width, ql.height), bitrate: ql.bitrate, bitrateName: this.getReadableBitrateString(ql.bitrate), isCurrent: false }; this.qualityLevels.push(quality); } /** * Get a list of the current quality levels in the plugin by true index. * * Tip: Use this to help pin-point new {@Link DefaultOptions.labels} to apply to your button and menu. * * @return {Array} The array of level names as displayed by the plugin */ getLevelNames() { const levelNames = []; this.qualityLevels.forEach(level => { levelNames.push(this.getQualityDisplayString(level)); }); return levelNames; } /** * Get a rendered name to a quality level, including overrides from the {@Link DefaultOptions.labels}. * * @param {number} id The true index of the quality level we want the name for * @param {string} originalName The fallback string to return if there is no customized level name label * * @return {string} Return the name if overwritten or the originalName. */ getLevelName(id, originalName) { const labels = this.options.labels; if (labels[id] !== undefined) { return labels[id].toString(); } return originalName; } /** * Get the dimension english name * * @param {number} [width] The quality width, not used * @param {number} height The quality height * * @return {string} Returns the dimension's english name. */ getDimensionEnglishName(width, height) { switch (height) { case 108: case 180: case 144: case 234: case 240: case 252: return 'VLQ'; case 360: return 'LQ'; case 480: case 486: case 540: return 'SD'; case 720: return 'HD'; case 1080: return 'FHD'; case 1440: return 'QHD'; case 2160: case 2304: return 'UHD'; } return 'N/A'; } /** * Get the dimension marketing name * * @param {number} [width] The quality width, not used * @param {number} height The quality height * * @return {string} Returns the dimension's marketing name. */ getDimensionMarketingName(width, height) { switch (height) { case 2160: return '4k'; case 2304: return 'True 4k'; } return height + 'p'; } /** * Get the stringified view of the bitrate * * @param {number} bitrate The quality level bitrate to stringify * * @return {string} Returns a humanized version of a bitrate number */ getReadableBitrateString(bitrate) { const byteUnits = [' Kbps', ' Mbps', ' Gbps']; let i = -1; do { bitrate = bitrate / 1024; i++; } while (bitrate > 1024); const output = Math.max(bitrate, 0.1).toFixed(1); return output + byteUnits[i]; } /** * Get the rendered string name for the quality level. * * @param {QualityLevel} qualityLevel The quality level to render to string * * @return {string} Returns the final display of the quality level */ getQualityDisplayString(qualityLevel) { if (!qualityLevel) { return ''; } let displayString = ''; if (this.options.displayMode === 1) { displayString = qualityLevel.dimensionMarketingName; } else if (this.options.displayMode === 2) { displayString = qualityLevel.dimensionEnglishName; } else { displayString = qualityLevel.dimensionMarketingName + '<sup>' + qualityLevel.dimensionEnglishName + '</sup>'; } if (this.autoMode && qualityLevel.isCurrent) { displayString = `${this.options.autoLabel}(${displayString})`; } if (this.options.showBitrates) { displayString += ' (' + qualityLevel.bitrateName + ')'; } return this.getLevelName(qualityLevel.id, displayString); } } // Define default values for the plugin's `state` object here. MaxQualitySelector.defaultState = {}; // Include the version number. MaxQualitySelector.VERSION = version; // Register the plugin with video.js. videojs__default["default"].registerPlugin('maxQualitySelector', MaxQualitySelector); module.exports = MaxQualitySelector;