videojs-hls-quality-selector
Version:
Adds a quality selector menu for HLS sources played in videojs. Requires `videojs-contrib-hls` and videojs-contrib-quality-levels plugins.
540 lines (404 loc) • 13.2 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var videojs = _interopDefault(require('video.js'));
var version = "1.0.5";
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var asyncGenerator = function () {
function AwaitValue(value) {
this.value = value;
}
function AsyncGenerator(gen) {
var front, back;
function send(key, arg) {
return new Promise(function (resolve, reject) {
var request = {
key: key,
arg: arg,
resolve: resolve,
reject: reject,
next: null
};
if (back) {
back = back.next = request;
} else {
front = back = request;
resume(key, arg);
}
});
}
function resume(key, arg) {
try {
var result = gen[key](arg);
var value = result.value;
if (value instanceof AwaitValue) {
Promise.resolve(value.value).then(function (arg) {
resume("next", arg);
}, function (arg) {
resume("throw", arg);
});
} else {
settle(result.done ? "return" : "normal", result.value);
}
} catch (err) {
settle("throw", err);
}
}
function settle(type, value) {
switch (type) {
case "return":
front.resolve({
value: value,
done: true
});
break;
case "throw":
front.reject(value);
break;
default:
front.resolve({
value: value,
done: false
});
break;
}
front = front.next;
if (front) {
resume(front.key, front.arg);
} else {
back = null;
}
}
this._invoke = send;
if (typeof gen.return !== "function") {
this.return = undefined;
}
}
if (typeof Symbol === "function" && Symbol.asyncIterator) {
AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
return this;
};
}
AsyncGenerator.prototype.next = function (arg) {
return this._invoke("next", arg);
};
AsyncGenerator.prototype.throw = function (arg) {
return this._invoke("throw", arg);
};
AsyncGenerator.prototype.return = function (arg) {
return this._invoke("return", arg);
};
return {
wrap: function (fn) {
return function () {
return new AsyncGenerator(fn.apply(this, arguments));
};
},
await: function (value) {
return new AwaitValue(value);
}
};
}();
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
var possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
var VideoJsButtonClass = videojs.getComponent('MenuButton');
var VideoJsMenuClass = videojs.getComponent('Menu');
var VideoJsComponent = videojs.getComponent('Component');
var Dom = videojs.dom;
/**
* Convert string to title case.
*
* @param {string} string - the string to convert
* @return {string} the returned titlecase string
*/
function toTitleCase(string) {
if (typeof string !== 'string') {
return string;
}
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* Extend vjs button class for quality button.
*/
var ConcreteButton = function (_VideoJsButtonClass) {
inherits(ConcreteButton, _VideoJsButtonClass);
/**
* Button constructor.
*
* @param {Player} player - videojs player instance
*/
function ConcreteButton(player) {
classCallCheck(this, ConcreteButton);
return possibleConstructorReturn(this, _VideoJsButtonClass.call(this, player, { title: player.localize('Quality') }));
}
/**
* Creates button items.
*
* @return {Array} - Button items
*/
ConcreteButton.prototype.createItems = function createItems() {
return [];
};
/**
* Create the menu and add all items to it.
*
* @return {Menu}
* The constructed menu
*/
ConcreteButton.prototype.createMenu = function createMenu() {
var menu = new VideoJsMenuClass(this.player_, { menuButton: this });
this.hideThreshold_ = 0;
// Add a title list item to the top
if (this.options_.title) {
var titleEl = Dom.createEl('li', {
className: 'vjs-menu-title',
innerHTML: toTitleCase(this.options_.title),
tabIndex: -1
});
var titleComponent = new VideoJsComponent(this.player_, { el: titleEl });
this.hideThreshold_ += 1;
menu.addItem(titleComponent);
}
this.items = this.createItems();
if (this.items) {
// Add menu items to the menu
for (var i = 0; i < this.items.length; i++) {
menu.addItem(this.items[i]);
}
}
return menu;
};
return ConcreteButton;
}(VideoJsButtonClass);
// Concrete classes
var VideoJsMenuItemClass = videojs.getComponent('MenuItem');
/**
* Extend vjs menu item class.
*/
var ConcreteMenuItem = function (_VideoJsMenuItemClass) {
inherits(ConcreteMenuItem, _VideoJsMenuItemClass);
/**
* Menu item constructor.
*
* @param {Player} player - vjs player
* @param {Object} item - Item object
* @param {ConcreteButton} qualityButton - The containing button.
* @param {HlsQualitySelectorPlugin} plugin - This plugin instance.
*/
function ConcreteMenuItem(player, item, qualityButton, plugin) {
classCallCheck(this, ConcreteMenuItem);
var _this = possibleConstructorReturn(this, _VideoJsMenuItemClass.call(this, player, {
label: item.label,
selectable: true,
selected: item.selected || false
}));
_this.item = item;
_this.qualityButton = qualityButton;
_this.plugin = plugin;
return _this;
}
/**
* Click event for menu item.
*/
ConcreteMenuItem.prototype.handleClick = function handleClick() {
// Reset other menu items selected status.
for (var i = 0; i < this.qualityButton.items.length; ++i) {
this.qualityButton.items[i].selected(false);
}
// Set this menu item to selected, and set quality.
this.plugin.setQuality(this.item.value);
this.selected(true);
};
return ConcreteMenuItem;
}(VideoJsMenuItemClass);
// Default options for the plugin.
var defaults = {};
// Cross-compatibility for Video.js 5 and 6.
var registerPlugin = videojs.registerPlugin || videojs.plugin;
// const dom = videojs.dom || videojs;
/**
* VideoJS HLS Quality Selector Plugin class.
*/
var HlsQualitySelectorPlugin = function () {
/**
* Plugin Constructor.
*
* @param {Player} player - The videojs player instance.
* @param {Object} options - The plugin options.
*/
function HlsQualitySelectorPlugin(player, options) {
classCallCheck(this, HlsQualitySelectorPlugin);
this.player = player;
// If there is quality levels plugin and the HLS tech exists
// then continue.
if (this.player.qualityLevels && this.getHls()) {
// Create the quality button.
this.createQualityButton();
this.bindPlayerEvents();
}
}
/**
* Returns HLS Plugin
*
* @return {*} - videojs-hls-contrib plugin.
*/
HlsQualitySelectorPlugin.prototype.getHls = function getHls() {
return this.player.tech({ IWillNotUseThisInPlugins: true }).hls;
};
/**
* Binds listener for quality level changes.
*/
HlsQualitySelectorPlugin.prototype.bindPlayerEvents = function bindPlayerEvents() {
this.player.qualityLevels().on('addqualitylevel', this.onAddQualityLevel.bind(this));
};
/**
* Adds the quality menu button to the player control bar.
*/
HlsQualitySelectorPlugin.prototype.createQualityButton = function createQualityButton() {
var player = this.player;
this._qualityButton = new ConcreteButton(player);
var placementIndex = player.controlBar.children().length - 2;
var concreteButtonInstance = player.controlBar.addChild(this._qualityButton, { componentClass: 'qualitySelector' }, placementIndex);
concreteButtonInstance.addClass('vjs-quality-selector');
concreteButtonInstance.menuButton_.$('.vjs-icon-placeholder').className += ' vjs-icon-hd';
concreteButtonInstance.removeClass('vjs-hidden');
};
/**
* Builds individual quality menu items.
*
* @param {Object} item - Individual quality menu item.
* @return {ConcreteMenuItem} - Menu item
*/
HlsQualitySelectorPlugin.prototype.getQualityMenuItem = function getQualityMenuItem(item) {
var player = this.player;
return new ConcreteMenuItem(player, item, this._qualityButton, this);
};
/**
* Executed when a quality level is added from HLS playlist.
*/
HlsQualitySelectorPlugin.prototype.onAddQualityLevel = function onAddQualityLevel() {
var _this = this;
var player = this.player;
var qualityList = player.qualityLevels();
var levels = qualityList.levels_ || [];
var levelItems = [];
var _loop = function _loop(i) {
if (!levelItems.filter(function (_existingItem) {
return _existingItem.item && _existingItem.item.value === levels[i].height;
}).length) {
var levelItem = _this.getQualityMenuItem.call(_this, {
label: levels[i].height + 'p',
value: levels[i].height
});
levelItems.push(levelItem);
}
};
for (var i = 0; i < levels.length; ++i) {
_loop(i);
}
levelItems.sort(function (current, next) {
if ((typeof current === 'undefined' ? 'undefined' : _typeof(current)) !== 'object' || (typeof next === 'undefined' ? 'undefined' : _typeof(next)) !== 'object') {
return -1;
}
if (current.item.value < next.item.value) {
return -1;
}
if (current.item.value > next.item.value) {
return 1;
}
return 0;
});
levelItems.push(this.getQualityMenuItem.call(this, {
label: player.localize('Auto'),
value: 'auto',
selected: true
}));
if (this._qualityButton) {
this._qualityButton.createItems = function () {
return levelItems;
};
this._qualityButton.update();
}
};
/**
* Sets quality (based on media height)
*
* @param {number} height - A number representing HLS playlist.
*/
HlsQualitySelectorPlugin.prototype.setQuality = function setQuality(height) {
var qualityList = this.player.qualityLevels();
for (var i = 0; i < qualityList.length; ++i) {
var quality = qualityList[i];
quality.enabled = quality.height === height || height === 'auto';
}
this._qualityButton.unpressButton();
};
return HlsQualitySelectorPlugin;
}();
/**
* 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.
*/
var onPlayerReady = function 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.
*/
var hlsQualitySelector = function hlsQualitySelector(options) {
var _this2 = this;
this.ready(function () {
onPlayerReady(_this2, videojs.mergeOptions(defaults, options));
});
};
// Register the plugin with video.js.
registerPlugin('hlsQualitySelector', hlsQualitySelector);
// Include the version number.
hlsQualitySelector.VERSION = version;
module.exports = hlsQualitySelector;