UNPKG

videojs-max-quality-selector

Version:

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

787 lines (675 loc) 23.6 kB
/*! @name videojs-max-quality-selector @version 0.9.1 @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) : typeof define === 'function' && define.amd ? define(['video.js'], factory) : (global = global || self, global.videojsMaxQualitySelector = factory(global.videojs)); }(this, function (videojs) { 'use strict'; videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs; function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } var MenuButton = videojs.getComponent('MenuButton'); var MenuItem = videojs.getComponent('MenuItem'); var Menu = videojs.getComponent('Menu'); var Dom = videojs.dom; // Default options for the plugin. var defaults = { parent: null }; var MaxQualityButton = /*#__PURE__*/ function (_MenuButton) { _inheritsLoose(MaxQualityButton, _MenuButton); /** * QualityButton constructor * * @param {Player} player - videojs player instance * @param {Object} options - component options */ function MaxQualityButton(player, options) { var _this; _this = _MenuButton.call(this, player, options) || this; _this.options = videojs.mergeOptions(defaults, options); _this.parent = _this.options.parent; _this.items = []; _this.addClass('vjs-max-quality-selector-button'); return _this; } var _proto = MaxQualityButton.prototype; _proto.handleMenuItemClick = function handleMenuItemClick(e) { var selectedIndex = parseInt(e.currentTarget.dataset.id, 10); this.parent.changeLevel(selectedIndex); }; _proto.handleSubmenuKeyPress = function handleSubmenuKeyPress(e) { if (e.currentTarget.dataset.id === undefined) { return; } var selectedIndex = parseInt(e.currentTarget.dataset.id, 10); this.parent.changeLevel(selectedIndex); }; _proto.createButton = function createButton(menu, cssClass, text, id) { var buttonEl = Dom.createEl('li', { className: cssClass, innerHTML: text, tabIndex: -1 }, { 'data-id': id }); var menuItem = new MenuItem(this.player_, { el: buttonEl }); menuItem.on('click', this.handleMenuItemClick.bind(this)); menu.addItem(menuItem); }; _proto.createMenu = function createMenu() { var menu = new Menu(this.player_, { menuButton: this }); var uniqueEntries = []; var uniqueHeights = []; if (this.items) { if (!this.parent.autoMode && !this.parent.options.disableAuto) { this.createButton(menu, 'vjs-menu-item', this.parent.options.autoLabel, -1); } for (var i = 0; i < this.items.length; i++) { var 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); } var 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; }; return MaxQualityButton; }(MenuButton); videojs.registerComponent('MaxQualityButton', MaxQualityButton); var version = "0.9.1"; var Plugin = videojs.getPlugin('plugin'); /** * @constant * @kind class * @alias DefaultOptions */ var defaults$1 = { /** * 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} */ var MaxQualitySelector = /*#__PURE__*/ function (_Plugin) { _inheritsLoose(MaxQualitySelector, _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} */ function MaxQualitySelector(player, options) { var _this; // the parent class will add player under this.player _this = _Plugin.call(this, player) || this; _this.defaults = defaults$1; _this.options = videojs.mergeOptions(defaults$1, options); _this.log = videojs.log.createLogger('MaxQualitySelector'); _this.autoMode = true; _this.qualityLevels = []; _this.player.on('loadstart', _this.handleMediaChange.bind(_assertThisInitialized(_this))); if (_this.player.qualityLevels !== undefined) { _this.qlInternal = _this.player.qualityLevels(); _this.qlInternal.on('addqualitylevel', _this.handleQualityLevel.bind(_assertThisInitialized(_this))); _this.qlInternal.on('change', _this.handleChange.bind(_assertThisInitialized(_this))); var buttonIndex = _this.options.index < 0 ? player.controlBar.children().length + _this.options.index : _this.options.index; _this.button = player.controlBar.addChild('MaxQualityButton', { parent: _assertThisInitialized(_this) }, buttonIndex); } _this.player.ready(function () { _this.player.addClass('vjs-max-quality-selector'); }); return _this; } /** * Run this to update the visual display of the plugin button with the current state. */ var _proto = MaxQualitySelector.prototype; _proto.update = function update() { var self = this; var 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; var 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(); var 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. */ ; _proto.changeLevel = function changeLevel(levelIndex) { var self = this; if (levelIndex < 0) { 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 = true; } else { obj.enabled = true; } }); this.update(); return; } var selectedQuality = this.qualityLevels.find(function (x) { return x.id === levelIndex; }); this.qlInternal.levels_.forEach(function (obj, idx) { var qual = self.qualityLevels.find(function (x) { return 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. */ ; _proto.handleMediaChange = function 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. */ ; _proto.handleChange = function handleChange(e) { this.log.debug("Handling quality change: " + e.selectedIndex); if (this.firstRun && this.options.defaultQuality !== 0) { this.firstRun = false; var levelPref = this.options.defaultQuality; if (levelPref === 1) { var 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 { var _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. */ ; _proto.handleQualityLevel = function handleQualityLevel(e) { var 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; } var uniqueId = ql.width + ql.height + ql.bitrate; var quality = { id: this.qlInternal.levels_.indexOf(ql), uniqueId: 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 */ ; _proto.getLevelNames = function getLevelNames() { var _this2 = this; var levelNames = []; this.qualityLevels.forEach(function (level) { levelNames.push(_this2.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. */ ; _proto.getLevelName = function getLevelName(id, originalName) { var 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. */ ; _proto.getDimensionEnglishName = function 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. */ ; _proto.getDimensionMarketingName = function 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 */ ; _proto.getReadableBitrateString = function getReadableBitrateString(bitrate) { var byteUnits = [' Kbps', ' Mbps', ' Gbps']; var i = -1; do { bitrate = bitrate / 1024; i++; } while (bitrate > 1024); var 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 */ ; _proto.getQualityDisplayString = function getQualityDisplayString(qualityLevel) { if (!qualityLevel) { return ''; } var 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); }; return MaxQualitySelector; }(Plugin); // 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.registerPlugin('maxQualitySelector', MaxQualitySelector); return MaxQualitySelector; }));