Add custom quality selector plugin
Adds custom video.js hls quality selector plugin This allows the quality selector plugin to stay active and listen for source changes on the player to prevent the need to recreate the player when switching between MP4's and M3U8's
This commit is contained in:
parent
21523fe707
commit
0fff2542b7
6 changed files with 522 additions and 7 deletions
|
@ -0,0 +1,85 @@
|
|||
import videojs from 'video.js';
|
||||
|
||||
const VideoJsButtonClass = videojs.getComponent('MenuButton');
|
||||
const VideoJsMenuClass = videojs.getComponent('Menu');
|
||||
const VideoJsComponent = videojs.getComponent('Component');
|
||||
const 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.
|
||||
*/
|
||||
export default class ConcreteButton extends VideoJsButtonClass {
|
||||
|
||||
/**
|
||||
* Button constructor.
|
||||
*
|
||||
* @param {Player} player - videojs player instance
|
||||
*/
|
||||
constructor(player) {
|
||||
super(player, {
|
||||
title: player.localize('Quality'),
|
||||
name: 'QualityButton'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates button items.
|
||||
*
|
||||
* @return {Array} - Button items
|
||||
*/
|
||||
createItems() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the menu and add all items to it.
|
||||
*
|
||||
* @return {Menu}
|
||||
* The constructed menu
|
||||
*/
|
||||
createMenu() {
|
||||
const menu = new VideoJsMenuClass(this.player_, {menuButton: this});
|
||||
|
||||
this.hideThreshold_ = 0;
|
||||
|
||||
// Add a title list item to the top
|
||||
if (this.options_.title) {
|
||||
const titleEl = Dom.createEl('li', {
|
||||
className: 'vjs-menu-title',
|
||||
innerHTML: toTitleCase(this.options_.title),
|
||||
tabIndex: -1
|
||||
});
|
||||
const 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 (let i = 0; i < this.items.length; i++) {
|
||||
menu.addItem(this.items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import videojs from 'video.js';
|
||||
|
||||
// Concrete classes
|
||||
const VideoJsMenuItemClass = videojs.getComponent('MenuItem');
|
||||
|
||||
/**
|
||||
* Extend vjs menu item class.
|
||||
*/
|
||||
export default class ConcreteMenuItem extends 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.
|
||||
*/
|
||||
constructor(player, item, qualityButton, plugin) {
|
||||
super(player, {
|
||||
label: item.label,
|
||||
selectable: true,
|
||||
selected: item.selected || false
|
||||
});
|
||||
this.item = item;
|
||||
this.qualityButton = qualityButton;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click event for menu item.
|
||||
*/
|
||||
handleClick() {
|
||||
|
||||
// Reset other menu items selected status.
|
||||
for (let 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);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"name": "videojs-hls-quality-selector",
|
||||
"version": "1.1.4",
|
||||
"description": "Adds a quality selector menu for HLS sources played in videojs. Requires `videojs-contrib-hls` and videojs-contrib-quality-levels plugins.",
|
||||
"main": "dist/videojs-hls-quality-selector.cjs.js",
|
||||
"module": "dist/videojs-hls-quality-selector.es.js",
|
||||
"generator-videojs-plugin": {
|
||||
"version": "5.0.3"
|
||||
},
|
||||
"repository": "https://github.com/chrisboustead/videojs-hls-quality-selector.git",
|
||||
"scripts": {
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm-run-all -p build:*",
|
||||
"build:css": "npm-run-all build:css:sass build:css:bannerize",
|
||||
"build:css:bannerize": "bannerize dist/videojs-hls-quality-selector.css --banner=scripts/banner.ejs",
|
||||
"build:css:sass": "node-sass src/plugin.scss dist/videojs-hls-quality-selector.css --output-style=compressed --linefeed=lf",
|
||||
"build:js": "npm-run-all build:js:rollup-modules build:js:rollup-umd build:js:bannerize build:js:uglify",
|
||||
"build:js:bannerize": "bannerize dist/videojs-hls-quality-selector.js --banner=scripts/banner.ejs",
|
||||
"build:js:rollup-modules": "rollup -c scripts/modules.rollup.config.js",
|
||||
"build:js:rollup-umd": "rollup -c scripts/umd.rollup.config.js",
|
||||
"build:js:uglify": "uglifyjs dist/videojs-hls-quality-selector.js --comments --mangle --compress -o dist/videojs-hls-quality-selector.min.js",
|
||||
"build:test": "rollup -c scripts/test.rollup.config.js",
|
||||
"clean": "rimraf dist test/dist",
|
||||
"postclean": "mkdirp dist test/dist",
|
||||
"docs": "npm-run-all docs:*",
|
||||
"docs:api": "jsdoc src -r -c jsdoc.json -d docs/api",
|
||||
"docs:toc": "doctoc README.md",
|
||||
"lint": "vjsstandard",
|
||||
"start": "npm-run-all -p start:server watch",
|
||||
"start:server": "static -a 0.0.0.0 -p 9999 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}' .",
|
||||
"pretest": "npm-run-all lint build",
|
||||
"test": "karma start test/karma.conf.js",
|
||||
"preversion": "npm test",
|
||||
"version": "node scripts/version.js",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:css": "npm-run-all build:css:sass watch:css:sass",
|
||||
"watch:css:sass": "node-sass src/plugin.scss dist/videojs-hls-quality-selector.css --output-style=compressed --linefeed=lf --watch src/**/*.scss",
|
||||
"watch:js-modules": "rollup -c scripts/modules.rollup.config.js -w",
|
||||
"watch:js-umd": "rollup -c scripts/umd.rollup.config.js -w",
|
||||
"watch:test": "rollup -c scripts/test.rollup.config.js -w",
|
||||
"prepublish": "npm run build",
|
||||
"prepush": "npm run lint",
|
||||
"precommit": "npm run docs:toc && git add README.md"
|
||||
},
|
||||
"keywords": [
|
||||
"videojs",
|
||||
"videojs-plugin"
|
||||
],
|
||||
"author": "Chris Boustead (chris@forgemotion.com)",
|
||||
"license": "MIT",
|
||||
"vjsstandard": {
|
||||
"ignore": [
|
||||
"dist",
|
||||
"docs",
|
||||
"test/dist",
|
||||
"test/karma.conf.js"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"CONTRIBUTING.md",
|
||||
"dist/",
|
||||
"docs/",
|
||||
"index.html",
|
||||
"scripts/",
|
||||
"src/",
|
||||
"test/"
|
||||
],
|
||||
"dependencies": {
|
||||
"global": "^4.3.2",
|
||||
"karma-safaritechpreview-launcher": "0.0.6",
|
||||
"video.js": "^7.5.5",
|
||||
"videojs-contrib-quality-levels": "^2.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-object-assign": "^6.8.0",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"bannerize": "^1.1.4",
|
||||
"conventional-changelog-cli": "^1.3.1",
|
||||
"conventional-changelog-videojs": "^3.0.0",
|
||||
"doctoc": "^1.3.0",
|
||||
"husky": "^0.13.3",
|
||||
"jsdoc": "^3.6.3",
|
||||
"karma": "^3.1.4",
|
||||
"karma-chrome-launcher": "^2.1.1",
|
||||
"karma-detect-browsers": "^2.2.5",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-qunit": "^1.2.1",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"node-static": "^0.7.9",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"qunitjs": "^2.3.2",
|
||||
"rimraf": "^2.6.1",
|
||||
"rollup": "^0.50.0",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-json": "^2.1.1",
|
||||
"rollup-plugin-multi-entry": "^2.0.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"semver": "^5.3.0",
|
||||
"sinon": "^2.2.0",
|
||||
"uglify-js": "^3.0.7",
|
||||
"videojs-standard": "^6.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
import videojs from 'video.js';
|
||||
import {version as VERSION} from './package.json';
|
||||
import ConcreteButton from './ConcreteButton';
|
||||
import ConcreteMenuItem from './ConcreteMenuItem';
|
||||
|
||||
// Default options for the plugin.
|
||||
const defaults = {};
|
||||
|
||||
// Cross-compatibility for Video.js 5 and 6.
|
||||
const registerPlugin = videojs.registerPlugin || videojs.plugin;
|
||||
// const dom = videojs.dom || videojs;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (!this.player.qualityLevels) {
|
||||
console.warn(`WARNING: Missing video.js quality levels plugin (required)`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupPlugin();
|
||||
}
|
||||
|
||||
setupPlugin() {
|
||||
// Create the quality button.
|
||||
this.createQualityButton();
|
||||
|
||||
// Bind event listeners
|
||||
this.bindPlayerEvents();
|
||||
|
||||
// Listen for source changes
|
||||
this.player.on('loadedmetadata', (e) => {
|
||||
console.log(`Loaded Metadata detected by plugin!`, e);
|
||||
this.updatePlugin();
|
||||
});
|
||||
}
|
||||
|
||||
updatePlugin() {
|
||||
console.log(`Updating Quality Selector...`);
|
||||
|
||||
// If there is quality levels plugin and the HLS tech exists
|
||||
// then continue.
|
||||
if (this.getHls()) {
|
||||
console.log('Show quality selector');
|
||||
// Show quality selector
|
||||
this._qualityButton.show();
|
||||
} else {
|
||||
console.log('Hide quality selector');
|
||||
console.log('Source type does not support multiple qulaity levels...');
|
||||
// Hide quality selector
|
||||
this._qualityButton.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HLS Plugin
|
||||
*
|
||||
* @return {*} - videojs-hls-contrib plugin.
|
||||
*/
|
||||
getHls() {
|
||||
return this.player.tech({ IWillNotUseThisInPlugins: true }).hls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const concreteButtonInstance = player.controlBar.addChild(this._qualityButton,
|
||||
{componentClass: 'qualitySelector'},
|
||||
this.config.placementIndex || placementIndex);
|
||||
|
||||
concreteButtonInstance.addClass('vjs-quality-selector');
|
||||
if (!this.config.displayCurrentQuality) {
|
||||
const icon = ` ${this.config.vjsIconClass || 'vjs-icon-hd'}`;
|
||||
|
||||
concreteButtonInstance
|
||||
.menuButton_.$('.vjs-icon-placeholder').className += icon;
|
||||
} else {
|
||||
this.setButtonInnerText('auto');
|
||||
}
|
||||
concreteButtonInstance.removeClass('vjs-hidden');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*Set inner button text.
|
||||
*
|
||||
* @param {string} text - the text to display in the button.
|
||||
*/
|
||||
setButtonInnerText(text) {
|
||||
this._qualityButton
|
||||
.menuButton_.$('.vjs-icon-placeholder').innerHTML = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
onAddQualityLevel() {
|
||||
|
||||
const player = this.player;
|
||||
const qualityList = player.qualityLevels();
|
||||
const levels = qualityList.levels_ || [];
|
||||
const levelItems = [];
|
||||
|
||||
for (let i = 0; i < levels.length; ++i) {
|
||||
if (!levelItems.filter(_existingItem => {
|
||||
return _existingItem.item && _existingItem.item.value === levels[i].height;
|
||||
}).length) {
|
||||
const levelItem = this.getQualityMenuItem.call(this, {
|
||||
label: levels[i].height + 'p',
|
||||
value: levels[i].height
|
||||
});
|
||||
|
||||
levelItems.push(levelItem);
|
||||
}
|
||||
}
|
||||
|
||||
levelItems.sort((current, next) => {
|
||||
if ((typeof current !== 'object') || (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.
|
||||
*/
|
||||
setQuality(height) {
|
||||
const qualityList = this.player.qualityLevels();
|
||||
|
||||
// Set quality on plugin
|
||||
this._currentQuality = height;
|
||||
|
||||
if (this.config.displayCurrentQuality) {
|
||||
this.setButtonInnerText(height === 'auto' ? height : `${height}p`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < qualityList.length; ++i) {
|
||||
const quality = qualityList[i];
|
||||
|
||||
quality.enabled = (quality.height === height || height === 'auto');
|
||||
}
|
||||
this._qualityButton.unpressButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current set quality or 'auto'
|
||||
*
|
||||
* @return {string} the currently set quality
|
||||
*/
|
||||
getCurrentQuality() {
|
||||
return this._currentQuality || 'auto';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const hlsQualitySelector = function(options) {
|
||||
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;
|
|
@ -0,0 +1,9 @@
|
|||
// Sass for videojs-hls-quality-selector
|
||||
|
||||
.video-js {
|
||||
|
||||
// This class is added to the video.js element by the plugin by default.
|
||||
&.vjs-hls-quality-selector {
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,8 @@ import 'video.js/dist/alt/video-js-cdn.min.css';
|
|||
import eventTracking from 'videojs-event-tracking';
|
||||
import * as OVERLAY from './overlays';
|
||||
import './plugins/videojs-mobile-ui/plugin';
|
||||
import qualitySelector from 'videojs-hls-quality-selector';
|
||||
// import qualitySelector from 'videojs-hls-quality-selector';
|
||||
import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin';
|
||||
import qualityLevels from 'videojs-contrib-quality-levels';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
|
||||
|
@ -102,7 +103,7 @@ if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) {
|
|||
}
|
||||
|
||||
if (!Object.keys(videojs.getPlugins()).includes('hlsQualitySelector')) {
|
||||
videojs.registerPlugin('hlsQualitySelector', qualitySelector);
|
||||
videojs.registerPlugin('hlsQualitySelector', hlsQualitySelector);
|
||||
}
|
||||
|
||||
if (!Object.keys(videojs.getPlugins()).includes('qualityLevels')) {
|
||||
|
@ -379,11 +380,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
// initialize mobile UI
|
||||
player.mobileUi(); // Inits mobile version. No-op if Desktop.
|
||||
|
||||
// Add quality selector to player
|
||||
player.hlsQualitySelector({
|
||||
displayCurrentQuality: true,
|
||||
});
|
||||
|
||||
// I think this is a callback function
|
||||
onPlayerReady(player);
|
||||
});
|
||||
|
@ -447,6 +443,17 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
src: source,
|
||||
type: type,
|
||||
});
|
||||
|
||||
// Add quality selector to player
|
||||
player.hlsQualitySelector({
|
||||
displayCurrentQuality: true,
|
||||
});
|
||||
|
||||
// Update player source
|
||||
player.src({
|
||||
src: source,
|
||||
type: type,
|
||||
});
|
||||
});
|
||||
}, [source, reload]);
|
||||
|
||||
|
|
Loading…
Reference in a new issue