Skip to content

Commit 9685fd9

Browse files
committed
Refactor and fix some bugs. Fixes #7.
- We now use a preload system to help make changing sources more seamless - Removed the redundant `extend` method - Fixed a bug the caused the `force_types` setting to not be respected - Removed issue tracking from readme (that should be done in issue tracker) - Added a generic changeRes method - Fixed bug where click handler was added twice
1 parent a4d237f commit 9685fd9

File tree

2 files changed

+138
-97
lines changed

2 files changed

+138
-97
lines changed

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,12 @@ The plugin also triggers a `changeRes` event on the player instance anytime the
3636
});
3737
});
3838

39+
The plugin provides a `changeRes` method on the `player` object. You can call it like so (after your player is ready): `player.changeRes( '480' )`.
40+
3941
Mobile devices
4042
--------------
4143
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.
4244

4345
<video data-setup='{"customControlsOnMobile": true}'>
4446
...
4547
</video>
46-
47-
Things to Work On
48-
-----------------
49-
- It would be really cool if this supported an "auto" option that used MPEG-DASH and/or HLS to enable adaptive resolution videos in addition to manual selection. [DASH playback](https://github.com/Dash-Industry-Forum/dash.js/tree/development/contrib/videojs) tech now available. See [blog post](http://blog.videojs.com/post/92536319027/dash-everywhere-ish-hack-project) on DASH support.
50-
- We're relying on several `for...in` style loops. This isn't ideal, and it should be changed.
51-
- Add a generic `change_res` method on the `player` object to allow resolution to be changed via js instead of just `onClick`
52-
- Implement a preload system to make changing resolutions smoother (see [issue #7](https://github.com/dominic-p/videojs-resolution-selector/issues/7))
53-
- Right now, this only works for HTML5 videos. In theory, it could be made to [work with Flash](http://help.videojs.com/discussions/questions/605-advise-for-setting-up-video-quality-resolution-selector#comment_15079585).

video-quality-selector.js

Lines changed: 136 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,6 @@
1717
***********************************************************************************/
1818
var methods = {
1919

20-
/**
21-
* Utility function for merging 2 objects recursively. It treats
22-
* arrays like plain objects and it relies on a for...in loop which will
23-
* break if the Object prototype is messed with.
24-
*
25-
* @param (object) destination The object to modify and return
26-
* @param (object) source The object to use to overwrite the first
27-
* object
28-
*
29-
* @returns (object) The modified first object is returned
30-
*/
31-
extend : function( destination, source ) {
32-
33-
for ( var prop in source ) {
34-
35-
// Sanity check
36-
if ( ! source.hasOwnProperty( prop ) ) { continue; }
37-
38-
// Enable recursive (deep) object extension
39-
if ( typeof source[prop] == 'object' && null !== source[prop] ) {
40-
41-
destination[prop] = methods.extend( destination[prop] || {}, source[prop] );
42-
43-
} else {
44-
45-
destination[prop] = source[prop];
46-
}
47-
}
48-
49-
return destination;
50-
},
51-
5220
/**
5321
* In a future version, this can be made more intelligent,
5422
* but for now, we'll just add a "p" at the end if we are passed
@@ -81,12 +49,12 @@
8149
// Call the parent constructor
8250
_V_.MenuItem.call( this, player, options );
8351

84-
// Store the resolution as a call property
52+
// Store the resolution as a property
8553
this.resolution = options.res;
8654

87-
// Register our click handler
88-
this.on( 'click', this.onClick );
89-
55+
// The parent constructor registers the click handler for us, so this would cause the event to fire twice
56+
// this.on( 'click', this.onClick );
57+
9058
// Register touch handler
9159
this.on( 'touchstart', function () { touchstart = true; });
9260

@@ -121,44 +89,8 @@
12189
// Handle clicks on the menu items
12290
_V_.ResolutionMenuItem.prototype.onClick = function() {
12391

124-
var player = this.player(),
125-
video_el = player.el().firstChild,
126-
current_time = player.currentTime(),
127-
is_paused = player.paused(),
128-
button_nodes = player.controlBar.resolutionSelector.el().firstChild.children,
129-
button_node_count = button_nodes.length;
130-
131-
// Do nothing if we aren't changing resolutions
132-
if ( player.getCurrentRes() == this.resolution ) { return; }
133-
134-
// Make sure the loadedmetadata event will fire
135-
if ( 'none' == video_el.preload ) { video_el.preload = 'metadata'; }
136-
137-
// Change the source and make sure we don't start the video over
138-
player.src( player.availableRes[this.resolution] ).one( 'loadedmetadata', function() {
139-
140-
player.currentTime( current_time );
141-
142-
if ( ! is_paused ) { player.play(); }
143-
});
144-
145-
// Save the newly selected resolution in our player options property
146-
player.currentRes = this.resolution;
147-
148-
// Update the button text
149-
while ( button_node_count > 0 ) {
150-
151-
button_node_count--;
152-
153-
if ( 'vjs-current-res' == button_nodes[button_node_count].className ) {
154-
155-
button_nodes[button_node_count].innerHTML = methods.res_label( this.resolution );
156-
break;
157-
}
158-
}
159-
160-
// Update the classes to reflect the currently selected resolution
161-
player.trigger( 'changeRes' );
92+
// Call the player.changeRes method
93+
this.player().changeRes( this.resolution );
16294
};
16395

16496
/***********************************************************************************
@@ -242,16 +174,19 @@
242174
_V_.plugin( 'resolutionSelector', function( options ) {
243175

244176
// Only enable the plugin on HTML5 videos
245-
if ( ! this.el().firstChild.canPlayType ) { return; }
177+
if ( ! this.el().firstChild.canPlayType ) { return; }
246178

179+
/*******************************************************************
180+
* Setup variables, parse settings
181+
*******************************************************************/
247182
var player = this,
248183
sources = player.options().sources,
249184
i = sources.length,
250185
j,
251186
found_type,
252187

253188
// Override default options with those provided
254-
settings = methods.extend({
189+
settings = _V_.util.mergeOptions({
255190

256191
default_res : '', // (string) The resolution that should be selected by default ( '480' or '480,1080,240' )
257192
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
@@ -294,34 +229,36 @@
294229
if ( 'length' == current_res ) { continue; }
295230

296231
i = settings.force_types.length;
232+
found_types = 0;
297233

298-
// For each resolution loop through the required types
234+
// Loop through all required types
299235
while ( i > 0 ) {
300236

301237
i--;
302238

303239
j = available_res[current_res].length;
304-
found_types = 0;
305240

306-
// For each required type loop through the available sources to check if its there
241+
// Loop through all available sources in current resolution
307242
while ( j > 0 ) {
308243

309244
j--;
310245

246+
// Check if the current source matches the current type we're checking
311247
if ( settings.force_types[i] === available_res[current_res][j].type ) {
312248

313249
found_types++;
250+
break;
314251
}
315-
} // End loop through current resolution sources
316-
317-
if ( found_types < settings.force_types.length ) {
318-
319-
delete available_res[current_res];
320-
available_res.length--;
321-
break;
322252
}
323-
} // End loop through required types
324-
} // End loop through resolutions
253+
}
254+
255+
// If we didn't find sources for all of the required types in the current res, remove it
256+
if ( found_types < settings.force_types.length ) {
257+
258+
delete available_res[current_res];
259+
available_res.length--;
260+
}
261+
}
325262
}
326263

327264
// Make sure we have at least 2 available resolutions before we add the button
@@ -339,6 +276,10 @@
339276
}
340277
}
341278

279+
/*******************************************************************
280+
* Add methods to player object
281+
*******************************************************************/
282+
342283
// Helper function to get the current resolution
343284
player.getCurrentRes = function() {
344285

@@ -359,7 +300,113 @@
359300
}
360301
};
361302

362-
// Get the started resolution
303+
// Define the change res method
304+
player.changeRes = function( target_resolution ) {
305+
306+
var video_el = player.el().firstChild,
307+
is_paused = player.paused(),
308+
current_time,
309+
button_nodes,
310+
button_node_count,
311+
dummy_vid,
312+
dummy_src,
313+
doc = document,
314+
i;
315+
316+
// Do nothing if we aren't changing resolutions or if the resolution isn't defined
317+
if ( player.getCurrentRes() == target_resolution
318+
|| ! player.availableRes
319+
|| ! player.availableRes[target_resolution] ) { return; }
320+
321+
// Make sure the loadedmetadata event will fire
322+
if ( 'none' == video_el.preload ) { video_el.preload = 'metadata'; }
323+
324+
// Preload the new resolution
325+
player.addClass( 'vjs-waiting' );
326+
327+
// Create a dummy video element to use to preload the new resolution
328+
dummy_vid = doc.createElement( 'video' );
329+
330+
i = player.availableRes[target_resolution].length;
331+
332+
while ( i > 0 ) {
333+
334+
i--;
335+
336+
dummy_src = doc.createElement( 'source' );
337+
dummy_src.setAttribute( 'src', player.availableRes[target_resolution][i].src );
338+
dummy_src.setAttribute( 'type', player.availableRes[target_resolution][i].type );
339+
340+
dummy_vid.appendChild( dummy_src );
341+
}
342+
343+
// Init Video.js on our dummy video
344+
_V_( dummy_vid, {}, function() {
345+
346+
dummy_vid = this;
347+
348+
// Wait for the dummy video to be ready
349+
dummy_vid.one( 'loadedmetadata', function() {
350+
351+
// Set the dummy video to the current time
352+
dummy_vid.currentTime( player.currentTime() );
353+
354+
// Wait for the dummy video to be ready again after setting the time
355+
dummy_vid.one( 'canplay', function() {
356+
357+
player.removeClass( 'vjs-waiting' );
358+
359+
// Get the current time
360+
current_time = player.currentTime();
361+
362+
// Change the source and make sure we don't start the video over
363+
player.src( player.availableRes[target_resolution] ).one( 'loadedmetadata', function() {
364+
365+
player.currentTime( current_time );
366+
367+
// If the video was paused, don't show the poster image again
368+
player.addClass( 'vjs-has-started' );
369+
370+
if ( ! is_paused ) { player.play(); }
371+
});
372+
373+
// Save the newly selected resolution in our player options property
374+
player.currentRes = target_resolution;
375+
376+
// Make sure the button has been added to the control bar
377+
if ( player.controlBar.resolutionSelector ) {
378+
379+
button_nodes = player.controlBar.resolutionSelector.el().firstChild.children;
380+
button_node_count = button_nodes.length;
381+
382+
// Update the button text
383+
while ( button_node_count > 0 ) {
384+
385+
button_node_count--;
386+
387+
if ( 'vjs-current-res' == button_nodes[button_node_count].className ) {
388+
389+
button_nodes[button_node_count].innerHTML = methods.res_label( target_resolution );
390+
break;
391+
}
392+
}
393+
}
394+
395+
// Update the classes to reflect the currently selected resolution
396+
player.trigger( 'changeRes' );
397+
398+
// Destroy our dummy video
399+
dummy_vid.dispose();
400+
});
401+
});
402+
});
403+
};
404+
405+
/*******************************************************************
406+
* Add the resolution selector button
407+
*******************************************************************/
408+
409+
// Get the starting resolution
363410
current_res = player.getCurrentRes();
364411

365412
if ( current_res ) { current_res = methods.res_label( current_res ); }

0 commit comments

Comments
 (0)