diff --git a/.travis.yml b/.travis.yml index 4f23153..41d37ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,6 @@ node_js: - 4 - 0.12 - 0.10 +before_install: npm install -g grunt-cli +install: npm install sudo: false diff --git a/Gruntfile.js b/Gruntfile.js index 89a9142..a0435f8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -81,4 +81,8 @@ module.exports = function(grunt) { 'qunit', 'concat', 'uglify']); + + grunt.registerTask('test', [ + 'jshint' + ]); }; diff --git a/README.md b/README.md index e3d6bac..c0a5550 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ -# Video.js Resolution Switcher +# Video.js Resolution Switcher [![Build Status](https://travis-ci.org/kmoskwiak/videojs-resolution-switcher.svg?branch=master)](https://travis-ci.org/kmoskwiak/videojs-resolution-switcher) Resolution switcher for [video.js v5](https://github.com/videojs/video.js) +## Example + +[Working examples](examples) of the plugin you can check out if you're having trouble. Or check out this [demo](https://kmoskwiak.github.io/videojs-resolution-switcher/). + ## Getting Started Install plugin with @@ -70,6 +74,36 @@ bower install videojs-resolution-switcher ``` + +### YouTube tech + +YouTube tech form https://github.com/eXon/videojs-youtube + +```html + + + + +``` + ### Flash tech When using flash tech `preload="auto"` is required. @@ -102,10 +136,9 @@ videojs('video', { ### Avalible options: * default - `{Number}|'low'|'high'` - default resolution. If any `Number` is passed plugin will try to choose source based on `res` parameter. If `low` or `high` is passed, plugin will choose respectively worse or best resolution (if `res` parameter is specified). If `res` parameter is not specified plugin assumes that sources array is sorted from best to worse. * dynamicLabel - `{Boolean}` - if `true` current label will be displayed in control bar. By default gear icon is displayed. +* customSourcePicker - `{Function}` - custom function for selecting source. +* ui - `{Boolean}` - If set to `false` button will not be displayed in control bar. Default is `true`. -## Example - -[Working example](example.html) of the plugin you can check out if you're having trouble. Or check out this [demo](https://kmoskwiak.github.io/videojs-resolution-switcher/). ## Methods @@ -127,7 +160,7 @@ player.updateSrc([ |:----:|:----:|:--------:|:-----------:| | source| array| no | array of sources | -### currentResolution([label]) +### currentResolution([label], [customSourcePicker]) If used as getter returns current resolution object: ```javascript { @@ -154,6 +187,51 @@ player.currentResolution('SD'); // returns videojs player object | name | type | required | description | |:----:|:----:|:--------:|:-----------:| | label| string| no | label name | +| customSourcePicker | function | no | cutom function to choose source | + +#### customSourcePicker +If there is more than one source with the same label, player will choose source automatically. This behavior can be changed if `customSourcePicker` is passed. + +`customSourcePicker` must return `player` object. +```javascript +player.currentResolution('SD', function(_player, _sources, _label){ + return _player.src(_sources[0]); \\ Always select first source in array +}); +``` +`customSourcePicker` accepst 3 arguments. + +| name | type | required | description | +|:----:|:----:|:--------:|:-----------:| +| player| Object | yes | videojs player object | +| sources | Array | no | array of sources | +| label | String | no | name of label | + +`customSourcePicker` may be passed in options when player is initialized: +```javascript + +var myCustomSrcPicker = function(_p, _s, _l){ + // select any source you want + return _p.src(selectedSource); +} + +videojs('video', { + controls: true, + muted: true, + width: 1000, + plugins: { + videoJsResolutionSwitcher: { + default: 'low', + customSourcePicker: myCustomSrcPicker + } + } + }, function(){ + // this is player + }) +``` + + +### getGroupedSrc() +Returns sources grouped by label, resolution and type. ## Events @@ -161,5 +239,3 @@ player.currentResolution('SD'); // returns videojs player object ### resolutionchange `EVENT` > Fired when resolution is changed - - diff --git a/bower.json b/bower.json index 6f0c2a7..7e734d4 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "vjs-resolution-switcher", - "version": "0.2.3", + "version": "0.4.2", "authors": [ "Kasper Moskwiak " ], diff --git a/examples/flash.html b/examples/flash.html new file mode 100644 index 0000000..60dfb11 --- /dev/null +++ b/examples/flash.html @@ -0,0 +1,81 @@ + + + + + Video.js Resolution Switcher + + + + + + +
+

+ Use flash +

+
+ + + + + + + + + diff --git a/examples/hls.html b/examples/hls.html new file mode 100644 index 0000000..aa3d19e --- /dev/null +++ b/examples/hls.html @@ -0,0 +1,65 @@ + + + + + Video.js Resolution Switcher + + + + + + +
+

+ HLS tech +

+
+ + + + + + + + + diff --git a/example.html b/examples/html5.html similarity index 62% rename from example.html rename to examples/html5.html index cdf913e..7a09956 100644 --- a/example.html +++ b/examples/html5.html @@ -4,8 +4,8 @@ Video.js Resolution Switcher - - + + + + +
+

+ Youtube tech +

