A big rewrite of the plugin

* Stricter separation between functionality and UI
* Option to disable the UI
* Scoping per instance. #29
* Cleaned up inheritence etc of the UI components
This commit is contained in:
Derk-Jan Hartman 2016-05-02 00:35:44 +02:00
parent 56760f6314
commit 033dfe291c

View file

@ -1,6 +1,6 @@
/*! videojs-resolution-switcher - 2015-7-26 /*! videojs-resolution-switcher - 2015-7-26
* Copyright (c) 2016 Kasper Moskwiak * Copyright (c) 2016 Kasper Moskwiak
* Modified by Pierre Kraft * Modified by Pierre Kraft and Derk-Jan Hartman
* Licensed under the Apache-2.0 license. */ * Licensed under the Apache-2.0 license. */
(function() { (function() {
@ -15,124 +15,64 @@
} }
(function(window, videojs) { (function(window, videojs) {
var videoJsResolutionSwitcher,
defaults = {
var defaults = {}, ui: true
videoJsResolutionSwitcher,
currentResolution = {}, // stores current resolution
menuItemsHolder = {}; // stores menuItems
function setSourcesSanitized(player, sources, label, customSourcePicker) {
currentResolution = {
label: label,
sources: sources
}; };
if(typeof customSourcePicker === 'function'){
return customSourcePicker(player, sources, label);
}
return player.src(sources.map(function(src) {
return {src: src.src, type: src.type, res: src.res};
}));
}
/* /*
* Resolution menu item * Resolution menu item
*/ */
var MenuItem = videojs.getComponent('MenuItem'); var MenuItem = videojs.getComponent('MenuItem');
var ResolutionMenuItem = videojs.extend(MenuItem, { var ResolutionMenuItem = videojs.extend(MenuItem, {
constructor: function(player, options, onClickListener, label){ constructor: function(player, options){
this.onClickListener = onClickListener; options.selectable = true;
this.label = label;
// Sets this.player_, this.options_ and initializes the component // Sets this.player_, this.options_ and initializes the component
MenuItem.call(this, player, options); MenuItem.call(this, player, options);
this.src = options.src; this.src = options.src;
this.on('click', this.onClick); player.on('resolutionchange', videojs.bind(this, this.update));
this.on('touchstart', this.onClick);
if (options.initialySelected) {
this.showAsLabel();
this.selected(true);
this.addClass('vjs-selected');
} }
}, } );
showAsLabel: function() { ResolutionMenuItem.prototype.handleClick = function(event){
// Change menu button label to the label of this item if the menu button label is provided MenuItem.prototype.handleClick.call(this,event);
if(this.label) { this.player_.currentResolution(this.options_.label);
this.label.innerHTML = this.options_.label; };
} ResolutionMenuItem.prototype.update = function(){
}, var selection = this.player_.currentResolution();
onClick: function(customSourcePicker){ this.selected(this.options_.label === selection.label);
this.onClickListener(this); };
// Remember player state MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem);
var currentTime = this.player_.currentTime();
var isPaused = this.player_.paused();
this.showAsLabel();
// add .current class
this.addClass('vjs-selected');
// Hide bigPlayButton
if(!isPaused){
this.player_.bigPlayButton.hide();
}
if(typeof customSourcePicker !== 'function' &&
typeof this.options_.customSourcePicker === 'function'){
customSourcePicker = this.options_.customSourcePicker;
}
// Change player source and wait for loadeddata event, then play video
// loadedmetadata doesn't work right now for flash.
// Probably because of https://github.com/videojs/video-js-swf/issues/124
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
var handleSeekEvent = 'loadeddata';
if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') {
handleSeekEvent = 'timeupdate';
}
setSourcesSanitized(this.player_, this.src, this.options_.label, customSourcePicker).one(handleSeekEvent, function() {
this.player_.currentTime(currentTime);
this.player_.handleTechSeeked_();
if(!isPaused){
// Start playing and hide loadingSpinner (flash issue ?)
this.player_.play().handleTechSeeked_();
}
this.player_.trigger('resolutionchange');
});
}
});
/* /*
* Resolution menu button * Resolution menu button
*/ */
var MenuButton = videojs.getComponent('MenuButton'); var MenuButton = videojs.getComponent('MenuButton');
var ResolutionMenuButton = videojs.extend(MenuButton, { var ResolutionMenuButton = videojs.extend(MenuButton, {
constructor: function(player, options, settings, label){ constructor: function(player, options){
this.sources = options.sources; this.label = document.createElement('span');
this.label = label; options.label = 'Quality';
this.label.innerHTML = options.initialySelectedLabel;
// Sets this.player_, this.options_ and initializes the component // Sets this.player_, this.options_ and initializes the component
MenuButton.call(this, player, options, settings); MenuButton.call(this, player, options);
this.el().setAttribute('aria-label','Quality');
this.controlText('Quality'); this.controlText('Quality');
if(settings.dynamicLabel){ if(options.dynamicLabel){
this.el().appendChild(label); videojs.addClass(this.label, 'vjs-resolution-button-label');
this.el().appendChild(this.label);
}else{ }else{
var staticLabel = document.createElement('span'); var staticLabel = document.createElement('span');
videojs.addClass(staticLabel, 'vjs-resolution-button-staticlabel'); videojs.addClass(staticLabel, 'vjs-resolution-button-staticlabel');
this.el().appendChild(staticLabel); this.el().appendChild(staticLabel);
} }
}, player.on('updateSources', videojs.bind( this, this.update ) );
createItems: function(){ }
} );
ResolutionMenuButton.prototype.createItems = function(){
var menuItems = []; var menuItems = [];
var labels = (this.sources && this.sources.label) || {}; var labels = (this.sources && this.sources.label) || {};
var onClickUnselectOthers = function(clickedItem) {
menuItems.map(function(item) {
item.selected(item === clickedItem);
item.removeClass('vjs-selected');
});
};
// FIXME order is not guaranteed here.
for (var key in labels) { for (var key in labels) {
if (labels.hasOwnProperty(key)) { if (labels.hasOwnProperty(key)) {
menuItems.push(new ResolutionMenuItem( menuItems.push(new ResolutionMenuItem(
@ -140,18 +80,23 @@
{ {
label: key, label: key,
src: labels[key], src: labels[key],
initialySelected: key === this.options_.initialySelectedLabel, selected: key === (this.currentSelection ? this.currentSelection.label : false)
customSourcePicker: this.options_.customSourcePicker })
}, );
onClickUnselectOthers,
this.label));
// Store menu item for API calls
menuItemsHolder[key] = menuItems[menuItems.length - 1];
} }
} }
return menuItems; return menuItems;
} };
}); ResolutionMenuButton.prototype.update = function(){
this.sources = this.player_.getGroupedSrc();
this.currentSelection = this.player_.currentResolution();
this.label.innerHTML = this.currentSelection ? this.currentSelection.label : '';
return MenuButton.prototype.update.call(this);
};
ResolutionMenuButton.prototype.buildCSSClass = function(){
return MenuButton.prototype.buildCSSClass.call( this ) + ' vjs-resolution-button';
};
MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton);
/** /**
* Initialize the plugin. * Initialize the plugin.
@ -160,10 +105,9 @@
videoJsResolutionSwitcher = function(options) { videoJsResolutionSwitcher = function(options) {
var settings = videojs.mergeOptions(defaults, options), var settings = videojs.mergeOptions(defaults, options),
player = this, player = this,
label = document.createElement('span'), groupedSrc = {},
groupedSrc = {}; currentSources = {},
currentResolutionState = {};
videojs.addClass(label, 'vjs-resolution-button-label');
/** /**
* Updates player sources or returns current source URL * Updates player sources or returns current source URL
@ -173,35 +117,65 @@
player.updateSrc = function(src){ player.updateSrc = function(src){
//Return current src if src is not given //Return current src if src is not given
if(!src){ return player.src(); } if(!src){ return player.src(); }
// Dispose old resolution menu button before adding new sources
if(player.controlBar.resolutionSwitcher){ // Sort sources
player.controlBar.resolutionSwitcher.dispose(); this.currentSources = src.sort(compareResolutions);
delete player.controlBar.resolutionSwitcher; this.groupedSrc = bucketSources(this.currentSources);
} // Pick one by default
//Sort sources var chosen = chooseSrc(this.groupedSrc, this.currentSources);
src = src.sort(compareResolutions); this.currentResolutionState = {
groupedSrc = bucketSources(src); label: chosen.label,
var choosen = chooseSrc(groupedSrc, src); sources: chosen.sources
var menuButton = new ResolutionMenuButton(player, { sources: groupedSrc, initialySelectedLabel: choosen.label , initialySelectedRes: choosen.res , customSourcePicker: settings.customSourcePicker}, settings, label);
videojs.addClass(menuButton.el(), 'vjs-resolution-button');
player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_);
player.controlBar.resolutionSwitcher.dispose = function(){
this.parentNode.removeChild(this);
}; };
return setSourcesSanitized(player, choosen.sources, choosen.label);
player.trigger('updateSources');
player.setSourcesSanitized(chosen.sources, chosen.label);
player.trigger('resolutionchange');
return player;
}; };
/** /**
* Returns current resolution or sets one when label is specified * Returns current resolution or sets one when label is specified
* @param {String} [label] label name * @param {String} [label] label name
* @param {Function} [customSourcePicker] custom function to choose source. Takes 3 arguments: player, sources, label. Must return player object. * @param {Function} [customSourcePicker] custom function to choose source. Takes 2 arguments: sources, label. Must return player object.
* @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter * @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter
*/ */
player.currentResolution = function(label, customSourcePicker){ player.currentResolution = function(label, customSourcePicker){
if(label == null) { return currentResolution; } if(label == null) { return this.currentResolutionState; }
if(menuItemsHolder[label] != null){
menuItemsHolder[label].onClick(customSourcePicker); // Lookup sources for label
if(!this.groupedSrc || !this.groupedSrc.label || !this.groupedSrc.label[label]){
return;
} }
var sources = this.groupedSrc.label[label];
// Remember player state
var currentTime = player.currentTime();
var isPaused = player.paused();
// Hide bigPlayButton
if(!isPaused){
this.player_.bigPlayButton.hide();
}
// Change player source and wait for loadeddata event, then play video
// loadedmetadata doesn't work right now for flash.
// Probably because of https://github.com/videojs/video-js-swf/issues/124
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
var handleSeekEvent = 'loadeddata';
if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') {
handleSeekEvent = 'timeupdate';
}
player
.setSourcesSanitized(sources, label, customSourcePicker || settings.customSourcePicker)
.one(handleSeekEvent, function() {
player.currentTime(currentTime);
player.handleTechSeeked_();
if(!isPaused){
// Start playing and hide loadingSpinner (flash issue ?)
player.play().handleTechSeeked_();
}
player.trigger('resolutionchange');
});
return player; return player;
}; };
@ -210,7 +184,21 @@
* @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } } * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } }
*/ */
player.getGroupedSrc = function(){ player.getGroupedSrc = function(){
return groupedSrc; return this.groupedSrc;
};
player.setSourcesSanitized = function(sources, label, customSourcePicker) {
this.currentResolutionState = {
label: label,
sources: sources
};
if(typeof customSourcePicker === 'function'){
return customSourcePicker(player, sources, label);
}
player.src(sources.map(function(src) {
return {src: src.src, type: src.type, res: src.res};
}));
return player;
}; };
/** /**
@ -281,17 +269,6 @@
} }
function initResolutionForYt(player){ function initResolutionForYt(player){
// Init resolution
player.tech_.ytPlayer.setPlaybackQuality('default');
// Capture events
player.tech_.ytPlayer.addEventListener('onPlaybackQualityChange', function(){
player.trigger('resolutionchange');
});
// We must wait for play event
player.one('play', function(){
var qualities = player.tech_.ytPlayer.getAvailableQualityLevels();
// Map youtube qualities names // Map youtube qualities names
var _yts = { var _yts = {
highres: {res: 1080, label: '1080', yt: 'highres'}, highres: {res: 1080, label: '1080', yt: 'highres'},
@ -301,9 +278,32 @@
medium: {res: 360, label: '360', yt: 'medium'}, medium: {res: 360, label: '360', yt: 'medium'},
small: {res: 240, label: '240', yt: 'small'}, small: {res: 240, label: '240', yt: 'small'},
tiny: {res: 144, label: '144', yt: 'tiny'}, tiny: {res: 144, label: '144', yt: 'tiny'},
auto: {res: 0, label: 'auto', yt: 'default'} auto: {res: 0, label: 'auto', yt: 'auto'}
}; };
// Overwrite default sourcePicker function
var _customSourcePicker = function(_player, _sources, _label){
// Note that setPlayebackQuality is a suggestion. YT does not always obey it.
player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt);
return player;
};
settings.customSourcePicker = _customSourcePicker;
// Init resolution
player.tech_.ytPlayer.setPlaybackQuality('auto');
// This is triggered when the resolution actually changes
player.tech_.ytPlayer.addEventListener('onPlaybackQualityChange', function(event){
for(var res in _yts) {
if(res.yt === event.data) {
player.currentResolution(res.label, _customSourcePicker);
return;
}
}
});
// We must wait for play event
player.one('play', function(){
var qualities = player.tech_.ytPlayer.getAvailableQualityLevels();
var _sources = []; var _sources = [];
qualities.map(function(q){ qualities.map(function(q){
@ -316,28 +316,21 @@
}); });
}); });
groupedSrc = bucketSources(_sources); player.groupedSrc = bucketSources(_sources);
var chosen = {label: 'auto', res: 0, sources: player.groupedSrc.label.auto};
// Overwrite defualt sourcePicer function player.trigger('updateSources');
var _customSourcePicker = function(_player, _sources, _label){ player.setSourcesSanitized(chosen.sources, chosen.label,_customSourcePicker);
player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt);
return player;
};
var choosen = {label: 'auto', res: 0, sources: groupedSrc.label.auto};
var menuButton = new ResolutionMenuButton(player, {
sources: groupedSrc,
initialySelectedLabel: choosen.label,
initialySelectedRes: choosen.res,
customSourcePicker: _customSourcePicker
}, settings, label);
menuButton.el().classList.add('vjs-resolution-button');
player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton);
}); });
} }
player.ready(function(){ player.ready(function(){
if( settings.ui ) {
var menuButton = new ResolutionMenuButton(player, {});
player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_);
player.controlBar.resolutionSwitcher.dispose = function(){
this.parentNode.removeChild(this);
};
}
if(player.options_.sources.length > 1){ if(player.options_.sources.length > 1){
// tech: Html5 and Flash // tech: Html5 and Flash
// Create resolution switcher for videos form <source> tag inside <video> // Create resolution switcher for videos form <source> tag inside <video>