├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── example.html ├── package.json ├── video-quality-selector.css └── video-quality-selector.js /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude everything except the main files 2 | **/* 3 | !video-quality-selector.css 4 | !video-quality-selector.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Dominic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video.js Resolution Selector 2 | Add a resolution selector button to Video.js to allow users to manually adjust the video quality. 3 | 4 | ## Install 5 | You can use bower (`bower install videojs-resolution-selector`), npm (`npm install videojs-resolution-selector`), or simply download the source from this repo. You must be running Video.js 4.7.3 or higher for this plugin to function. You can download the latest source at the [main Video.js repo](https://github.com/videojs/video.js), or you can get production files from [videojs.com](http://videojs.com), or you can use the CDN files. 6 | 7 | ## Usage 8 | Add an extra attribute to your `` elements. 9 | ```html 10 | 14 | ``` 15 | 16 | Enable the plugin as described in the [video.js docs](https://github.com/videojs/video.js/blob/v4.5.2/docs/guides/plugins.md#step-3-using-a-plugin). Optionally, you can pass some settings to the plugin: 17 | ```javascript 18 | videojs( '#my-video', { plugins : { resolutionSelector : { 19 | force_types : [ 'video/mp4', 'video/webm' ], 20 | default_res : "480" 21 | } } } ); 22 | ``` 23 | 24 | `force_types` is an array. The plugin will check each resolution to make sure there is a source of each type at that resolution. 25 | 26 | `default_res` must be a string. You can either specify a single resolution or a comma separated list (e.g. `"480,240"`). When using a list, the first available resolution in the list will be selected by default. 27 | 28 | The plugin also triggers a `changeRes` event on the player instance anytime the resolution is changed, so your code can listen for that and take any desired action on resolution changes: 29 | ```javascript 30 | videojs( '#my-video', { plugins : { resolutionSelector : {} } }, function() { 31 | 32 | var player = this; 33 | 34 | player.on( 'changeRes', function() { 35 | 36 | console.log( 'Current Res is: ' + player.getCurrentRes() ); 37 | }); 38 | }); 39 | ``` 40 | The plugin provides a `changeRes` method on the `player` object. You can call it like so (after your player is ready): `player.changeRes( '480' )`. 41 | 42 | ## Simple Example 43 | ```html 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | ``` 60 | Please see example.html for a more advanced example. 61 | 62 | ## Styling the Button 63 | By default, the button will not be visible. You will either need to include the styles from `video-quality-selector.css` (after the default Video.js styles to override them), or use your own icon for the button. To match the rest of the Video.js controls, I recommend using an icon font to style the button, but it's up to you. 64 | 65 | ## Mobile devices 66 | If you want this plugin to work on mobile devices, you need to enable the video.js controls because the native controls are default on iOS and Android. 67 | 68 | ```html 69 | 72 | ``` 73 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-resolution-selector", 3 | "description": "Adds a resolution selector button to Video.js to allow users to manually adjust the video quality.", 4 | "version": "1.6.2", 5 | "main": [ 6 | "video-quality-selector.js", 7 | "video-quality-selector.css" 8 | ], 9 | "ignore": [ 10 | "**/*" 11 | ], 12 | "dependencies": { 13 | "video.js": "^4.7" 14 | } 15 | } -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Video.js | Resolution Selector Plugin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Example Setup

