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
JavaScript
/*! @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;
}));