+
+ + + + + + + + + + + \ No newline at end of file diff --git a/lib/videojs-resolution-switcher.css b/lib/videojs-resolution-switcher.css index a2224ce..3c7ef67 100644 --- a/lib/videojs-resolution-switcher.css +++ b/lib/videojs-resolution-switcher.css @@ -1,30 +1,31 @@ -.vjs-resolution-button { - color: #ccc; - font-family: VideoJS; -} - -.vjs-resolution-button .vjs-resolution-button-staticlabel:before { +.vjs-resolution-button .vjs-menu-icon:before { content: '\f110'; + font-family: VideoJS; + font-weight: normal; + font-style: normal; font-size: 1.8em; - line-height: 1.67; + line-height: 1.67em; } .vjs-resolution-button .vjs-resolution-button-label { - font-size: 1.2em; - line-height: 2.50em; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - box-sizing: inherit; -} - -.vjs-resolution-button ul.vjs-menu-content { - width: 4em !important; -} - -.vjs-resolution-button .vjs-menu { + font-size: 1em; + line-height: 3em; + position: absolute; + top: 0; left: 0; + width: 100%; + height: 100%; + text-align: center; + box-sizing: inherit; +} + +.vjs-resolution-button .vjs-menu .vjs-menu-content { + width: 4em; + left: 50%; /* Center the menu, in it's parent */ + margin-left: -2em; /* half of width, to center */ +} + +.vjs-resolution-button .vjs-menu li { + text-transform: none; + font-size: 1em; } diff --git a/lib/videojs-resolution-switcher.js b/lib/videojs-resolution-switcher.js index 20f5621..eb8adbf 100644 --- a/lib/videojs-resolution-switcher.js +++ b/lib/videojs-resolution-switcher.js @@ -1,6 +1,6 @@ -/*! videojs-resolution-switcher - v0.0.0 - 2015-7-26 - * Copyright (c) 2015 Kasper Moskwiak - * Modified by Pierre Kraft +/*! videojs-resolution-switcher - 2015-7-26 + * Copyright (c) 2016 Kasper Moskwiak + * Modified by Pierre Kraft and Derk-Jan Hartman * Licensed under the Apache-2.0 license. */ (function() { @@ -15,136 +15,89 @@ } (function(window, videojs) { - - - var defaults = {}, - videoJsResolutionSwitcher, - currentResolution = {}, // stores current resolution - menuItemsHolder = {}; // stores menuItems - - function setSourcesSanitized(player, sources, label, customSourcePicker) { - currentResolution = { - label: label, - sources: sources + var videoJsResolutionSwitcher, + defaults = { + ui: true }; - 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; + /* + * 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; - this.on('click', this.onClick); - this.on('touchstart', this.onClick); - - if (options.initialySelected) { - this.showAsLabel(); - this.selected(true); + player.on('resolutionchange', videojs.bind(this, this.update)); } - }, - 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; - } - }, - onClick: function(customSourcePicker){ - this.onClickListener(this); - // Remember player state - var currentTime = this.player_.currentTime(); - var isPaused = this.player_.paused(); - this.showAsLabel(); - // 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_.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'); - staticLabel.classList.add('vjs-resolution-button-staticlabel'); + videojs.addClass(staticLabel, 'vjs-menu-icon'); 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); - }); - }; - - 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. @@ -153,51 +106,116 @@ videoJsResolutionSwitcher = function(options) { var settings = videojs.mergeOptions(defaults, options), player = this, - label = document.createElement('span'), - groupedSrc = {}; - - label.classList.add('vjs-resolution-button-label'); + groupedSrc = {}, + currentSources = {}, + currentResolutionState = {}; /** * Updates player sources or returns current source URL * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}] * @returns {Object|String|Array} videojs player object if used as setter or current source URL, object, or array of sources */ - player.updateSrc = function(src){ + player.updateSrc = function(src, _options){ //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; - } + + if(_options && _options.hls){ + player.src(src); + return initResolutionForHLS(player); + } + + // Only add those sources which we can (maybe) play + src = src.filter( function(source) { + try { + return ( player.canPlayType( source.type ) !== '' ); + } catch (e) { + // If a Tech doesn't yet have canPlayType just add it + return true; + } + }); + //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); - menuButton.el().classList.add('vjs-resolution-button'); - player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton); - return setSourcesSanitized(player, choosen.sources, choosen.label); + 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 + }; + + 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_.options_.bigPlayButton){ + 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; }; - + + /** + * Returns grouped sources by label, resolution and type + * @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; + }; /** * Method used for sorting list of sources @@ -255,7 +273,7 @@ if (selectedRes === 'high') { selectedRes = src[0].res; selectedLabel = src[0].label; - } else if (selectedRes === 'low' || selectedRes == null) { + } else if (selectedRes === 'low' || selectedRes == null || !groupedSrc.res[selectedRes]) { // Select low-res if default is low or not set selectedRes = src[src.length - 1].res; selectedLabel = src[src.length -1].label; @@ -263,19 +281,164 @@ selectedLabel = groupedSrc.res[selectedRes][0].label; } - if(selectedRes === undefined){ - return {res: selectedRes, label: selectedLabel, sources: groupedSrc.label[selectedLabel]}; - } return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]}; } - // Create resolution switcher for videos form tag inside