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,143 +15,88 @@
} }
(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(){ }
var menuItems = []; } );
var labels = (this.sources && this.sources.label) || {}; ResolutionMenuButton.prototype.createItems = function(){
var onClickUnselectOthers = function(clickedItem) { var menuItems = [];
menuItems.map(function(item) { var labels = (this.sources && this.sources.label) || {};
item.selected(item === clickedItem);
item.removeClass('vjs-selected');
});
};
for (var key in labels) { // FIXME order is not guaranteed here.
if (labels.hasOwnProperty(key)) { for (var key in labels) {
menuItems.push(new ResolutionMenuItem( if (labels.hasOwnProperty(key)) {
this.player_, menuItems.push(new ResolutionMenuItem(
{ this.player_,
label: key, {
src: labels[key], label: key,
initialySelected: key === this.options_.initialySelectedLabel, src: labels[key],
customSourcePicker: this.options_.customSourcePicker selected: key === (this.currentSelection ? this.currentSelection.label : false)
}, })
onClickUnselectOthers, );
this.label)); }
// Store menu item for API calls }
menuItemsHolder[key] = menuItems[menuItems.length - 1]; return menuItems;
} };
} ResolutionMenuButton.prototype.update = function(){
return menuItems; 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,11 +105,10 @@
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
* @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}] * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}]
@ -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;
}; };
/** /**
@ -276,79 +264,84 @@
} else if (groupedSrc.res[selectedRes]) { } else if (groupedSrc.res[selectedRes]) {
selectedLabel = groupedSrc.res[selectedRes][0].label; selectedLabel = groupedSrc.res[selectedRes][0].label;
} }
return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]}; return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]};
} }
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
var _yts = {
highres: {res: 1080, label: '1080', yt: 'highres'},
hd1080: {res: 1080, label: '1080', yt: 'hd1080'},
hd720: {res: 720, label: '720', yt: 'hd720'},
large: {res: 480, label: '480', yt: 'large'},
medium: {res: 360, label: '360', yt: 'medium'},
small: {res: 240, label: '240', yt: 'small'},
tiny: {res: 144, label: '144', yt: 'tiny'},
auto: {res: 0, label: 'auto', yt: 'default'}
};
var _sources = []; function initResolutionForYt(player){
// Map youtube qualities names
var _yts = {
highres: {res: 1080, label: '1080', yt: 'highres'},
hd1080: {res: 1080, label: '1080', yt: 'hd1080'},
hd720: {res: 720, label: '720', yt: 'hd720'},
large: {res: 480, label: '480', yt: 'large'},
medium: {res: 360, label: '360', yt: 'medium'},
small: {res: 240, label: '240', yt: 'small'},
tiny: {res: 144, label: '144', yt: 'tiny'},
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;
qualities.map(function(q){ // Init resolution
_sources.push({ player.tech_.ytPlayer.setPlaybackQuality('auto');
src: player.src().src,
type: player.src().type,
label: _yts[q].label,
res: _yts[q].res,
_yt: _yts[q].yt
});
});
groupedSrc = bucketSources(_sources); // 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;
}
}
});
// Overwrite defualt sourcePicer function // We must wait for play event
var _customSourcePicker = function(_player, _sources, _label){ player.one('play', function(){
player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt); var qualities = player.tech_.ytPlayer.getAvailableQualityLevels();
return player; var _sources = [];
};
var choosen = {label: 'auto', res: 0, sources: groupedSrc.label.auto}; qualities.map(function(q){
var menuButton = new ResolutionMenuButton(player, { _sources.push({
sources: groupedSrc, src: player.src().src,
initialySelectedLabel: choosen.label, type: player.src().type,
initialySelectedRes: choosen.res, label: _yts[q].label,
customSourcePicker: _customSourcePicker res: _yts[q].res,
}, settings, label); _yt: _yts[q].yt
});
});
menuButton.el().classList.add('vjs-resolution-button'); player.groupedSrc = bucketSources(_sources);
player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton); var chosen = {label: 'auto', res: 0, sources: player.groupedSrc.label.auto};
}); player.trigger('updateSources');
} player.setSourcesSanitized(chosen.sources, chosen.label,_customSourcePicker);
});
player.ready(function(){ }
if(player.options_.sources.length > 1){
// tech: Html5 and Flash player.ready(function(){
// Create resolution switcher for videos form <source> tag inside <video> if( settings.ui ) {
player.updateSrc(player.options_.sources); var menuButton = new ResolutionMenuButton(player, {});
} player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_);
player.controlBar.resolutionSwitcher.dispose = function(){
if(player.techName_ === 'Youtube'){ this.parentNode.removeChild(this);
// tech: YouTube };
initResolutionForYt(player); }
} if(player.options_.sources.length > 1){
}); // tech: Html5 and Flash
// Create resolution switcher for videos form <source> tag inside <video>
player.updateSrc(player.options_.sources);
}
if(player.techName_ === 'Youtube'){
// tech: YouTube
initResolutionForYt(player);
}
});
}; };