video-quality-selector.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. /*!
  2. * @source: here!
  3. * @base: https://github.com/dominic-p/videojs-resolution-selector
  4. *
  5. * @licstart The following is the entire license notice for the
  6. * JavaScript code in this page.
  7. *
  8. * Copyleft 2016 Jesus Estupiñán Medina
  9. *
  10. * The JavaScript code in this page is free software: you can
  11. * redistribute it and/or modify it under the terms of the GNU
  12. * General Public License (GNU GPL) as published by the Free Software
  13. * Foundation, either version 3 of the License, or (at your option)
  14. * any later version. The code is distributed WITHOUT ANY WARRANTY;
  15. * without even the implied warranty of MERCHANTABILITY or FITNESS
  16. * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
  17. *
  18. * As additional permission under GNU GPL version 3 section 7, you
  19. * may distribute non-source (e.g., minimized or compacted) forms of
  20. * that code without the copy of the GNU GPL normally required by
  21. * section 4, provided you include this license notice and a URL
  22. * through which recipients can access the Corresponding Source.
  23. *
  24. * @licend The above is the entire license notice
  25. * for the JavaScript code in this page.
  26. *
  27. */
  28. /**
  29. * LibreVideoJS Resolution Selector
  30. *
  31. * This plugin for LibreVideo.js adds a resolution selector option
  32. * to the toolbar. Usage:
  33. *
  34. * <video>
  35. * <source data-res="480" src="..." />
  36. * <source data-res="240" src="..." />
  37. * </video>
  38. */
  39. (function( _V_ ) {
  40. /***********************************************************************************
  41. * Define some helper functions
  42. ***********************************************************************************/
  43. var methods = {
  44. /**
  45. * Utility function for merging 2 objects recursively. It treats
  46. * arrays like plain objects and it relies on a for...in loop which will
  47. * break if the Object prototype is messed with.
  48. *
  49. * @param (object) destination The object to modify and return
  50. * @param (object) source The object to use to overwrite the first
  51. * object
  52. *
  53. * @returns (object) The modified first object is returned
  54. */
  55. extend : function( destination, source ) {
  56. for ( var prop in source ) {
  57. // Sanity check
  58. if ( ! source.hasOwnProperty( prop ) ) { continue; }
  59. // Enable recursive (deep) object extension
  60. if ( typeof source[prop] == 'object' && null !== source[prop] ) {
  61. destination[prop] = methods.extend( destination[prop] || {}, source[prop] );
  62. } else {
  63. destination[prop] = source[prop];
  64. }
  65. }
  66. return destination;
  67. },
  68. /**
  69. * In a future version, this can be made more intelligent,
  70. * but for now, we'll just add a "p" at the end if we are passed
  71. * numbers.
  72. *
  73. * @param (string) res The resolution to make a label for
  74. *
  75. * @returns (string) The label text string
  76. */
  77. res_label : function( res ) {
  78. return ( /^\d+$/.test( res ) ) ? res + 'p' : res;
  79. },
  80. matchResolution: function(resStack, res) {
  81. },
  82. /**
  83. * returns a dummy object that implements the basic get/set
  84. * functionality of the Cookies library. Used in the case where
  85. * the Cookies library is not present
  86. */
  87. buildCookiesDummy: function() {
  88. return {
  89. get: function(key) {
  90. return "";
  91. },
  92. set: function(key, val) {
  93. return false;
  94. }
  95. };
  96. }
  97. };
  98. /***********************************************************************************
  99. * Setup our resolution menu items
  100. ***********************************************************************************/
  101. _V_.ResolutionMenuItem = _V_.MenuItem.extend({
  102. /** @constructor */
  103. init : function( player, options ){
  104. // Modify options for parent MenuItem class's init.
  105. options.label = methods.res_label( options.res );
  106. options.selected = ( options.res.toString() === player.getCurrentRes().toString() );
  107. // Call the parent constructor
  108. _V_.MenuItem.call( this, player, options );
  109. // Store the resolution as a call property
  110. this.resolution = options.res;
  111. // Register our click handler
  112. this.on( 'click', this.onClick );
  113. // Toggle the selected class whenever the resolution changes
  114. player.on( 'changeRes', _V_.bind( this, function() {
  115. if ( this.resolution == player.getCurrentRes() ) {
  116. this.selected( true );
  117. } else {
  118. this.selected( false );
  119. }
  120. }));
  121. }
  122. });
  123. // Handle clicks on the menu items
  124. _V_.ResolutionMenuItem.prototype.onClick = function() {
  125. var player = this.player(),
  126. video_el = player.el().firstChild,
  127. current_time = player.currentTime(),
  128. is_paused = player.paused(),
  129. button_nodes = player.controlBar.resolutionSelector.el().firstChild.children,
  130. button_node_count = button_nodes.length;
  131. // Do nothing if we aren't changing resolutions
  132. if ( player.getCurrentRes() == this.resolution ) { return; }
  133. // Make sure the loadedmetadata event will fire
  134. if ( 'none' == video_el.preload ) { video_el.preload = 'metadata'; }
  135. // Change the source and make sure we don't start the video over
  136. player.src( player.availableRes[this.resolution] ).one( 'loadedmetadata', function() {
  137. player.currentTime( current_time );
  138. if ( ! is_paused ) { player.play(); }
  139. });
  140. // Save the newly selected resolution in our player options property
  141. player.currentRes = this.resolution;
  142. // Update the button text
  143. while ( button_node_count > 0 ) {
  144. button_node_count--;
  145. if ( 'librevjs-current-res' == button_nodes[button_node_count].className ) {
  146. button_nodes[button_node_count].innerHTML = methods.res_label( this.resolution );
  147. break;
  148. }
  149. }
  150. // Update the classes to reflect the currently selected resolution
  151. player.trigger( 'changeRes' );
  152. };
  153. /***********************************************************************************
  154. * Setup our resolution menu title item
  155. ***********************************************************************************/
  156. _V_.ResolutionTitleMenuItem = _V_.MenuItem.extend({
  157. init : function( player, options ) {
  158. // Call the parent constructor
  159. _V_.MenuItem.call( this, player, options );
  160. // No click handler for the menu title
  161. this.off( 'click' );
  162. }
  163. });
  164. /***********************************************************************************
  165. * Define our resolution selector button
  166. ***********************************************************************************/
  167. _V_.ResolutionSelector = _V_.MenuButton.extend({
  168. /** @constructor */
  169. init : function( player, options ) {
  170. // Add our list of available resolutions to the player object
  171. player.availableRes = options.available_res;
  172. // Call the parent constructor
  173. _V_.MenuButton.call( this, player, options );
  174. }
  175. });
  176. // Create a menu item for each available resolution
  177. _V_.ResolutionSelector.prototype.createItems = function() {
  178. var player = this.player(),
  179. items = [],
  180. current_res;
  181. // Add the menu title item
  182. items.push( new _V_.ResolutionTitleMenuItem( player, {
  183. el : _V_.Component.prototype.createEl( 'li', {
  184. className : 'librevjs-menu-title librevjs-res-menu-title',
  185. innerHTML : 'Calidad'
  186. })
  187. }));
  188. // Add an item for each available resolution
  189. for ( current_res in player.availableRes ) {
  190. // Don't add an item for the length attribute
  191. if ( 'length' == current_res ) { continue; }
  192. items.push( new _V_.ResolutionMenuItem( player, {
  193. res : current_res
  194. }));
  195. }
  196. // Sort the available resolutions in descending order
  197. items.sort(function( a, b ) {
  198. if ( typeof a.resolution == 'undefined' ) {
  199. return -1;
  200. } else {
  201. return parseInt( b.resolution ) - parseInt( a.resolution );
  202. }
  203. });
  204. return items;
  205. };
  206. /***********************************************************************************
  207. * Register the plugin with cliplibrejs, main plugin function
  208. ***********************************************************************************/
  209. _V_.plugin( 'resolutionSelector', function( options ) {
  210. // Only enable the plugin on HTML5 videos
  211. if ( ! this.el().firstChild.canPlayType ) { return; }
  212. var player = this,
  213. sources = player.options().sources,
  214. i = sources.length,
  215. j,
  216. found_type,
  217. // Override default options with those provided
  218. settings = methods.extend({
  219. default_res : '', // (string) The resolution that should be selected by default ( '480' or '480,1080,240' )
  220. 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
  221. }, options || {} ),
  222. available_res = { length : 0 },
  223. current_res,
  224. resolutionSelector,
  225. // Split default resolutions if set and valid, otherwise default to an empty array
  226. default_resolutions = ( settings.default_res && typeof settings.default_res == 'string' ) ? settings.default_res.split( ',' ) : [],
  227. cookieNamespace = 'cliplibrejs.resolutionSelector',
  228. resCookieName = cookieNamespace + '.res',
  229. cookieRef = (typeof(Cookies) === "function") ? Cookies : methods.buildCookiesDummy();
  230. // Get all of the available resoloutions
  231. while ( i > 0 ) {
  232. i--;
  233. // Skip sources that don't have data-res attributes
  234. if ( ! sources[i]['data-res'] ) { continue; }
  235. current_res = sources[i]['data-res'];
  236. if ( typeof available_res[current_res] !== 'object' ) {
  237. available_res[current_res] = [];
  238. available_res.length++;
  239. }
  240. available_res[current_res].push( sources[i] );
  241. }
  242. // Check for forced types
  243. if ( settings.force_types ) {
  244. // Loop through all available resoultions
  245. for ( current_res in available_res ) {
  246. // Don't count the length property as a resolution
  247. if ( 'length' == current_res ) { continue; }
  248. i = settings.force_types.length;
  249. // For each resolution loop through the required types
  250. while ( i > 0 ) {
  251. i--;
  252. j = available_res[current_res].length;
  253. found_types = 0;
  254. // For each required type loop through the available sources to check if its there
  255. while ( j > 0 ) {
  256. j--;
  257. if ( settings.force_types[i] === available_res[current_res][j].type ) {
  258. found_types++;
  259. }
  260. } // End loop through current resolution sources
  261. if ( found_types < settings.force_types.length ) {
  262. delete available_res[current_res];
  263. available_res.length--;
  264. break;
  265. }
  266. } // End loop through required types
  267. } // End loop through resolutions
  268. }
  269. // Make sure we have at least 2 available resolutions before we add the button
  270. if ( available_res.length < 2 ) { return; }
  271. var resCookie = cookieRef.get(resCookieName)
  272. if (resCookie) {
  273. // rebuild the default_resolutions stack with the cookie's resolution on top
  274. default_resolutions = [resCookie].concat(default_resolutions);
  275. }
  276. // Loop through the choosen default resolutions if there were any
  277. for ( i = 0; i < default_resolutions.length; i++ ) {
  278. // Set the video to start out with the first available default res
  279. if ( available_res[default_resolutions[i]] ) {
  280. player.src( available_res[default_resolutions[i]] );
  281. player.currentRes = default_resolutions[i];
  282. break;
  283. }
  284. }
  285. // Helper function to get the current resolution
  286. player.getCurrentRes = function() {
  287. if ( typeof player.currentRes !== 'undefined' ) {
  288. return player.currentRes;
  289. } else {
  290. try {
  291. return res = player.options().sources[0]['data-res'];
  292. } catch(e) {
  293. return '';
  294. }
  295. }
  296. };
  297. // Get the started resolution
  298. current_res = player.getCurrentRes();
  299. if ( current_res ) { current_res = methods.res_label( current_res ); }
  300. // Add the resolution selector button
  301. resolutionSelector = new _V_.ResolutionSelector( player, {
  302. el : _V_.Component.prototype.createEl( null, {
  303. className : 'librevjs-res-button librevjs-menu-button librevjs-control',
  304. innerHTML : '<div class="librevjs-control-content"><span class="librevjs-current-res">' + ( current_res || 'Quality' ) + '</span></div>',
  305. role : 'button',
  306. 'aria-live' : 'polite', // let the screen reader user know that the text of the button may change
  307. tabIndex : 0
  308. }),
  309. available_res : available_res
  310. });
  311. // Attach an event to remember previous res selection via cookie
  312. this.on('changeRes', function() {
  313. cookieRef.set(resCookieName, player.getCurrentRes());
  314. });
  315. // Attach an event to update player.src once on loadedmetadata
  316. // if a resolution was previously set
  317. this.one('loadedmetadata', function() {
  318. var resCookie = cookieRef.get(resCookieName);
  319. if (resCookie) {
  320. player.src(player.availableRes[resCookie]);
  321. player.currentRes = resCookie;
  322. player.trigger( 'changeRes' );
  323. }
  324. });
  325. // Add the button to the control bar object and the DOM
  326. player.controlBar.resolutionSelector = player.controlBar.addChild( resolutionSelector );
  327. });
  328. })( cliplibrejs );