18 | 19 | 20 | 24 | 25 | 26 | 30 | 31 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-resolution-selector", 3 | "description": "Adds a resolution selector button to Video.js to allow users to manually adjust the video quality.", 4 | "version": "1.6.2", 5 | "author": "Dominic P", 6 | "license": "MIT", 7 | "main": "video-quality-selector.js", 8 | "style": "video-quality-selector.css", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dominic-p/videojs-resolution-selector.git" 12 | }, 13 | "dependencies": {}, 14 | "bugs": { 15 | "url": "https://github.com/dominic-p/videojs-resolution-selector/issues" 16 | }, 17 | "keywords": [ 18 | "videojs", 19 | "resolution", 20 | "selector" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /video-quality-selector.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /* 3 | You are free to style the button however you wish. I plan to use 4 | an icon from my site's own icon font to make it more visible. These 5 | are just basic styles to make it look ok with plain text. 6 | */ 7 | 8 | /* Position the button */ 9 | .vjs-res-button { 10 | float: right; 11 | line-height: 3em; 12 | } 13 | 14 | /* Don't show hover effects on title */ 15 | ul li.vjs-menu-title.vjs-res-menu-title:hover { 16 | cursor: default; 17 | background-color: transparent; 18 | color: #CCC; 19 | -moz-box-shadow: none; 20 | -webkit-box-shadow: none; 21 | box-shadow: none; 22 | } 23 | 24 | /* Needed to keep text visible in video.js 4.9 */ 25 | .vjs-res-button .vjs-control-text { 26 | width: auto; 27 | height: auto; 28 | clip: auto; 29 | } -------------------------------------------------------------------------------- /video-quality-selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Video.js Resolution Selector 3 | * 4 | * This plugin for Video.js adds a resolution selector option 5 | * to the toolbar. Usage: 6 | * 7 | * 11 | */ 12 | 13 | (function( _V_ ) { 14 | 15 | /*********************************************************************************** 16 | * Define some helper functions 17 | ***********************************************************************************/ 18 | var methods = { 19 | 20 | /** 21 | * In a future version, this can be made more intelligent, 22 | * but for now, we'll just add a "p" at the end if we are passed 23 | * numbers. 24 | * 25 | * @param (string) res The resolution to make a label for 26 | * 27 | * @returns (string) The label text string 28 | */ 29 | res_label : function( res ) { 30 | 31 | return ( /^\d+$/.test( res ) ) ? res + 'p' : res; 32 | } 33 | }; 34 | 35 | /*********************************************************************************** 36 | * Setup our resolution menu items 37 | ***********************************************************************************/ 38 | _V_.ResolutionMenuItem = _V_.MenuItem.extend({ 39 | 40 | // Call variable to prevent the resolution change from being called twice 41 | call_count : 0, 42 | 43 | /** @constructor */ 44 | init : function( player, options ){ 45 | 46 | var touchstart = false; 47 | 48 | // Modify options for parent MenuItem class's init. 49 | options.label = methods.res_label( options.res ); 50 | options.selected = ( options.res.toString() === player.getCurrentRes().toString() ); 51 | 52 | // Call the parent constructor 53 | _V_.MenuItem.call( this, player, options ); 54 | 55 | // Store the resolution as a property 56 | this.resolution = options.res; 57 | 58 | // Register our click and tap handlers 59 | this.on( ['click', 'tap'], this.onClick ); 60 | 61 | // Toggle the selected class whenever the resolution changes 62 | player.on( 'changeRes', _V_.bind( this, function() { 63 | 64 | if ( this.resolution == player.getCurrentRes() ) { 65 | 66 | this.selected( true ); 67 | 68 | } else { 69 | 70 | this.selected( false ); 71 | } 72 | 73 | // Reset the call count 74 | this.call_count = 0; 75 | })); 76 | } 77 | }); 78 | 79 | // Handle clicks on the menu items 80 | _V_.ResolutionMenuItem.prototype.onClick = function() { 81 | 82 | // Check if this has already been called 83 | if ( this.call_count > 0 ) { return; } 84 | 85 | // Call the player.changeRes method 86 | this.player().changeRes( this.resolution ); 87 | 88 | // Increment the call counter 89 | this.call_count++; 90 | }; 91 | 92 | /*********************************************************************************** 93 | * Setup our resolution menu title item 94 | ***********************************************************************************/ 95 | _V_.ResolutionTitleMenuItem = _V_.MenuItem.extend({ 96 | 97 | init : function( player, options ) { 98 | 99 | // Call the parent constructor 100 | _V_.MenuItem.call( this, player, options ); 101 | 102 | // No click handler for the menu title 103 | this.off( 'click' ); 104 | } 105 | }); 106 | 107 | /*********************************************************************************** 108 | * Define our resolution selector button 109 | ***********************************************************************************/ 110 | _V_.ResolutionSelector = _V_.MenuButton.extend({ 111 | 112 | /** @constructor */ 113 | init : function( player, options ) { 114 | 115 | // Add our list of available resolutions to the player object 116 | player.availableRes = options.available_res; 117 | 118 | // Call the parent constructor 119 | _V_.MenuButton.call( this, player, options ); 120 | 121 | // Set the button text based on the option provided 122 | this.el().firstChild.firstChild.innerHTML = options.buttonText; 123 | } 124 | }); 125 | 126 | // Set class for resolution selector button 127 | _V_.ResolutionSelector.prototype.className = 'vjs-res-button'; 128 | 129 | // Create a menu item for each available resolution 130 | _V_.ResolutionSelector.prototype.createItems = function() { 131 | 132 | var player = this.player(), 133 | items = [], 134 | current_res; 135 | 136 | // Add the menu title item 137 | items.push( new _V_.ResolutionTitleMenuItem( player, { 138 | 139 | el : _V_.Component.prototype.createEl( 'li', { 140 | 141 | className : 'vjs-menu-title vjs-res-menu-title', 142 | innerHTML : player.localize( 'Quality' ) 143 | }) 144 | })); 145 | 146 | // Add an item for each available resolution 147 | for ( current_res in player.availableRes ) { 148 | 149 | // Don't add an item for the length attribute 150 | if ( 'length' == current_res ) { continue; } 151 | 152 | items.push( new _V_.ResolutionMenuItem( player, { 153 | res : current_res 154 | })); 155 | } 156 | 157 | // Sort the available resolutions in descending order 158 | items.sort(function( a, b ) { 159 | 160 | if ( typeof a.resolution == 'undefined' ) { 161 | 162 | return -1; 163 | 164 | } else { 165 | 166 | return parseInt( b.resolution ) - parseInt( a.resolution ); 167 | } 168 | }); 169 | 170 | return items; 171 | }; 172 | 173 | /*********************************************************************************** 174 | * Register the plugin with videojs, main plugin function 175 | ***********************************************************************************/ 176 | _V_.plugin( 'resolutionSelector', function( options ) { 177 | 178 | // Only enable the plugin on HTML5 videos 179 | if ( ! this.el().firstChild.canPlayType ) { return; } 180 | 181 | /******************************************************************* 182 | * Setup variables, parse settings 183 | *******************************************************************/ 184 | var player = this, 185 | sources = player.options().sources, 186 | i = sources.length, 187 | j, 188 | found_type, 189 | 190 | // Override default options with those provided 191 | settings = _V_.util.mergeOptions({ 192 | 193 | default_res : '', // (string) The resolution that should be selected by default ( '480' or '480,1080,240' ) 194 | force_types : false // (array) List of media types. If passed, we need to have source for each type in each resolution or that resolution will not be an option 195 | 196 | }, options || {} ), 197 | 198 | available_res = { length : 0 }, 199 | current_res, 200 | resolutionSelector, 201 | 202 | // Split default resolutions if set and valid, otherwise default to an empty array 203 | default_resolutions = ( settings.default_res && typeof settings.default_res == 'string' ) ? settings.default_res.split( ',' ) : []; 204 | 205 | // Get all of the available resoloutions 206 | while ( i > 0 ) { 207 | 208 | i--; 209 | 210 | // Skip sources that don't have data-res attributes 211 | if ( ! sources[i]['data-res'] ) { continue; } 212 | 213 | current_res = sources[i]['data-res']; 214 | 215 | if ( typeof available_res[current_res] !== 'object' ) { 216 | 217 | available_res[current_res] = []; 218 | available_res.length++; 219 | } 220 | 221 | available_res[current_res].unshift( sources[i] ); 222 | } 223 | 224 | // Check for forced types 225 | if ( settings.force_types ) { 226 | 227 | // Loop through all available resoultions 228 | for ( current_res in available_res ) { 229 | 230 | // Don't count the length property as a resolution 231 | if ( 'length' == current_res ) { continue; } 232 | 233 | i = settings.force_types.length; 234 | found_types = 0; 235 | 236 | // Loop through all required types 237 | while ( i > 0 ) { 238 | 239 | i--; 240 | 241 | j = available_res[current_res].length; 242 | 243 | // Loop through all available sources in current resolution 244 | while ( j > 0 ) { 245 | 246 | j--; 247 | 248 | // Check if the current source matches the current type we're checking 249 | if ( settings.force_types[i] === available_res[current_res][j].type ) { 250 | 251 | found_types++; 252 | break; 253 | } 254 | } 255 | } 256 | 257 | // If we didn't find sources for all of the required types in the current res, remove it 258 | if ( found_types < settings.force_types.length ) { 259 | 260 | delete available_res[current_res]; 261 | available_res.length--; 262 | } 263 | } 264 | } 265 | 266 | // Make sure we have at least 2 available resolutions before we add the button 267 | if ( available_res.length < 2 ) { return; } 268 | 269 | // Loop through the choosen default resolutions if there were any 270 | for ( i = 0; i < default_resolutions.length; i++ ) { 271 | 272 | // Set the video to start out with the first available default res 273 | if ( available_res[default_resolutions[i]] ) { 274 | 275 | player.src( available_res[default_resolutions[i]] ); 276 | player.currentRes = default_resolutions[i]; 277 | break; 278 | } 279 | } 280 | 281 | /******************************************************************* 282 | * Add methods to player object 283 | *******************************************************************/ 284 | 285 | // Make sure we have player.localize() if it's not defined by Video.js 286 | if ( typeof player.localize !== 'function' ) { 287 | 288 | player.localize = function( string ) { 289 | 290 | return string; 291 | }; 292 | } 293 | 294 | // Helper function to get the current resolution 295 | player.getCurrentRes = function() { 296 | 297 | if ( typeof player.currentRes !== 'undefined' ) { 298 | 299 | return player.currentRes; 300 | 301 | } else { 302 | 303 | try { 304 | 305 | return res = player.options().sources[0]['data-res']; 306 | 307 | } catch(e) { 308 | 309 | return ''; 310 | } 311 | } 312 | }; 313 | 314 | // Define the change res method 315 | player.changeRes = function( target_resolution ) { 316 | 317 | var video_el = player.el().firstChild, 318 | is_paused = player.paused(), 319 | current_time = player.currentTime(), 320 | button_nodes, 321 | button_node_count; 322 | 323 | // Do nothing if we aren't changing resolutions or if the resolution isn't defined 324 | if ( player.getCurrentRes() == target_resolution 325 | || ! player.availableRes 326 | || ! player.availableRes[target_resolution] ) { return; } 327 | 328 | // Make sure the loadedmetadata event will fire 329 | if ( 'none' == video_el.preload ) { video_el.preload = 'metadata'; } 330 | 331 | // Change the source and make sure we don't start the video over 332 | player.src( player.availableRes[target_resolution] ).one( 'loadedmetadata', function() { 333 | 334 | player.currentTime( current_time ); 335 | 336 | // If the video was paused, don't show the poster image again 337 | player.addClass( 'vjs-has-started' ); 338 | 339 | if ( ! is_paused ) { player.play(); } 340 | }); 341 | 342 | // Save the newly selected resolution in our player options property 343 | player.currentRes = target_resolution; 344 | 345 | // Make sure the button has been added to the control bar 346 | if ( player.controlBar.resolutionSelector ) { 347 | 348 | button_nodes = player.controlBar.resolutionSelector.el().firstChild.children; 349 | button_node_count = button_nodes.length; 350 | 351 | // Update the button text 352 | while ( button_node_count > 0 ) { 353 | 354 | button_node_count--; 355 | 356 | if ( 'vjs-control-text' == button_nodes[button_node_count].className ) { 357 | 358 | button_nodes[button_node_count].innerHTML = methods.res_label( target_resolution ); 359 | break; 360 | } 361 | } 362 | } 363 | 364 | // Update the classes to reflect the currently selected resolution 365 | player.trigger( 'changeRes' ); 366 | }; 367 | 368 | /******************************************************************* 369 | * Add the resolution selector button 370 | *******************************************************************/ 371 | 372 | // Get the starting resolution 373 | current_res = player.getCurrentRes(); 374 | 375 | if ( current_res ) { current_res = methods.res_label( current_res ); } 376 | 377 | // Add the resolution selector button 378 | resolutionSelector = new _V_.ResolutionSelector( player, { 379 | buttonText : player.localize( current_res || 'Quality' ), 380 | available_res : available_res 381 | }); 382 | 383 | // Add the button to the control bar object and the DOM 384 | player.controlBar.resolutionSelector = player.controlBar.addChild( resolutionSelector ); 385 | }); 386 | 387 | })( videojs ); --------------------------------------------------------------------------------