From 033dfe291cd58ddcedbe310f41ec6207a5b08f5d Mon Sep 17 00:00:00 2001 From: Derk-Jan Hartman Date: Mon, 2 May 2016 00:35:44 +0200 Subject: [PATCH] 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 --- lib/videojs-resolution-switcher.js | 431 ++++++++++++++--------------- 1 file changed, 212 insertions(+), 219 deletions(-) diff --git a/lib/videojs-resolution-switcher.js b/lib/videojs-resolution-switcher.js index 5b1448e..6d94f3e 100644 --- a/lib/videojs-resolution-switcher.js +++ b/lib/videojs-resolution-switcher.js @@ -1,6 +1,6 @@ /*! videojs-resolution-switcher - 2015-7-26 * Copyright (c) 2016 Kasper Moskwiak - * Modified by Pierre Kraft + * Modified by Pierre Kraft and Derk-Jan Hartman * Licensed under the Apache-2.0 license. */ (function() { @@ -15,143 +15,88 @@ } (function(window, videojs) { + var videoJsResolutionSwitcher, + defaults = { + ui: true + }; + /* + * Resolution menu item + */ + var MenuItem = videojs.getComponent('MenuItem'); + var ResolutionMenuItem = videojs.extend(MenuItem, { + constructor: function(player, options){ + options.selectable = true; + // Sets this.player_, this.options_ and initializes the component + MenuItem.call(this, player, options); + this.src = options.src; - var defaults = {}, - 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 - */ - var MenuItem = videojs.getComponent('MenuItem'); - var ResolutionMenuItem = videojs.extend(MenuItem, { - constructor: function(player, options, onClickListener, label){ - this.onClickListener = onClickListener; - this.label = label; - // Sets this.player_, this.options_ and initializes the component - MenuItem.call(this, player, options); - this.src = options.src; - - this.on('click', this.onClick); - this.on('touchstart', this.onClick); - - if (options.initialySelected) { - this.showAsLabel(); - this.selected(true); - - this.addClass('vjs-selected'); - } - }, - showAsLabel: function() { - // Change menu button label to the label of this item if the menu button label is provided - if(this.label) { - this.label.innerHTML = this.options_.label; + player.on('resolutionchange', videojs.bind(this, this.update)); } - }, - onClick: function(customSourcePicker){ - this.onClickListener(this); - // Remember player state - 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'); - }); - } - }); - + } ); + ResolutionMenuItem.prototype.handleClick = function(event){ + MenuItem.prototype.handleClick.call(this,event); + this.player_.currentResolution(this.options_.label); + }; + ResolutionMenuItem.prototype.update = function(){ + var selection = this.player_.currentResolution(); + this.selected(this.options_.label === selection.label); + }; + MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem); /* * Resolution menu button */ - var MenuButton = videojs.getComponent('MenuButton'); - var ResolutionMenuButton = videojs.extend(MenuButton, { - constructor: function(player, options, settings, label){ - this.sources = options.sources; - this.label = label; - this.label.innerHTML = options.initialySelectedLabel; + var MenuButton = videojs.getComponent('MenuButton'); + var ResolutionMenuButton = videojs.extend(MenuButton, { + constructor: function(player, options){ + this.label = document.createElement('span'); + options.label = 'Quality'; // 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'); - if(settings.dynamicLabel){ - this.el().appendChild(label); + if(options.dynamicLabel){ + videojs.addClass(this.label, 'vjs-resolution-button-label'); + this.el().appendChild(this.label); }else{ var staticLabel = document.createElement('span'); - videojs.addClass(staticLabel, 'vjs-resolution-button-staticlabel'); + videojs.addClass(staticLabel, 'vjs-resolution-button-staticlabel'); this.el().appendChild(staticLabel); } - }, - createItems: function(){ - var menuItems = []; - var labels = (this.sources && this.sources.label) || {}; - var onClickUnselectOthers = function(clickedItem) { - menuItems.map(function(item) { - item.selected(item === clickedItem); - item.removeClass('vjs-selected'); - }); - }; - - for (var key in labels) { - if (labels.hasOwnProperty(key)) { - menuItems.push(new ResolutionMenuItem( - this.player_, - { - label: key, - src: labels[key], - initialySelected: key === this.options_.initialySelectedLabel, - customSourcePicker: this.options_.customSourcePicker - }, - onClickUnselectOthers, - this.label)); - // Store menu item for API calls - menuItemsHolder[key] = menuItems[menuItems.length - 1]; - } - } - return menuItems; - } - }); + player.on('updateSources', videojs.bind( this, this.update ) ); + } + } ); + ResolutionMenuButton.prototype.createItems = function(){ + var menuItems = []; + var labels = (this.sources && this.sources.label) || {}; + + // FIXME order is not guaranteed here. + for (var key in labels) { + if (labels.hasOwnProperty(key)) { + menuItems.push(new ResolutionMenuItem( + this.player_, + { + label: key, + src: labels[key], + selected: key === (this.currentSelection ? this.currentSelection.label : false) + }) + ); + } + } + 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. @@ -160,11 +105,10 @@ videoJsResolutionSwitcher = function(options) { var settings = videojs.mergeOptions(defaults, options), 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 * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}] @@ -173,35 +117,65 @@ player.updateSrc = function(src){ //Return current src if src is not given if(!src){ return player.src(); } - // Dispose old resolution menu button before adding new sources - if(player.controlBar.resolutionSwitcher){ - player.controlBar.resolutionSwitcher.dispose(); - delete player.controlBar.resolutionSwitcher; - } - //Sort sources - src = src.sort(compareResolutions); - groupedSrc = bucketSources(src); - var choosen = chooseSrc(groupedSrc, src); - 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); + + // Sort sources + this.currentSources = src.sort(compareResolutions); + this.groupedSrc = bucketSources(this.currentSources); + // Pick one by default + var chosen = chooseSrc(this.groupedSrc, this.currentSources); + this.currentResolutionState = { + label: chosen.label, + sources: chosen.sources }; - 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 * @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 */ player.currentResolution = function(label, customSourcePicker){ - if(label == null) { return currentResolution; } - if(menuItemsHolder[label] != null){ - menuItemsHolder[label].onClick(customSourcePicker); + if(label == null) { return this.currentResolutionState; } + + // 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; }; @@ -210,7 +184,21 @@ * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } } */ 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]) { selectedLabel = groupedSrc.res[selectedRes][0].label; } - + 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 = []; - - qualities.map(function(q){ - _sources.push({ - src: player.src().src, - type: player.src().type, - label: _yts[q].label, - res: _yts[q].res, - _yt: _yts[q].yt - }); - }); - - groupedSrc = bucketSources(_sources); - - // Overwrite defualt sourcePicer function - var _customSourcePicker = function(_player, _sources, _label){ - 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(){ - if(player.options_.sources.length > 1){ - // tech: Html5 and Flash - // Create resolution switcher for videos form tag inside