From 0f56382f84b82779a7eaa6d292f4f6d079e05fcd Mon Sep 17 00:00:00 2001 From: Jeremy Zimm Date: Sat, 28 Mar 2015 19:26:07 -0700 Subject: [PATCH 01/23] added videojs-resolution Added ability to add multiple resolutions with button to select resolution. This adds videojs-resolution.js to the plugin. --- .DS_Store | Bin 0 -> 6148 bytes admin.php | 10 + video-js.php | 74 +++++- videojs/button-styles.css | 31 +++ videojs/video-js.css | 21 +- videojs/video-quality-selector.js | 387 ++++++++++++++++++++++++++++++ 6 files changed, 501 insertions(+), 22 deletions(-) create mode 100644 .DS_Store create mode 100755 videojs/button-styles.css create mode 100755 videojs/video-quality-selector.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b7b38bd5c2f2bdb1d94dc3b2f3c85a22521473ba GIT binary patch literal 6148 zcmeHK%Tv=p82>(~Qa~lVQIv^t(SZY~pdK7c!3R1H)iOK;tVz3+)im2lQVKHF9vu(< z0UmuH`~y5Y@JHyyyW_!=}G$P>}CM~l+&7r8PG!B$q@n=^0+ZjT_FKf5$V-RC>bjmMZ$B5Z0z{m{Xh; zQ(D`ousws@%-NPYKh5ooD8@y>&baf*puK6H@s?6rd%|JMl$tPXHpN|Ta*KPb(Rz8M zw9d7)bEo_D!J*+&zrJ?i%=vzOXn3Gh($=HV?o&e}H)r#ur!QWM1SmEhqTGb>g__e~y7fsv7%YlQ3nzZJY*0%PJ&aT5px{n^~>CsbKt7)Vk zWF0x8QDF%uZWY)JPYXNAAF>&bt$0#NX)U|rZnMnYEz{&`o?_az41QZz9medrtiv-C zi;FJvW>Q*{n|6d{C4|d8F6`ND-r0GVrfl3}xp8|@P>+($iM$MRrar8mZ&Hvl~QqOuoV@!0D7yG zsD_)yN!!S=`ug734HXuJJ{W=Pkc1^D!ZUad@8JV%z-RadKQMwFcmn(IEMCMbcnxpi z9ZaKzITW~pC47XBgLY~v@4@j(_W|M)!d>sda?-IaVQ0xx$=aE;cka~lM;rIfBZp5K zG7vKGml%-igHA"; } +/* Plugins for videojs */ + +function resolution_output() { + $options = get_option('videojs_options'); + if($options['resolution_output']) { $checked = ' checked="checked" '; } else { $checked = ''; } + echo ""; +} /* Set Defaults */ diff --git a/video-js.php b/video-js.php index 6bb2ca6..0b92c4f 100755 --- a/video-js.php +++ b/video-js.php @@ -4,7 +4,7 @@ * @version 4.5.0 */ /* -Plugin Name: Video.js - HTML5 Video Player for WordPress +Plugin Name: Video.js+ - HTML5 Video Player for WordPress + Plugins Plugin URI: http://videojs.com/ Description: Self-hosted responsive HTML5 video for WordPress, built on the widely used Video.js HTML5 video player library. Allows you to embed video in your post or page using HTML5 with Flash fallback support for non-HTML5 browsers. Author: Dustin Lammiman, Steve Heffernan @@ -35,12 +35,19 @@ function register_videojs(){ wp_register_style( 'videojs', '//vjs.zencdn.net/4.5/video-js.css' ); wp_enqueue_style( 'videojs' ); } else { //use the self hosted version - wp_register_script( 'videojs', plugins_url( 'videojs/video.js' , __FILE__ ) ); + wp_register_script( 'videojs', plugins_url( 'videojs/video.dev.js' , __FILE__ ) ); wp_register_style( 'videojs', plugins_url( 'videojs/video-js.css' , __FILE__ ) ); wp_enqueue_style( 'videojs' ); } wp_register_script( 'videojs-youtube', plugins_url( 'videojs/vjs.youtube.js' , __FILE__ ) ); + if($options['videojs_resolution'] == 'on') { //add needed files for selecting video resolution + wp_register_script( 'videojs-resolution', plugins_url( 'videojs/video-quality-selector.js' , __FILE__ ) ); + wp_register_style( 'videojs-resolution', plugins_url( 'videojs/button-styles.css' , __FILE__ ) ); + wp_enqueue_style( 'videojs' ); + }else{ + wp_register_script( 'videojs-resolution', ''); + } } add_action( 'wp_enqueue_scripts', 'register_videojs' ); @@ -49,6 +56,7 @@ function register_videojs(){ function add_videojs_header(){ wp_enqueue_script( 'videojs' ); wp_enqueue_script( 'videojs-youtube' ); + wp_enqueue_script( 'videojs-resolution' ); } @@ -100,6 +108,7 @@ function video_shortcode($atts, $content=null){ extract(shortcode_atts(array( 'mp4' => '', + 'mp42' => '', 'webm' => '', 'ogg' => '', 'youtube' => '', @@ -112,7 +121,10 @@ function video_shortcode($atts, $content=null){ 'controls' => '', 'id' => '', 'class' => '', - 'muted' => '' + 'muted' => '', + 'data-res' => '', + 'data-res2' => '', + 'data-res3' => '' ), $atts)); $dataSetup = array(); @@ -123,9 +135,19 @@ function video_shortcode($atts, $content=null){ // MP4 Source Supplied if ($mp4) - $mp4_source = ''; + $mp4_source = ''; + if ($mp42) + $mp4_source2 = ''; + else + $mp4_source2 = ''; + if ($mp43) + $mp4_source3 = ''; + else + $mp4_source3 = ''; else $mp4_source = ''; + $mp4_source2 = ''; + $mp4_source3 = ''; // WebM Source Supplied if ($webm) @@ -200,6 +222,8 @@ function video_shortcode($atts, $content=null){ @@ -223,6 +247,48 @@ function video_shortcode($atts, $content=null){ +_end_; + } + + if($options['videojs_resolution'] == 'on') { //add the resolution button + + $videojs = <<<_end_ + + + + + _end_; } diff --git a/videojs/button-styles.css b/videojs/button-styles.css new file mode 100755 index 0000000..e70a197 --- /dev/null +++ b/videojs/button-styles.css @@ -0,0 +1,31 @@ +@charset "utf-8"; + +/* +You are free to style the button however you wish. I plan to use +an icon from my site's own icon font to make it more visible. These +are just basic styles to make it look ok with plain text. +*/ + +/* Position the button */ +.vjs-res-button { + float: right; + line-height: 3em; +} + +/* Don't show hover effects on title */ +ul li.vjs-menu-title.vjs-res-menu-title:hover { + cursor: default; + background-color: transparent; + color: #CCC; + + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +/* Needed to keep text visible in video.js 4.9 */ +.vjs-res-button .vjs-control-text { + width: auto; + height: auto; + clip: auto; +} \ No newline at end of file diff --git a/videojs/video-js.css b/videojs/video-js.css index b81de37..7838ae1 100644 --- a/videojs/video-js.css +++ b/videojs/video-js.css @@ -1,6 +1,7 @@ +/* Generated by less 2.4.0 */ /*! Video.js Default Styles (http://videojs.com) -Version 4.5.1 +Version GENERATED_AT_BUILD Create your own skin at http://designer.videojs.com */ /* SKIN @@ -93,7 +94,6 @@ The default control bar that is a container for most of the controls. .vjs-default-skin.vjs-has-started .vjs-control-bar { display: block; /* Visibility needed to make sure things hide in older browsers too. */ - visibility: visible; opacity: 1; /* transition */ @@ -162,7 +162,6 @@ fonts to show/hide properly. /* outline: 0; */ /* keyboard-only users cannot see the focus on several of the UI elements when this is set to 0 */ - } /* Hide control text visually, but have it available for screenreaders */ .vjs-default-skin .vjs-control-text { @@ -272,7 +271,6 @@ fonts to show/hide properly. font-size: .9em; /* Even though we're not changing the top/height, we need to include them in the transition so they're handled correctly. */ - /* transition */ -webkit-transition: all 0.2s; -moz-transition: all 0.2s; @@ -304,7 +302,6 @@ fonts to show/hide properly. Also using a paralax effect to make the lines move backwards. The -50% left position makes that happen. */ - background: #66a8cc url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAP0lEQVQIHWWMAQoAIAgDR/QJ/Ub//04+w7ZICBwcOg5FZi5iBB82AGzixEglJrd4TVK5XUJpskSTEvpdFzX9AB2pGziSQcvAAAAAAElFTkSuQmCC) -50% 0 repeat; } .vjs-default-skin .vjs-load-progress { @@ -400,7 +397,7 @@ easily in the skin designer. http://designer.videojs.com/ margin-left: -2.1em; /* Center it vertically */ top: 50%; - margin-top: -1.4000000000000001em; + margin-top: -1.4em; } /* Hide if controls are disabled */ .vjs-default-skin.vjs-controls-disabled .vjs-big-play-button { @@ -437,7 +434,6 @@ easily in the skin designer. http://designer.videojs.com/ font-family: VideoJS; /* In order to center the play icon vertically we need to set the line height to the same as the button height */ - line-height: 2.6em; text-shadow: 0.05em 0.05em 0.1em #000; text-align: center /* Needed for IE8 */; @@ -523,7 +519,6 @@ easily in the skin designer. http://designer.videojs.com/ bottom: 0; left: 0em; /* (Width of vjs-menu - width of button) / 2 */ - width: 0em; height: 0em; margin-bottom: 3em; @@ -531,10 +526,8 @@ easily in the skin designer. http://designer.videojs.com/ border-right: 2em solid transparent; border-top: 1.55em solid #000000; /* Same width top as ul bottom */ - border-top-color: rgba(7, 40, 50, 0.5); /* Same as ul background */ - } /* Button Pop-up Menu */ .vjs-default-skin .vjs-menu-button .vjs-menu .vjs-menu-content { @@ -545,12 +538,10 @@ easily in the skin designer. http://designer.videojs.com/ width: 10em; bottom: 1.5em; /* Same bottom as vjs-menu border-top */ - max-height: 15em; overflow: auto; left: -5em; /* Width of menu - width of button / 2 */ - /* background-color-with-alpha */ background-color: #07141e; background-color: rgba(7, 20, 30, 0.7); @@ -632,25 +623,19 @@ control positioning and full window mode. ** padding: 0; /* Start with 10px for base font size so other dimensions can be em based and easily calculable. */ - font-size: 10px; /* Allow poster to be vertially aligned. */ - vertical-align: middle; /* display: table-cell; */ /*This works in Safari but not Firefox.*/ - /* Provide some basic defaults for fonts */ - font-weight: normal; font-style: normal; /* Avoiding helvetica: issue #376 */ - font-family: Arial, sans-serif; /* Turn off user selection (text highlighting) by default. The majority of player components will not be text blocks. Text areas will need to turn user selection back on. */ - /* user-select */ -webkit-user-select: none; -moz-user-select: none; diff --git a/videojs/video-quality-selector.js b/videojs/video-quality-selector.js new file mode 100755 index 0000000..0a33c2a --- /dev/null +++ b/videojs/video-quality-selector.js @@ -0,0 +1,387 @@ +/** + * Video.js Resolution Selector + * + * This plugin for Video.js adds a resolution selector option + * to the toolbar. Usage: + * + * + */ + +(function( _V_ ) { + + /*********************************************************************************** + * Define some helper functions + ***********************************************************************************/ + var methods = { + + /** + * In a future version, this can be made more intelligent, + * but for now, we'll just add a "p" at the end if we are passed + * numbers. + * + * @param (string) res The resolution to make a label for + * + * @returns (string) The label text string + */ + res_label : function( res ) { + + return ( /^\d+$/.test( res ) ) ? res + 'p' : res; + } + }; + + /*********************************************************************************** + * Setup our resolution menu items + ***********************************************************************************/ + _V_.ResolutionMenuItem = _V_.MenuItem.extend({ + + // Call variable to prevent the resolution change from being called twice + call_count : 0, + + /** @constructor */ + init : function( player, options ){ + + var touchstart = false; + + // Modify options for parent MenuItem class's init. + options.label = methods.res_label( options.res ); + options.selected = ( options.res.toString() === player.getCurrentRes().toString() ); + + // Call the parent constructor + _V_.MenuItem.call( this, player, options ); + + // Store the resolution as a property + this.resolution = options.res; + + // Register our click and tap handlers + this.on( ['click', 'tap'], this.onClick ); + + // Toggle the selected class whenever the resolution changes + player.on( 'changeRes', _V_.bind( this, function() { + + if ( this.resolution == player.getCurrentRes() ) { + + this.selected( true ); + + } else { + + this.selected( false ); + } + + // Reset the call count + this.call_count = 0; + })); + } + }); + + // Handle clicks on the menu items + _V_.ResolutionMenuItem.prototype.onClick = function() { + + // Check if this has already been called + if ( this.call_count > 0 ) { return; } + + // Call the player.changeRes method + this.player().changeRes( this.resolution ); + + // Increment the call counter + this.call_count++; + }; + + /*********************************************************************************** + * Setup our resolution menu title item + ***********************************************************************************/ + _V_.ResolutionTitleMenuItem = _V_.MenuItem.extend({ + + init : function( player, options ) { + + // Call the parent constructor + _V_.MenuItem.call( this, player, options ); + + // No click handler for the menu title + this.off( 'click' ); + } + }); + + /*********************************************************************************** + * Define our resolution selector button + ***********************************************************************************/ + _V_.ResolutionSelector = _V_.MenuButton.extend({ + + /** @constructor */ + init : function( player, options ) { + + // Add our list of available resolutions to the player object + player.availableRes = options.available_res; + + // Call the parent constructor + _V_.MenuButton.call( this, player, options ); + + // Set the button text based on the option provided + this.el().firstChild.firstChild.innerHTML = options.buttonText; + } + }); + + // Set class for resolution selector button + _V_.ResolutionSelector.prototype.className = 'vjs-res-button'; + + // Create a menu item for each available resolution + _V_.ResolutionSelector.prototype.createItems = function() { + + var player = this.player(), + items = [], + current_res; + + // Add the menu title item + items.push( new _V_.ResolutionTitleMenuItem( player, { + + el : _V_.Component.prototype.createEl( 'li', { + + className : 'vjs-menu-title vjs-res-menu-title', + innerHTML : player.localize( 'Quality' ) + }) + })); + + // Add an item for each available resolution + for ( current_res in player.availableRes ) { + + // Don't add an item for the length attribute + if ( 'length' == current_res ) { continue; } + + items.push( new _V_.ResolutionMenuItem( player, { + res : current_res + })); + } + + // Sort the available resolutions in descending order + items.sort(function( a, b ) { + + if ( typeof a.resolution == 'undefined' ) { + + return -1; + + } else { + + return parseInt( b.resolution ) - parseInt( a.resolution ); + } + }); + + return items; + }; + + /*********************************************************************************** + * Register the plugin with videojs, main plugin function + ***********************************************************************************/ + _V_.plugin( 'resolutionSelector', function( options ) { + + // Only enable the plugin on HTML5 videos + if ( ! this.el().firstChild.canPlayType ) { return; } + + /******************************************************************* + * Setup variables, parse settings + *******************************************************************/ + var player = this, + sources = player.options().sources, + i = sources.length, + j, + found_type, + + // Override default options with those provided + settings = _V_.util.mergeOptions({ + + default_res : '', // (string) The resolution that should be selected by default ( '480' or '480,1080,240' ) + 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 + + }, options || {} ), + + available_res = { length : 0 }, + current_res, + resolutionSelector, + + // Split default resolutions if set and valid, otherwise default to an empty array + default_resolutions = ( settings.default_res && typeof settings.default_res == 'string' ) ? settings.default_res.split( ',' ) : []; + + // Get all of the available resoloutions + while ( i > 0 ) { + + i--; + + // Skip sources that don't have data-res attributes + if ( ! sources[i]['data-res'] ) { continue; } + + current_res = sources[i]['data-res']; + + if ( typeof available_res[current_res] !== 'object' ) { + + available_res[current_res] = []; + available_res.length++; + } + + available_res[current_res].push( sources[i] ); + } + + // Check for forced types + if ( settings.force_types ) { + + // Loop through all available resoultions + for ( current_res in available_res ) { + + // Don't count the length property as a resolution + if ( 'length' == current_res ) { continue; } + + i = settings.force_types.length; + found_types = 0; + + // Loop through all required types + while ( i > 0 ) { + + i--; + + j = available_res[current_res].length; + + // Loop through all available sources in current resolution + while ( j > 0 ) { + + j--; + + // Check if the current source matches the current type we're checking + if ( settings.force_types[i] === available_res[current_res][j].type ) { + + found_types++; + break; + } + } + } + + // If we didn't find sources for all of the required types in the current res, remove it + if ( found_types < settings.force_types.length ) { + + delete available_res[current_res]; + available_res.length--; + } + } + } + + // Make sure we have at least 2 available resolutions before we add the button + if ( available_res.length < 2 ) { return; } + + // Loop through the choosen default resolutions if there were any + for ( i = 0; i < default_resolutions.length; i++ ) { + + // Set the video to start out with the first available default res + if ( available_res[default_resolutions[i]] ) { + + player.src( available_res[default_resolutions[i]] ); + player.currentRes = default_resolutions[i]; + break; + } + } + + /******************************************************************* + * Add methods to player object + *******************************************************************/ + + // Make sure we have player.localize() if it's not defined by Video.js + if ( typeof player.localize !== 'function' ) { + + player.localize = function( string ) { + + return string; + }; + } + + // Helper function to get the current resolution + player.getCurrentRes = function() { + + if ( typeof player.currentRes !== 'undefined' ) { + + return player.currentRes; + + } else { + + try { + + return res = player.options().sources[0]['data-res']; + + } catch(e) { + + return ''; + } + } + }; + + // Define the change res method + player.changeRes = function( target_resolution ) { + + var video_el = player.el().firstChild, + is_paused = player.paused(), + current_time = player.currentTime(), + button_nodes, + button_node_count; + + // Do nothing if we aren't changing resolutions or if the resolution isn't defined + if ( player.getCurrentRes() == target_resolution + || ! player.availableRes + || ! player.availableRes[target_resolution] ) { return; } + + // Make sure the loadedmetadata event will fire + if ( 'none' == video_el.preload ) { video_el.preload = 'metadata'; } + + // Change the source and make sure we don't start the video over + player.src( player.availableRes[target_resolution] ).one( 'loadedmetadata', function() { + + player.currentTime( current_time ); + + // If the video was paused, don't show the poster image again + player.addClass( 'vjs-has-started' ); + + if ( ! is_paused ) { player.play(); } + }); + + // Save the newly selected resolution in our player options property + player.currentRes = target_resolution; + + // Make sure the button has been added to the control bar + if ( player.controlBar.resolutionSelector ) { + + button_nodes = player.controlBar.resolutionSelector.el().firstChild.children; + button_node_count = button_nodes.length; + + // Update the button text + while ( button_node_count > 0 ) { + + button_node_count--; + + if ( 'vjs-control-text' == button_nodes[button_node_count].className ) { + + button_nodes[button_node_count].innerHTML = methods.res_label( target_resolution ); + break; + } + } + } + + // Update the classes to reflect the currently selected resolution + player.trigger( 'changeRes' ); + }; + + /******************************************************************* + * Add the resolution selector button + *******************************************************************/ + + // Get the starting resolution + current_res = player.getCurrentRes(); + + if ( current_res ) { current_res = methods.res_label( current_res ); } + + // Add the resolution selector button + resolutionSelector = new _V_.ResolutionSelector( player, { + buttonText : player.localize( current_res || 'Quality' ), + available_res : available_res + }); + + // Add the button to the control bar object and the DOM + player.controlBar.resolutionSelector = player.controlBar.addChild( resolutionSelector ); + }); + +})( videojs ); \ No newline at end of file From edb980e37d24ad75e99e27c268be10cbd176952b Mon Sep 17 00:00:00 2001 From: Jeremy Zimm Date: Mon, 30 Mar 2015 10:52:56 -0700 Subject: [PATCH 02/23] Updated videojs + bug fixes to resolution selector Updated videojs to 4.12.5 adding languages, plus fixed an issue with resolution selector. --- admin.php | 4 +- video-js.php | 6 +- videojs/demo.captions.vtt | 0 videojs/demo.html | 40 +- videojs/font/vjs.eot | Bin 3536 -> 4804 bytes videojs/font/vjs.svg | 83 +- videojs/font/vjs.ttf | Bin 3372 -> 4640 bytes videojs/font/vjs.woff | Bin 4228 -> 2508 bytes videojs/lang/ar.js | 26 + videojs/lang/bg.js | 26 + videojs/lang/ca.js | 26 + videojs/lang/cs.js | 26 + videojs/lang/de.js | 26 + videojs/lang/es.js | 26 + videojs/lang/fr.js | 26 + videojs/lang/hu.js | 26 + videojs/lang/it.js | 26 + videojs/lang/ja.js | 26 + videojs/lang/ko.js | 26 + videojs/lang/nl.js | 26 + videojs/lang/pt-BR.js | 26 + videojs/lang/ru.js | 26 + videojs/lang/tr.js | 26 + videojs/lang/uk.js | 26 + videojs/lang/vi.js | 26 + videojs/lang/zh-CN.js | 27 + videojs/lang/zh-TW.js | 27 + videojs/video-js.css | 343 +- videojs/video-js.less | 378 +- videojs/video-js.min.css | 4 +- videojs/video-js.swf | Bin 16803 -> 17202 bytes videojs/video.dev.js | 7982 ++++++++++++++++++++++------- videojs/video.js | 338 +- videojs/video.novtt.dev.js | 9857 ++++++++++++++++++++++++++++++++++++ videojs/video.novtt.js | 196 + 35 files changed, 17623 insertions(+), 2104 deletions(-) mode change 100644 => 100755 videojs/demo.captions.vtt mode change 100644 => 100755 videojs/demo.html mode change 100644 => 100755 videojs/font/vjs.eot mode change 100644 => 100755 videojs/font/vjs.svg mode change 100644 => 100755 videojs/font/vjs.ttf mode change 100644 => 100755 videojs/font/vjs.woff create mode 100755 videojs/lang/ar.js create mode 100755 videojs/lang/bg.js create mode 100755 videojs/lang/ca.js create mode 100755 videojs/lang/cs.js create mode 100755 videojs/lang/de.js create mode 100755 videojs/lang/es.js create mode 100755 videojs/lang/fr.js create mode 100755 videojs/lang/hu.js create mode 100755 videojs/lang/it.js create mode 100755 videojs/lang/ja.js create mode 100755 videojs/lang/ko.js create mode 100755 videojs/lang/nl.js create mode 100755 videojs/lang/pt-BR.js create mode 100755 videojs/lang/ru.js create mode 100755 videojs/lang/tr.js create mode 100755 videojs/lang/uk.js create mode 100755 videojs/lang/vi.js create mode 100755 videojs/lang/zh-CN.js create mode 100755 videojs/lang/zh-TW.js mode change 100644 => 100755 videojs/video-js.css mode change 100644 => 100755 videojs/video-js.less mode change 100644 => 100755 videojs/video-js.min.css mode change 100644 => 100755 videojs/video-js.swf mode change 100644 => 100755 videojs/video.dev.js mode change 100644 => 100755 videojs/video.js create mode 100755 videojs/video.novtt.dev.js create mode 100755 videojs/video.novtt.js diff --git a/admin.php b/admin.php index 1808d34..adadb05 100644 --- a/admin.php +++ b/admin.php @@ -189,8 +189,8 @@ function reset_output() { function resolution_output() { $options = get_option('videojs_options'); - if($options['resolution_output']) { $checked = ' checked="checked" '; } else { $checked = ''; } - echo ""; + if($options['videojs_resolution']) { $checked = ' checked="checked" '; } else { $checked = ''; } + echo ""; } diff --git a/video-js.php b/video-js.php index 0b92c4f..1cb107b 100755 --- a/video-js.php +++ b/video-js.php @@ -1,7 +1,7 @@ '; if ($mp42) $mp4_source2 = ''; - else - $mp4_source2 = ''; if ($mp43) $mp4_source3 = ''; - else - $mp4_source3 = ''; else $mp4_source = ''; $mp4_source2 = ''; diff --git a/videojs/demo.captions.vtt b/videojs/demo.captions.vtt old mode 100644 new mode 100755 diff --git a/videojs/demo.html b/videojs/demo.html old mode 100644 new mode 100755 index b2b3fa4..ba3b33b --- a/videojs/demo.html +++ b/videojs/demo.html @@ -1,31 +1,31 @@ - - Video.js | HTML5 Video Player - - - - + + Video.js | HTML5 Video Player - - + + + + + + + + + - + - + + \ No newline at end of file diff --git a/videojs/font/vjs.eot b/videojs/font/vjs.eot old mode 100644 new mode 100755 index a075c19f0b5d7d01c0edf7741d1b733c8b123b72..a2c0f5f24111047aa05a51377821e11312b48912 GIT binary patch literal 4804 zcmcH-OKcm*b!KOOT#CzGa`~6|6@U6rq9ls4W24l+XM(|p-oXBZR#9~B5iu$!Mznt4@F?0Xdxg)E=5uM5TJDa-K|+IWh~+ z9JvSrDmg@E$XW7Da)C^d7lGm@6F_;9Tmo5?iIXBJkbYBkb8jHxfKE~+jP!r)aNmG> zlXU^*H}FhO&rZ!heCKB{3~dMeD6)OYN{-1JnGj{vA$hN>_-b!DE!$j5-c z1$g}8)a=a8;4eM|{4R{KWq$6`Wv~+pEGqB`;|aY%1VXqsxZeTgRjZ|cBZJg|B29&Y z*{!8QU?^5wgCDCAz}6QZ<>FYG8;2trFfOi?L3r{sv?`H-Cju?LqOQa~CTpu}Ye0v7 zD`X|w!0FmYaX$7**w^T73N$K8DD-ffS}hVdNqGwn^29Xgu~y?@4q%B3y6IUdTh1~m zkkA8Vo-EfUYs<^E<@)3@%n)n9SK*;RwZ?&YGx*^=y$)LAs;gC2saNY&pj3ee7}zcW zH=!peJXKn$*JurER#^q!YQtVPt+55uH%4ic`gB93nLe7*2g~$3itdsmm##cmUVgw9 z6iL+7Pw%RlC@BwU^?|j@Zf-a9Pmv))(#ZmsEzqIi@+ck3*wBDK5T%AFsVv1d55c7c zhb;3FlxQp##a8loJW@|@Zx1ia4&k+C6aK_8k(vDH{)79YCWJkD7A`OJ0E^?k_gNd! z3*2lTY|N2;fykqLfL&c*mc(Gv<4F!C%;iHr$ZgO2{rT-V(-+?UUoUZHeen|e4j(TR zjvwBaSa+)2iJ1!{BNt{)(tyNmMySU()3zzTq(+6x9~s8%I3e6w)j3H$_);Ta^)1bH+XkxXcX`$ z7Z^-@kpK}+Kl~y8XWnUO=*5CI{_>j>4%==4RsdcVcnL~+EI?x{{4^_>vZSzGbKZiM3HxD3ePK=lNUuvFiOu%?;Ja` zr{qH(Yts~gQbEz$7;=53J!i)HN{6?^a(iF+>evf=bFr;QO5S8h4dsVBa!=)rwm{PN zMSL@MZp2wL%2qL^Vr+#BOGXGpci1z7__!2s~rBaXw))!MmBj zHWP#qRFuKN9EL(m}DVmz9klf$W%!)eXT z*=)s1;1HRermEmTuAvS?b%g~Ae~irEWAE3@%q9JN;#Sz>`=IfE7esDV{+x&pFVW5 zh)i(Hb-U3wr(taL!se~ln~X6>r&i2>P=jbW08XS*0~3Q8Qp{vuZe)oKOKpowr*`FX zyH1ttmKHZsn4KQSFlh$M+bhXrWqUb@*qF^@HJqQV4cj%+tPl0Beqc*)=pHlkW*{AM zxkBlHiHvPQB#DLn);v6ek;O>O;H%FofN6dfTLCiO*6udNhw` z`DuLZKHN3mC6oAeu#q-kJEe`xp|Sw@KqGrZSs8PMnL)7L%8G!O`nPeVBBmMhZ(Wiw z0l~kFj$z9OFtJ1%sYn_W{h~hwDD>iq2(elTwkJG#wyP)L@9!@LgT=mr-`}0>)b))l#u&n;P(IvfvbX zQzdAXHN(Z=Vj|I6XrS2d5A@{Ix+fa(I~*FH#K7TIL zscG#YpX_Ker=dC={&0t`r7~HcD-@2nFbd%yZyT9Z!tIU(yfT;v8o@k=FBoyV6REV< zo6F~6D!Gi;lS;%?SIFm)WW}6Cn=E?+Aythgk{&&s&Uir?=kn`L>I#QKZLP5>O13w= zdlU{U1{v!fV0yNWn{3|)8tDl696A5qD~A|2w(#9?3>p}eF(~3Wd^CsE4lMoh+=<)Y z+PUlMk51V^6wm9Yqoe@DnHU{JR|}N0 zqwFhGXu8dqA(SV#I6)BE#L}YZa;Z|V7L-(%OI%!gdu{vCXB5p;|~( zU2WpkrK51-fL$k>GYy?IW!6=&n>Sh02(}7o5mg8RyLHQCA6;6M%n=}?w5CzqBZxm( zKD_qojjy}6eL|dgDJSHEH-7Tljka;~!y5(U8?!(^eD@`AEkhjYh9;AXm46)q0K1`P zcxv!e8!+O8uUK@{)ZmS@@U4Dvn+7N$3fyU;aHrWvzCo^$Kho1&CpW|hQ+dM2`IG#L za6~*Hz9q#$7A^CWzk>Bx>=~Fyqf*Jw8}?IpH@8=X19*3X&H(Ogz&ya?4OjrpY6BMG zO?RLHEAXB-4Ox!z4xpe;3$}Dxu%*+2Eu9u@>9k-G`fh3TwRE^r=7&-6@G7*4%9TWN`70-_Maz<`Mv22Hu8^ilEh&~IEBY~H z8MR%YQqbg(z$tUP(b^h&N&O#yL37;An9iw6L5VhQc9DVxPbT<9_-98L@4)WAX%QF}M_}X8#A%Bi~ zWq#(;#d;l{sO2vD8|Ig;o*(?N5hIe{A@bZ^n439Q`|L;mMBgrIaRCa~9|(P{LGE5y zUcci11(zZ3KptLNeQ8F0!)Idr66F5nnJX9REIot%LCDdSndP~YpTGNaqNYvgUcb0{ zX?-P>{Rif6LI19`HmvVX=PPfX^?v&cYPR$6(W^HwCy`C-yV4Gu5F4B{!4>|^`YxrU zn?yB|b}Ua~$X;o&Gyyt|QKAUPZ0X8EL}96g-y%0P@RNKV@+pUYL5Jwo{a6sjV!QWB z<^1S5Dw}jq#9&eiv2|}+F$oKM1z_G6k|mdg?m>-O{IrN1x-x}@6^cE@w8djXK}8Ka zD_etvRYP_GKG;xeH{ifj&B#4MR#8kcyY~3VzkT)yd;LTj`7zcFv?v(c2oKHF0%WP= zrFMi56p0B$;k{sO(QfxT_v`*&d6nYy7%k8T?BO84!SC>wk|qsGE7IR&pFA#4$?IaX z>ZSw?h%~lwOac(utXW$kA+J%c@;0)j5czX$2~o@2+vfH*Z`U??v$hFKdrwWSA{XQ@ z1_N!HC(VgM?!!h^ZM<5k@NXL3YHe0^H(ss6UgU+i0(&oY;y}jre5sgE>iJ?e6x0-X zp9c>6Ri&#-QT@um!1Io~Wv^d}OhpvGx1s+h{XDYYxj-B7uPxsZ!X0wZyZ)%z)-+Wy z3`NzHp`jPQX67<_jO&I1v+}L7{)jkSA`U6$#F3`Qzd4deM)oI6IWi(5o1A>%8=~~X zYNR?MqMW=tssGP0Gfme`-Ub%1i!66C(>4-q)~Rmg7;AEFF_|J%q^E$GSX+!!oYa$g zBCdE8m6a|I1@Tub6-&ptC&h`lC!hs$*+#Q1kl_~m9zlC);oKYX_E_|qp&p8ifDJyGch{Fh+CXCLm#j}?{$ z&;{Vd?H#Vy*~A_xKyL{9kfbz~Xo#p?3ucRXJsvzjJ&wFbbw`#C=&O2O*YmSI1zj)X z{D=Mi!xL6}V_%yH_lCp0wwGXbzdZ(ne_8(G|t}_{N;R|Fr_l?-U2sCkfU%G`) zoK2T#N+DaBECqQHes6Ep{=7Ibu{gocZQR-ziW{@nXN~v}n_|-2L3B(k{%m98Ryl55 zpPjvK#LIAuwfgE`;%@s4_uvQ-uqbdUFzQh}AseT_D1cflWdYDaUUVoPEJqkRU5=rm z0mUH?-1YW!r&3wt`FO0ojaiS!`})$UGm)sTNoH>K=h`FDqrH87r;i-*dYNNgNA#ZF z-to@Es>;4rx0!n(k!WvcP9_oq1DVWJEZPP)J+au)L!JIs=}d2=%j;u~MG`d29wMk7bPcrt!_e*F~(--^$Zmu2p%v zTD9`IhaC&MY-3lpu`7`U*NTN*Hf`|Mb}UgW1h0fGu^JS{+4VWR1ishdW$LDD4)22g zro+4OUHjPK#k=EQKry=-p;6(@<|XhQ4lmO<9>TWX1^v4Y@5ZC~R}Sxi{?5T?_SA)Q zbE{9_y?x-ZJvldjd1(f(?lJ4By>8JpJjdrKN?GgK9mSVEdcdEpGjLN6;BlTon?-9- z4vtn=*2h-Y=I5f>R3`dZ)R`HaQ5-Nb1KErLs{`-&2jM;B - -This is a custom SVG font generated by IcoMoon. - - +Generated by IcoMoon - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/videojs/font/vjs.ttf b/videojs/font/vjs.ttf old mode 100644 new mode 100755 index eb2463791823ded2c3f9b1321e9fa3a331e8a5eb..682a9b21ef23161d9f1ce0087c4385a8b31b3e26 GIT binary patch literal 4640 zcmcH+TWnlMb>_}}?cLqG_wL?(+xzss{8)Q!uh&TvcY~v(X&$6W>_mBCB`MHK6|tZQsZi1ngn;s~ALS2GK2Q}2fg%<42O*M=N+{H8&bg0x?KWwP zV0Q1!dCZ(SGjrygGlUXC+@wJm8Tjhq{z3Hy>juhi;hUP7n_hVE_RlU7LOTFId3O5J z0<;?7SD=;8URZkepZ~b@HsB3H)cU#E=`-y1t02+W1pdf5Ah=RO74Uxn-gR#7@|C?m z;O>A3ly~gH{LFNMj{~S(hN?U_ePw|p$wz>{33%$_^xW*u$S*zs{5_as%fkGn%b+I| zKUOKictT$x0wLV%-0y+%venYRlOgKDE^UQ^+8axqz*MZc20zv$fDzmar=(1PgBoWv zphm7ic=8mqDv=3CM4+Yas&|tgk+s#eHK2o5cgfv^jnlOcQ+)E{SkNAAcCaf-D2(v6 z%?WeWwwNlc>+yIRaMzkVtN}Ffg8F(vD%1*03a9mOjVCM3spiT`bEP%40xQHC@KyLI zP*Dv^VBHLUuoe<<(I(eeZLoT)(P{vt0X)D!cL}&@Jx$?j(0Z#$n^?2@D)3fKIdH_Z z$rer97^4Xq)D4y9`)O7ms?qN#x>u6Cx^jPI8EvI zKi2&B172*=oik5Y4M7q`-mNJ-uV`*w6eYo^K0UK@{LG$e5P8g_DFUT}qIno{gVjA} z#`~*>wupa07E^LvZQtw*YXOjM1QMmmd6lnhTe6Z|~B={wi`tm$PN=u^?RLWCuw z5PraSL);uEoBWvkfqV$A+dc@qV|Z{uVKX7HnZPy^gi%&;XaK_B5e8uHaZL7D zrX$mlMjJ-99FLc?hSO5b$n?i!{h44e(;tiXXN-3gJ>vC7bj4{G6g{kZ!@Aq8hrMc8 zS6cTRmeaQGIXtItN{eL!MmFa2#j-{q8`F-t4MYsL)3SQI9;nX(PT#Ih>t4GaI2lmd z6v4?BHg{Rr+=ENC>w$W;?zJm|3c(TM)%=VYPOTVDn`X>rcdZ~!8+H(2NZ-Y)iO9^xTzd@?%nGL4_<%wImIvxrF8t%)IhA94N}^> zs}e^#YwTEjZF2Ip#T}G#v^3PIaB*+8yAs3T%6B|<=wt<%W>#eDjkY-hbE6fuAHCkD zj5#~Cd5e4R_2{cn;Z)*eW? zVOWW^?Kx{{4=-s4rbfahPbomEWNg?uHl?RJWA4^76w7UQtTUyjxAsP(y<5}RQrdcE z1JUeGZ&T{@z?Krp6L8-_cf}aGusiO6M{4i!Y3llj_W3M3O|9L7T=N|=g|CD4umQ&@ ztw#=(g~0~w=#gMGj1^`C!FnsJ0A3x~##JjAX3W2pBw+x8e+>=8=^w_xlCVROG$e+^ zP!>=a#S;;HwG!z_`}IP1Z#XnCP>Dn;{pC=or_iPAow1PY^2$6F6`xB9$C8@fRVamm zeSLl5a9?jvFj&ZCd|F4ukOf&3INWBu(a*}EXou#@cI6GD)D7LbyNiK9E|c`Bu~0yg z-4de$2$qdVO!Xx**?_;0F9w47LRUb`CKDbt9MWXLE%K&FkSc42mqB78(Pnh8G7t*) zmU6m35f8asUKi-+2K^#&Nbd5*o=~V|elscYGM zA?S_9;$HMZILTW^KAZOW;^BY{>VZU1&lQZseZF)y7YGzfC0I%^AMj_>N!1$-`XyO0 zSK*Q6Ksc(ZiFC%V=W_V~2;;mV-A%o*XwmC(vo2Q%0wV_wdmi);zHM%X24geQW2gt3NvBI8nT>pO8m!Hu%vx znE+j@Xi&9+RA%rBK}#!0U^zY-M2nKb;AfI_2u&?qD~z!xsnC|qm@bqjH#tEN+2rz) z==G{nq#2P^uUA}Jo@AmRa5sSsm+I*9Rng;B#b`4ss$P$Hb@?dVIAFKP=14=Q%svlA zup2j6+YF8fX$eIL0=s$BWFK8#lFS((qHGMKct+rVuzYas+4x zck8X4`C&$ZZ!Ih^4&S~6(lSJ$wk08EmXHHri^EVdd`lPg)*}TyWd~00F zp#e&W0y#|ra+-bQ>*N~w6Ftp!al?!-l_z|PKgr(}j)(`uH>DKxMa@FwZ=gLEX9Xr= zS1S3rtv`jgxw9)Az`Jca1GvkEd4MNuSOCeY4U6#79kgKu-gz^SOH7&-6@G7*fn;iIyc-9T2d@aR`g@Y zGHSa(rJ~6pfm;+g2my>Dyd*%202X}dA5gI9p$G~jZDAlmqZWH~(ThDih#6;9iJ0w$ia`Noc7v6k#0SX_qq0{3NlQc#iqPBaGBhNgS z%A|g~KS0FKL!NzkZT`~lU;A?f@~3#NF3n%QRIkGmwcW*d)6(j-3&Y?td=}=fLLOY3zj}$z z(X$vIh8$U+Ut2u&>DxagYT1GA^-CLZG?v=< zC*-Ck{sv!weA=O((Ghx8OmHJl2Ti2=pp`F7oTrjS_XsgqltgU3J9bRM;vhu&xsYtR zByjniKNvi$`+wzCiqR9a zOz*LW1N;WR#h**MG%T%4f0cdmlsqeMiDcC+2^bJ%?BJ*bAhK1nQzD_R5w3CtRa1zx zY)4!?rAo!BRJc;x;howJEE_qsxQbekRtWezbWe)o`Rx16s@i<5T;^XjyVcr)>TbSP zg}tZ?aT)er>c)YL8M$I1moRdLOfaA;@*xi#(p06VM^QCpXy^sU-I`ZZ!n0vT^EM5B ze~`xyJGar!zqEaa33tfD-i^ntwyCR%X)3C&jE=ta1uK^@V%{(nn3ZmqjK{>`5_^=Q zP8=zE^2;N6Y-E4HlH=ndvYD9|za&ZxR^!!i5#`L?8RLJBnQ5+WaRpdF7Fq6QrV0ve z)v0dR7<+PVF`2?tpr?VDXh)QjoG=nbJf?UQm6aY22JkBs3&j!cOL9Es@#}$XrcjJ~ z;<0=#n+XJ?U#ymH-G5fv;1;js4Sn#JkXQCLGdFuB-_biCcKg`dQu~{0IFlL~DGUy~ zC)|<2;~l!hS=R|OH9A@tIN_djIXdh9eRiE5)1_9wzeUzhPJR8PCb#O6q_@i2*tz_Z z&zwAU=4<)Xbh*p_UxE$IL%1gmJ1hyH3&4xpJ5;Z;g&ZkBZxDG%P>PB)O4O+bGKHKG z3p_+Uj(k83N0uJaSB;!uXx}wB6r4)W<{pp-}&nzsK+I;a>}T<2(62 zO!dXXm)G~o;=SMi7OC-_l-r3z<(RJfnCqlhEnC0sA_pZxEx1_cY z9#NE@vuSbR3uHO>jmTdZnph(*y}~EXrb~3iU_+TG26zR2@9ft8xH3JxGR@C#-`*aL znF}`-%-ASfV$nMR3{0>5czgSHDQ4bSSh!)vN^p$5`s$zKZu=Pb;5ZSmC~zt;>QOww z22O!d0JTue0HFDt7*IUejxcnF979D1ih~}w>+kDLCNt&>v1n%pvk{9845X50!x3ML z%-pVJJHwIV{R0DMjve!QnWH_&jK2QGW(g(g8Pp z(dh9b-CDbJwm;nC^)bi7;e_#(vMPse80oX)JxYL-Ua_MyXz?~Z>0#q4T^MuoQqFM;oJc$uas z?eH$>-*$L61?U$J?}7dkd;?vi^R!4C^b|dZPu+{>7dM^)GXvccU7=N)r%i~9OIKFs z!A!z(owjVc35!LFP==CNErK_Fk%k4jpsoDTj_Xs5#-#Nk{L(9wlQ?3ht^9liK2XzX8D O(EG1%n1UYuukdfbdl*On diff --git a/videojs/font/vjs.woff b/videojs/font/vjs.woff old mode 100644 new mode 100755 index c3f0f1d80718ac2a16e2daeaf82ce59ed76a3f06..a79d0fa098a95982d7ec20a8afe3c6475d57c795 GIT binary patch literal 2508 zcmZvec{r5o8^>QWW^BijeGo%P)~RgCc9gLe*+ODuons$NW1GQPQ%Q#!OO`{jhH2q& zFl6aagyIMpTbe9`>|uVd^ZWhzyRYl}+}HQ>KG%Ie&-=dD^*pz2&}bV62O9w3dI#_T zVDS1MkN+<&q0vYHVB-V;csc-Zs=8ri9MNcpD*(We4bGQ3R#-@4TOimo$OmNKK>rEsQMP`{ zOCdBQJObP=4P?K7LIO|#AQbpGlB{oxiL{9wtktqL283DY^mk1NR-s*Gb*B`_r2%iC!*1q}UR>oDuTc<&|~614QpU*;%)47f(n0e~pXuT?6m zbIMEP94t`efdWk)lOMmxe&9yaU=I|lzeLM`;Mgg!dE8sK_mi!4_;w;@6XJ1bj*16 zVxs6J10=Cbssad=Xvt~ z@bVmG-=ohe#_Fzc8sMt7HM_#B47hCyLUg6zb^RgtxIost0t!1HZnl z{mi%?yec_O>J}Vm&n96P$Dg!4X`4Q3D1KeMy_3o(oCP&q7~N0YPn>Hxm>f8KK0O=~ z#a!UG>akK(mgCLZX9$fD&#kma25$_{KMi;^Rs_)I0_B2$@Tz^y77Kd!A3n3MMXnbsIIU(;l zB_(ps1XC(v5{;=OD;q<8GH**~+wpAeG&Ar)PI=-NzY zY;8ta7?qt@S@C1od6FZ*-&Q$pw{5F0w%V)Cc{0+GP4wEiwG%_v3x@E{(Hk!HJ<@|G zj5h{DXS;P4egUosMn0@RBqU%JM-Xs$3X${u zZe9l^>vuL06gM&qGVEqAE+sBrbQUGEONwVRheR1`g~GUv-*N&auYV!^TLXjfjmIG1 zXt;nm+lF{YtSFox`M#IdLF=Nyh3yHn9pfX!8&*4uMRruy#}JSl3+InWH8eSvqor__ zBn(AmJ1f+zq>Sm&FqmfpqCX2>`0v`h0kW5zFW35kwg;lgg?5mkI&hA2pQS#k`xR=e z&H)uN7X}#8oMHdltKk|8fIVW z%N#8NN`|@R>!$%g*iC2&_||LzK=in8B_2bP#H?~ZdLyL9vC=x)F%?;zs5@qHlLf(->K*?p|9)#4cyWsZ*#83{JY~E9 literal 4228 zcmZWtXHb({wEX~Sp$kZnDn)8c0!R-vSO`U$AORDkBTYcMQlv>hI-vLM=>8ENpBn0D$`uzy{Dl7zfb( zyZ-+$GBJ?=06G=`;E)9X{z!Mml_C=p8#4glgwV2uX%KPGs&ctvj4+{TNE#QVK?2YR z&RAGqg3+|QG;T@*`qKOtQli!F3onI2xwd%FVF0U}E15!F`{T~Ek>G% z4sYH&VI~@=H(cn-ZWk35ml749>)DAu0KJ?XDIGHGF^2Q$|9HWJuu}B4C|P&dL_~XW zlMKo|MP#f&AFGad`>@+${QL=3jy5)mBrk&cSOg_!?$uaPYs7@P|Lm5T2WjKEy3-F? zbDvxHCyI8D%jQzOYt(PtCx{O6%6koptQxt!e6B#rIU1yQd)9RI=&D0qR9%IlX-?(f z#X%Te=9rivpjIXq5i5&^OaycX%n=4xyxJG2B^FNea-C6~7xZdF-Cn8C&K3`ev?h6^=P87k8IMjI#^iDNq1 zJ`A_c^%&Raf5V3;Gdk83_9}f2X+Eh;8=N3q%RBINY&ZLpUwxjmw}@-QAFwojc+GQd z+iF?<-W_V-cN5%hY8ac+v_^b*+GIZQ9xHUs-4-J7kb{S93B5GPqDc9>s)!@#84lrv zN1u29VCwx6SVFA0%pO{6CQR|za|wL6DmFyv6qYi+$>8`oIc1o`-I?jREBnl2QSnEx zBa!1IZYR#P<;|Fbm#|{=Y6SOJ2T)oPS6_R~-Kuu&sklmvjCGjD&2P+(w=u%N7EFG` z57=zy{Bt)2x4Sgp3iiI}=ojOC^I=1WIC({uSIxvv057%8`)wd!vfD@F7f;#s_e4Et z+FmKwykKced^IMO+~RWKDhYg(Hz<;BEkeXoGGf^xD&EY`RGy&FHwzd?QZnj zVT;1T_cRY3GHN#_Y$iOO8kLq<*lk(YZDcQwuKi_6f=lwZ!}m^8;Fv!m$Gvs@(d>$d z(v`3z_>5aS{HLJ1n(4a2lPgIz81XT0(g5eHmqFNi`sJvugOaa(Sj}G-?o~<-fA1m@ z?``~!&BjIkxG+g4s$`SK7tm_b_LswswGndTo`?ZQ=R;dq_%D^|*r|~8@4jf1f_+W% z9~^XTj@yi3)TAvhR^g)W@*U65s%NL;|E&B}x0miqr!O_?q56MMK)7Xh_FagpajF(R zXJ6!PBP?$F?O_0{4a3{=UVlE@{C;&sq~WAp%_O^R+G8(}B0T7enqQ*n!Z~TGN5QT) z^Q}*s@86Nz0@`Z>niIUfXt_wJeTLiWC_8T9XBBM4a|w78MzPH1&>A@+yKG$ulquip zWt{)?gN^sIvDoisLkIr2(JnKj#`~W$U1rgZt?IJ99Xd5SbK|3=lSrR8m6>_L(oe9b zca;pO4Tz!ION-PIu5!!%+!kL(a52yKyQjNi$Q`!&CF&_+@5lLtt}gg zldYpQF>9mwCM!lUmZiX}4Bjm;yJRoSL1!Sq)Jik#V`%u+3N)8MaAH%)u+ z9Z!TNynO6KK0(iUtMsI`{?JNoXpu+hKMD@|0rj5WDux?;Raa|d5SoY$VmIC9oxaa0 z={qyb@*seu7zaX}Y=ePwAY3r})Vd{;zh@`0fNa(&DLvwqFqS@+Tt*ob;#vGL{8jh6S733)o83z#@2@sZaQu)^)7c z(~pn+Bw2UM#~rG{@rOEZn4eJR1u>HP$;`po3jQXlFY}*Wn zE%~c}yX4=rWaFDni!H47RxoZ&Vi;Bk=lWr33@mq!| zwN{{uw;$#z+rS~$p&b7dP5ssQ02wm;8Yr)D>X)LV)1RNDv!DHpK033BAI*Ca94_Ha z7=5PuNM`sFUsiUY*7d~E9U=59pan@%l^@Y@|4E1M)_IVfUM{zl_DF>LjhMq+v~Yty zA8QK7$Ez0D&x|1}sYry^R*i1i2?~nF7^I>(@zH|NS8^~;4ARiN3;hUL zPuJDpq+sA#ua24g(44xXHka^YBz{65`MCgmu-8C@gZW*()2GGfL`ka*f52P+js~9Z z_5<%4C-nZ;vgN50mB8HAY3y8)}$!yy^og8*Cn1tyXd1mglM~}XJ0`e$i z9*CY@Q<)E2<@8n{64dn{e-V(FMft?=LYI1p`xjpr+%_N>rhI&5@IJ4+uBnS9O83D< zd)ZEUq<31Rh6flQulPV)hAkZP!*DuuOf8w}Xd+Oyk$q!r(KI;~hFR^Per%}MvzsTR z+&Vp$SfTu}S`;{zIrhM4nrT;G0quyW27*vM&Si~T?s=t1_nfo@+Cnom9 z%dr{DCXRd^Ef;tSuD|SJ^G;Z1s9o7SJ6*9ukBi|1Np75@aMh>t(ed=~w>JG7gG*mt-*NaiifzrxBUT+8Hb`!DM*;V8&kDc4I?# zAzU=wb6A&gdsC?RHB7*CvuyluS`+j+Z>@l;5YA^Vnl#as1R)!4q_;2#NwK-hv+%0A znTG17S}g^A{$VrtrVO-L+<6#tT%g_WP^u3mk@3Mo@3j)*>zc{VBdMi^(gV5u8p|l1#X*J-`>eYEoG*O~4&|B7 zHqCzq)=H$5;=;tbUmC17c}_}ytH6R4;`~uWArr4z5n|mUYenP9H~Z2c`s&i4H*&4`^fe@b48@gn_P)~wTdVZ5ujP*&W<1pzZRgs0u=^xJtIyDQb_Rd-Ntj*Geq_7J z$}-e(f>&H?W5o5^!L1}$FJ5uUqq9vrmokpMmLZ~sRV`^PH_-JLVrRkbUgIMH+rR9Z z6E+RTnv((wr6VlfJLE2GT_LFEmnu9_zu?ndbiV!b+t&q^Q_WRH1Y%zJ)7`EW+f?iU z>JYzFn@-RIjD zm&NX9rsQ0ie_;n|Td$i!rh5~Xt%naPNyd?+TU!BrHx5Jws6FR%bD$2K1ZjqoBkf%0 znaW=8+!E6eH;;%zwN20Tpb2VcSlU5@3_cv1nW@c`m$ULtP?u>p8H*jT_Dxw$@kTx! zTh_zSQwsj|Nq5j_v?~yoU2>Etag6$~Ed&t;O#GRs|GVdkMlUe9urhI@UC?I?B_-VH zD1rbZH-MlSt$%CXmoUX-@*ufuy$AQ6OkUuu?1Wu~y@Eg>&<2)Ei2mWq-|y* z4BEV)FEn@a^c}<71NPK^?@5~jaG6}55M4U*eG3avfyR)Dc{3{9ho>FLEg^A$D8i2R z0(5V)9sb|kQTwOhc(I8Hro@Nr`e)_h=-YtQT1NZ-_FZBCStcfCSr(2i5LzqNOV>Ev zfC--=j+BMatn=lo^fVJq3rhgt{Wm`(9qP>hup$yy`ehE>YOi5pO)M-_3hDg9t0f3=itYthmimV7e0$>t zUe!~vXj{~#*idg4Ai|Y{Lh;vi&g!{}QLrCTgAZMw28`VNR&&Cotm~)~t5!ey(@}gX zPJoy$rG44hDLV9v?-@lmP7!kto5a3R%i1DL&EM0`<{+gPd<2*B^|0|P7>zo^?KfWD zK6$dC=ZJ{ayOZ|Sb*9TL_cBS&6B;!L;n=CZXmO@x@5siVT&PL)tN* */ @@ -22,7 +21,7 @@ The control icons are from a custom font. Each icon corresponds to a character @font-face { font-family: 'VideoJS'; src: url('font/vjs.eot'); - src: url('font/vjs.eot?#iefix') format('embedded-opentype'), url('font/vjs.woff') format('woff'), url('font/vjs.ttf') format('truetype'); + src: url('font/vjs.eot?#iefix') format('embedded-opentype'), url('font/vjs.woff') format('woff'), url('font/vjs.ttf') format('truetype'), url('font/vjs.svg#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -31,7 +30,7 @@ The control icons are from a custom font. Each icon corresponds to a character */ /* Slider - used for Volume bar and Seek bar */ .vjs-default-skin .vjs-slider { - /* Replace browser focus hightlight with handle highlight */ + /* Replace browser focus highlight with handle highlight */ outline: 0; position: relative; cursor: pointer; @@ -94,6 +93,7 @@ The default control bar that is a container for most of the controls. .vjs-default-skin.vjs-has-started .vjs-control-bar { display: block; /* Visibility needed to make sure things hide in older browsers too. */ + visibility: visible; opacity: 1; /* transition */ @@ -119,6 +119,15 @@ The default control bar that is a container for most of the controls. .vjs-default-skin.vjs-using-native-controls .vjs-control-bar { display: none; } +/* The control bar shouldn't show after an error */ +.vjs-default-skin.vjs-error .vjs-control-bar { + display: none; +} +/* Don't hide the control bar if it's audio */ +.vjs-audio.vjs-default-skin.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { + opacity: 1; + visibility: visible; +} /* IE8 is flakey with fonts, and you have to change the actual content to force fonts to show/hide properly. - "\9" IE8 hack didn't work for this @@ -140,7 +149,7 @@ fonts to show/hide properly. height: 3.0em; width: 4em; } -/* FontAwsome button icons */ +/* Font button icons */ .vjs-default-skin .vjs-control:before { font-family: VideoJS; font-size: 1.5em; @@ -162,6 +171,7 @@ fonts to show/hide properly. /* outline: 0; */ /* keyboard-only users cannot see the focus on several of the UI elements when this is set to 0 */ + } /* Hide control text visually, but have it available for screenreaders */ .vjs-default-skin .vjs-control-text { @@ -188,6 +198,25 @@ fonts to show/hide properly. .vjs-default-skin.vjs-playing .vjs-play-control:before { content: "\e002"; } +/* Playback toggle +-------------------------------------------------------------------------------- +*/ +.vjs-default-skin .vjs-playback-rate .vjs-playback-rate-value { + font-size: 1.5em; + line-height: 2; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} +.vjs-default-skin .vjs-playback-rate.vjs-menu-button .vjs-menu .vjs-menu-content { + width: 4em; + left: -2em; + list-style: none; +} /* Volume/Mute -------------------------------------------------------------------------------- */ .vjs-default-skin .vjs-mute-control, @@ -220,19 +249,24 @@ fonts to show/hide properly. height: 0.6em; margin: 1.1em auto 0; } -.vjs-default-skin .vjs-volume-menu-button .vjs-menu-content { - height: 2.9em; -} .vjs-default-skin .vjs-volume-level { position: absolute; top: 0; left: 0; height: 0.5em; + /* assuming volume starts at 1.0 */ + + width: 100%; background: #66a8cc url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAP0lEQVQIHWWMAQoAIAgDR/QJ/Ub//04+w7ZICBwcOg5FZi5iBB82AGzixEglJrd4TVK5XUJpskSTEvpdFzX9AB2pGziSQcvAAAAAAElFTkSuQmCC) -50% 0 repeat; } .vjs-default-skin .vjs-volume-bar .vjs-volume-handle { width: 0.5em; height: 0.5em; + /* Assumes volume starts at 1.0. If you change the size of the + handle relative to the volume bar, you'll need to update this value + too. */ + + left: 4.5em; } .vjs-default-skin .vjs-volume-handle:before { font-size: 0.9em; @@ -241,9 +275,31 @@ fonts to show/hide properly. width: 1em; height: 1em; } +/* The volume menu button is like menu buttons (captions/subtitles) but works + a little differently. It needs to be possible to tab to the volume slider + without hitting space bar on the menu button. To do this we're not using + display:none to hide the slider menu by default, and instead setting the + width and height to zero. */ +.vjs-default-skin .vjs-volume-menu-button .vjs-menu { + display: block; + width: 0; + height: 0; + border-top-color: transparent; +} .vjs-default-skin .vjs-volume-menu-button .vjs-menu .vjs-menu-content { - width: 6em; - left: -4em; + height: 0; + width: 0; +} +.vjs-default-skin .vjs-volume-menu-button:hover .vjs-menu, +.vjs-default-skin .vjs-volume-menu-button .vjs-menu.vjs-lock-showing { + border-top-color: rgba(7, 40, 50, 0.5); + /* Same as ul background */ + +} +.vjs-default-skin .vjs-volume-menu-button:hover .vjs-menu .vjs-menu-content, +.vjs-default-skin .vjs-volume-menu-button .vjs-menu.vjs-lock-showing .vjs-menu-content { + height: 2.9em; + width: 10em; } /* Progress -------------------------------------------------------------------------------- @@ -271,6 +327,7 @@ fonts to show/hide properly. font-size: .9em; /* Even though we're not changing the top/height, we need to include them in the transition so they're handled correctly. */ + /* transition */ -webkit-transition: all 0.2s; -moz-transition: all 0.2s; @@ -283,12 +340,16 @@ fonts to show/hide properly. } /* Progress Bars */ .vjs-default-skin .vjs-progress-holder .vjs-play-progress, -.vjs-default-skin .vjs-progress-holder .vjs-load-progress { +.vjs-default-skin .vjs-progress-holder .vjs-load-progress, +.vjs-default-skin .vjs-progress-holder .vjs-load-progress div { position: absolute; display: block; height: 100%; margin: 0; padding: 0; + /* updated by javascript during playback */ + + width: 0; /* Needed for IE6 */ left: 0; top: 0; @@ -302,11 +363,18 @@ fonts to show/hide properly. Also using a paralax effect to make the lines move backwards. The -50% left position makes that happen. */ + background: #66a8cc url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAP0lEQVQIHWWMAQoAIAgDR/QJ/Ub//04+w7ZICBwcOg5FZi5iBB82AGzixEglJrd4TVK5XUJpskSTEvpdFzX9AB2pGziSQcvAAAAAAElFTkSuQmCC) -50% 0 repeat; } .vjs-default-skin .vjs-load-progress { background: #646464 /* IE8- Fallback */; - background: rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.2); +} +/* there are child elements of the load progress bar that represent the + specific time ranges that have been buffered */ +.vjs-default-skin .vjs-load-progress div { + background: #787878 /* IE8- Fallback */; + background: rgba(255, 255, 255, 0.1); } .vjs-default-skin .vjs-seek-handle { width: 1.5em; @@ -315,6 +383,25 @@ fonts to show/hide properly. .vjs-default-skin .vjs-seek-handle:before { padding-top: 0.1em /* Minor adjustment */; } +/* Live Mode +-------------------------------------------------------------------------------- +*/ +.vjs-default-skin.vjs-live .vjs-time-controls, +.vjs-default-skin.vjs-live .vjs-time-divider, +.vjs-default-skin.vjs-live .vjs-progress-control { + display: none; +} +.vjs-default-skin.vjs-live .vjs-live-display { + display: block; +} +/* Live Display +-------------------------------------------------------------------------------- +*/ +.vjs-default-skin .vjs-live-display { + display: none; + font-size: 1em; + line-height: 3em; +} /* Time Display -------------------------------------------------------------------------------- */ @@ -397,7 +484,7 @@ easily in the skin designer. http://designer.videojs.com/ margin-left: -2.1em; /* Center it vertically */ top: 50%; - margin-top: -1.4em; + margin-top: -1.4000000000000001em; } /* Hide if controls are disabled */ .vjs-default-skin.vjs-controls-disabled .vjs-big-play-button { @@ -434,6 +521,7 @@ easily in the skin designer. http://designer.videojs.com/ font-family: VideoJS; /* In order to center the play icon vertically we need to set the line height to the same as the button height */ + line-height: 2.6em; text-shadow: 0.05em 0.05em 0.1em #000; text-align: center /* Needed for IE8 */; @@ -442,10 +530,61 @@ easily in the skin designer. http://designer.videojs.com/ width: 100%; height: 100%; } +.vjs-error .vjs-big-play-button { + display: none; +} +/* Error Display +-------------------------------------------------------------------------------- +*/ +.vjs-error-display { + display: none; +} +.vjs-error .vjs-error-display { + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} +.vjs-error .vjs-error-display:before { + content: 'X'; + font-family: Arial; + font-size: 4em; + color: #666666; + /* In order to center the play icon vertically we need to set the line height + to the same as the button height */ + + line-height: 1; + text-shadow: 0.05em 0.05em 0.1em #000; + text-align: center /* Needed for IE8 */; + vertical-align: middle; + position: absolute; + left: 0; + top: 50%; + margin-top: -0.5em; + width: 100%; +} +.vjs-error-display div { + position: absolute; + bottom: 1em; + right: 0; + left: 0; + font-size: 1.4em; + text-align: center; + padding: 3px; + background: #000000; + background: rgba(0, 0, 0, 0.5); +} +.vjs-error-display a, +.vjs-error-display a:visited { + color: #F4A460; +} /* Loading Spinner -------------------------------------------------------------------------------- */ .vjs-loading-spinner { + /* Should be hidden by default */ display: none; position: absolute; top: 50%; @@ -457,12 +596,28 @@ easily in the skin designer. http://designer.videojs.com/ margin-left: -0.5em; margin-top: -0.5em; opacity: 0.75; +} +/* Show the spinner when waiting for data and seeking to a new time */ +.vjs-waiting .vjs-loading-spinner, +.vjs-seeking .vjs-loading-spinner { + display: block; + /* only animate when showing because it can be processor heavy */ /* animation */ -webkit-animation: spin 1.5s infinite linear; -moz-animation: spin 1.5s infinite linear; -o-animation: spin 1.5s infinite linear; animation: spin 1.5s infinite linear; } +/* Errors are unrecoverable without user interaction so hide the spinner */ +.vjs-error .vjs-loading-spinner { + display: none; + /* ensure animation doesn't continue while hidden */ + /* animation */ + -webkit-animation: none; + -moz-animation: none; + -o-animation: none; + animation: none; +} .vjs-default-skin .vjs-loading-spinner:before { content: "\e01e"; font-family: VideoJS; @@ -519,6 +674,7 @@ easily in the skin designer. http://designer.videojs.com/ bottom: 0; left: 0em; /* (Width of vjs-menu - width of button) / 2 */ + width: 0em; height: 0em; margin-bottom: 3em; @@ -526,8 +682,10 @@ easily in the skin designer. http://designer.videojs.com/ border-right: 2em solid transparent; border-top: 1.55em solid #000000; /* Same width top as ul bottom */ + border-top-color: rgba(7, 40, 50, 0.5); /* Same as ul background */ + } /* Button Pop-up Menu */ .vjs-default-skin .vjs-menu-button .vjs-menu .vjs-menu-content { @@ -538,10 +696,12 @@ easily in the skin designer. http://designer.videojs.com/ width: 10em; bottom: 1.5em; /* Same bottom as vjs-menu border-top */ + max-height: 15em; overflow: auto; left: -5em; /* Width of menu - width of button / 2 */ + /* background-color-with-alpha */ background-color: #07141e; background-color: rgba(7, 20, 30, 0.7); @@ -550,9 +710,14 @@ easily in the skin designer. http://designer.videojs.com/ -moz-box-shadow: -0.2em -0.2em 0.3em rgba(255, 255, 255, 0.2); box-shadow: -0.2em -0.2em 0.3em rgba(255, 255, 255, 0.2); } -.vjs-default-skin .vjs-menu-button:hover .vjs-menu { +.vjs-default-skin .vjs-menu-button:hover .vjs-control-content .vjs-menu, +.vjs-default-skin .vjs-control-content .vjs-menu.vjs-lock-showing { display: block; } +/* prevent menus from opening while scrubbing (FF, IE) */ +.vjs-default-skin.vjs-scrubbing .vjs-menu-button:hover .vjs-control-content .vjs-menu { + display: none; +} .vjs-default-skin .vjs-menu-button ul li { list-style: none; margin: 0; @@ -597,6 +762,14 @@ easily in the skin designer. http://designer.videojs.com/ .vjs-default-skin .vjs-captions-button:before { content: "\e008"; } +/* Chapters Button */ +.vjs-default-skin .vjs-chapters-button:before { + content: "\e00c"; +} +.vjs-default-skin .vjs-chapters-button.vjs-menu-button .vjs-menu .vjs-menu-content { + width: 24em; + left: -12em; +} /* Replacement for focus outline */ .vjs-default-skin .vjs-captions-button:focus .vjs-control-content:before, .vjs-default-skin .vjs-captions-button:hover .vjs-control-content:before { @@ -623,19 +796,25 @@ control positioning and full window mode. ** padding: 0; /* Start with 10px for base font size so other dimensions can be em based and easily calculable. */ + font-size: 10px; - /* Allow poster to be vertially aligned. */ + /* Allow poster to be vertically aligned. */ + vertical-align: middle; /* display: table-cell; */ /*This works in Safari but not Firefox.*/ + /* Provide some basic defaults for fonts */ + font-weight: normal; font-style: normal; /* Avoiding helvetica: issue #376 */ + font-family: Arial, sans-serif; /* Turn off user selection (text highlighting) by default. The majority of player components will not be text blocks. Text areas will need to turn user selection back on. */ + /* user-select */ -webkit-user-select: none; -moz-user-select: none; @@ -690,11 +869,13 @@ body.vjs-full-window { background-position: 50% 50%; background-size: contain; cursor: pointer; - height: 100%; margin: 0; padding: 0; - position: relative; - width: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; } .vjs-poster img { display: block; @@ -703,6 +884,19 @@ body.vjs-full-window { padding: 0; width: 100%; } +/* Hide the poster after the video has started playing */ +.video-js.vjs-has-started .vjs-poster { + display: none; +} +/* Don't hide the poster if we're playing audio */ +.video-js.vjs-audio.vjs-has-started .vjs-poster { + display: block; +} +/* Hide the poster when controls are disabled because it's clickable + and the native poster can take over */ +.video-js.vjs-controls-disabled .vjs-poster { + display: none; +} /* Hide the poster when native controls are used otherwise it covers them */ .video-js.vjs-using-native-controls .vjs-poster { display: none; @@ -710,46 +904,107 @@ body.vjs-full-window { /* Text Track Styles */ /* Overall track holder for both captions and subtitles */ .video-js .vjs-text-track-display { - text-align: center; position: absolute; - bottom: 4em; - /* Leave padding on left and right */ - left: 1em; - right: 1em; + top: 0; + left: 0; + bottom: 3em; + right: 0; + pointer-events: none; } -/* Move captions down when controls aren't being shown */ -.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display { - bottom: 1em; +/* Captions Settings Dialog */ +.vjs-caption-settings { + position: relative; + top: 1em; + background-color: #000; + opacity: 0.75; + color: #FFF; + margin: 0 auto; + padding: 0.5em; + height: 15em; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + width: 40em; } -/* Individual tracks */ -.video-js .vjs-text-track { - display: none; - font-size: 1.4em; - text-align: center; - margin-bottom: 0.1em; - /* Transparent black background, or fallback to all black (oldIE) */ - /* background-color-with-alpha */ - background-color: #000000; - background-color: rgba(0, 0, 0, 0.5); +.vjs-caption-settings .vjs-tracksettings { + top: 0; + bottom: 2em; + left: 0; + right: 0; + position: absolute; + overflow: auto; +} +.vjs-caption-settings .vjs-tracksettings-colors, +.vjs-caption-settings .vjs-tracksettings-font { + float: left; +} +.vjs-caption-settings .vjs-tracksettings-colors:after, +.vjs-caption-settings .vjs-tracksettings-font:after, +.vjs-caption-settings .vjs-tracksettings-controls:after { + clear: both; } -.video-js .vjs-subtitles { - color: #ffffff /* Subtitles are white */; +.vjs-caption-settings .vjs-tracksettings-controls { + position: absolute; + bottom: 1em; + right: 1em; } -.video-js .vjs-captions { - color: #ffcc66 /* Captions are yellow */; +.vjs-caption-settings .vjs-tracksetting { + margin: 5px; + padding: 3px; + min-height: 40px; } -.vjs-tt-cue { +.vjs-caption-settings .vjs-tracksetting label { display: block; + width: 100px; + margin-bottom: 5px; +} +.vjs-caption-settings .vjs-tracksetting span { + display: inline; + margin-left: 5px; +} +.vjs-caption-settings .vjs-tracksetting > div { + margin-bottom: 5px; + min-height: 20px; +} +.vjs-caption-settings .vjs-tracksetting > div:last-child { + margin-bottom: 0; + padding-bottom: 0; + min-height: 0; +} +.vjs-caption-settings label > input { + margin-right: 10px; +} +.vjs-caption-settings input[type="button"] { + width: 40px; + height: 40px; } /* Hide disabled or unsupported controls */ -.vjs-default-skin .vjs-hidden { - display: none; +.vjs-hidden { + display: none !important; } .vjs-lock-showing { display: block !important; opacity: 1; visibility: visible; } +/* In IE8 w/ no JavaScript (no HTML5 shim), the video tag doesn't register. + The .video-js classname on the video tag also isn't considered. + This optional paragraph inside the video tag can provide a message to users + about what's required to play video. */ +.vjs-no-js { + padding: 2em; + color: #ccc; + background-color: #333; + font-size: 1.8em; + font-family: Arial, sans-serif; + text-align: center; + width: 30em; + height: 15em; + margin: 0 auto; +} +.vjs-no-js a, +.vjs-no-js a:visited { + color: #F4A460; +} /* ----------------------------------------------------------------------------- The original source of this file lives at https://github.com/videojs/video.js/blob/master/src/css/video-js.less */ diff --git a/videojs/video-js.less b/videojs/video-js.less old mode 100644 new mode 100755 index 9663396..fc43aee --- a/videojs/video-js.less +++ b/videojs/video-js.less @@ -1,4 +1,4 @@ -/*! + /*! Video.js Default Styles (http://videojs.com) Version GENERATED_AT_BUILD Create your own skin at http://designer.videojs.com @@ -9,7 +9,7 @@ Create your own skin at http://designer.videojs.com // (This file uses LESS. Learn more at http://lesscss.org/) // The base font size controls the size of everything, not just text. All -// diminensions use em-based sizes so that the scale along with the font size. +// dimensions use em-based sizes so that the scale along with the font size. // Try increasing it to 20px and see what happens. @base-font-size: 10px; @touch-device-font-size: 15px; @@ -60,7 +60,7 @@ Create your own skin at http://designer.videojs.com /* SKIN ================================================================================ The main class name for all skin-specific styles. To make your own skin, -replace all occurances of 'vjs-default-skin' with a new name. Then add your new +replace all occurrences of 'vjs-default-skin' with a new name. Then add your new skin name to your video tag instead of the default skin. e.g. or download the ' + - 'latest Adobe Flash Player.' + 'notSupportedMessage': 'No compatible source was found for this video.' }; // Set CDN Version of swf -// The added (+) blocks the replace from changing this 4.5 string +// The added (+) blocks the replace from changing this 4.12 string if (vjs.CDN_VERSION !== 'GENERATED'+'_CDN_VSN') { videojs.options['flash']['swf'] = vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/'+vjs.CDN_VERSION+'/video-js.swf'; } +/** + * Utility function for adding languages to the default options. Useful for + * amending multiple language support at runtime. + * + * Example: vjs.addLanguage('es', {'Hello':'Hola'}); + * + * @param {String} code The language code or dictionary property + * @param {Object} data The data values to be translated + * @return {Object} The resulting global languages dictionary object + */ +vjs.addLanguage = function(code, data){ + if(vjs.options['languages'][code] !== undefined) { + vjs.options['languages'][code] = vjs.util.mergeOptions(vjs.options['languages'][code], data); + } else { + vjs.options['languages'][code] = data; + } + return vjs.options['languages']; +}; + /** * Global player list * @type {Object} @@ -125,14 +171,14 @@ vjs.players = {}; * compiler compatible, so string keys are used. */ if (typeof define === 'function' && define['amd']) { - define([], function(){ return videojs; }); + define('videojs', [], function(){ return videojs; }); // checking that module is an object too because of umdjs/umd#35 } else if (typeof exports === 'object' && typeof module === 'object') { module['exports'] = videojs; } /** - * Core Object/Class for objects that use inheritance + contstructors + * Core Object/Class for objects that use inheritance + constructors * * To create a class that can be subclassed itself, extend the CoreObject class. * @@ -185,7 +231,7 @@ if (typeof define === 'function' && define['amd']) { vjs.CoreObject = vjs['CoreObject'] = function(){}; // Manually exporting vjs['CoreObject'] here for Closure Compiler // because of the use of the extend/create class methods -// If we didn't do this, those functions would get flattend to something like +// If we didn't do this, those functions would get flattened to something like // `a = ...` and `this.prototype` would refer to the global object instead of // CoreObject @@ -211,10 +257,10 @@ vjs.CoreObject.extend = function(props){ // In Resig's simple class inheritance (previously used) the constructor // is a function that calls `this.init.apply(arguments)` // However that would prevent us from using `ParentObject.call(this);` - // in a Child constuctor because the `this` in `this.init` - // would still refer to the Child and cause an inifinite loop. + // in a Child constructor because the `this` in `this.init` + // would still refer to the Child and cause an infinite loop. // We would instead have to do - // `ParentObject.prototype.init.apply(this, argumnents);` + // `ParentObject.prototype.init.apply(this, arguments);` // Bleh. We're not creating a _super() function, so it's good to keep // the parent constructor reference simple. subObj = function(){ @@ -243,7 +289,7 @@ vjs.CoreObject.extend = function(props){ }; /** - * Create a new instace of this Object class + * Create a new instance of this Object class * * var myAnimal = Animal.create(); * @@ -273,11 +319,15 @@ vjs.CoreObject.create = function(){ * and adds a generic handler to the element's event, * along with a unique id (guid) to the element. * @param {Element|Object} elem Element or object to bind listeners to - * @param {String} type Type of event to bind to. + * @param {String|Array} type Type of event to bind to. * @param {Function} fn Event listener. * @private */ vjs.on = function(elem, type, fn){ + if (vjs.obj.isArray(type)) { + return _handleMultipleEvents(vjs.on, elem, type, fn); + } + var data = vjs.getData(elem); // We need a place to store all our handler data @@ -315,9 +365,9 @@ vjs.on = function(elem, type, fn){ } if (data.handlers[type].length == 1) { - if (document.addEventListener) { + if (elem.addEventListener) { elem.addEventListener(type, data.dispatcher, false); - } else if (document.attachEvent) { + } else if (elem.attachEvent) { elem.attachEvent('on' + type, data.dispatcher); } } @@ -326,8 +376,8 @@ vjs.on = function(elem, type, fn){ /** * Removes event listeners from an element * @param {Element|Object} elem Object to remove listeners from - * @param {String=} type Type of listener to remove. Don't include to remove all events from element. - * @param {Function} fn Specific listener to remove. Don't incldue to remove listeners for an event type. + * @param {String|Array=} type Type of listener to remove. Don't include to remove all events from element. + * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. * @private */ vjs.off = function(elem, type, fn) { @@ -339,6 +389,10 @@ vjs.off = function(elem, type, fn) { // If no events exist, nothing to unbind if (!data.handlers) { return; } + if (vjs.obj.isArray(type)) { + return _handleMultipleEvents(vjs.off, elem, type, fn); + } + // Utility function var removeType = function(t){ data.handlers[t] = []; @@ -390,9 +444,9 @@ vjs.cleanUpEvents = function(elem, type) { // Setting to null was causing an error with data.handlers // Remove the meta-handler from the element - if (document.removeEventListener) { + if (elem.removeEventListener) { elem.removeEventListener(type, data.dispatcher, false); - } else if (document.detachEvent) { + } else if (elem.detachEvent) { elem.detachEvent('on' + type, data.dispatcher); } } @@ -442,7 +496,7 @@ vjs.fixEvent = function(event) { for (var key in old) { // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation - if (key !== 'layerX' && key !== 'layerY' && key !== 'keyboardEvent.keyLocation') { + if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation') { // Chrome 32+ warns if you try to copy deprecated returnValue, but // we still want to if preventDefault isn't supported (IE8). if (!(key == 'returnValue' && old.preventDefault)) { @@ -526,8 +580,8 @@ vjs.fixEvent = function(event) { /** * Trigger an event for an element - * @param {Element|Object} elem Element to trigger an event on - * @param {String} event Type of event to trigger + * @param {Element|Object} elem Element to trigger an event on + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute * @private */ vjs.trigger = function(elem, event) { @@ -579,7 +633,7 @@ vjs.trigger = function(elem, event) { * We've since updated to the latest version, but keeping this around * for now just in case. */ - // // Added in attion to book. Book code was broke. + // // Added in addition to book. Book code was broke. // event = typeof event === 'object' ? // event[vjs.expando] ? // event : @@ -599,18 +653,36 @@ vjs.trigger = function(elem, event) { /** * Trigger a listener only once for an event * @param {Element|Object} elem Element or object to - * @param {String} type + * @param {String|Array} type * @param {Function} fn * @private */ vjs.one = function(elem, type, fn) { + if (vjs.obj.isArray(type)) { + return _handleMultipleEvents(vjs.one, elem, type, fn); + } var func = function(){ vjs.off(elem, type, func); fn.apply(this, arguments); }; + // copy the guid to the new function so it can removed using the original function's ID func.guid = fn.guid = fn.guid || vjs.guid++; vjs.on(elem, type, func); }; + +/** + * Loops through an array of event types and calls the requested method for each type. + * @param {Function} fn The event method we want to use. + * @param {Element|Object} elem Element or object to bind listeners to + * @param {String} type Type of event to bind to. + * @param {Function} callback Event listener. + * @private + */ +function _handleMultipleEvents(fn, elem, type, callback) { + vjs.arr.forEach(type, function(type) { + fn(elem, type, callback); //Call the event method for each one of the types + }); +} var hasOwnProp = Object.prototype.hasOwnProperty; /** @@ -621,29 +693,29 @@ var hasOwnProp = Object.prototype.hasOwnProperty; * @private */ vjs.createEl = function(tagName, properties){ - var el, propName; + var el; - el = document.createElement(tagName || 'div'); + tagName = tagName || 'div'; + properties = properties || {}; - for (propName in properties){ - if (hasOwnProp.call(properties, propName)) { - //el[propName] = properties[propName]; - // Not remembering why we were checking for dash - // but using setAttribute means you have to use getAttribute + el = document.createElement(tagName); - // The check for dash checks for the aria-* attributes, like aria-label, aria-valuemin. - // The additional check for "role" is because the default method for adding attributes does not - // add the attribute "role". My guess is because it's not a valid attribute in some namespaces, although - // browsers handle the attribute just fine. The W3C allows for aria-* attributes to be used in pre-HTML5 docs. - // http://www.w3.org/TR/wai-aria-primer/#ariahtml. Using setAttribute gets around this problem. + vjs.obj.each(properties, function(propName, val){ + // Not remembering why we were checking for dash + // but using setAttribute means you have to use getAttribute - if (propName.indexOf('aria-') !== -1 || propName=='role') { - el.setAttribute(propName, properties[propName]); - } else { - el[propName] = properties[propName]; - } + // The check for dash checks for the aria-* attributes, like aria-label, aria-valuemin. + // The additional check for "role" is because the default method for adding attributes does not + // add the attribute "role". My guess is because it's not a valid attribute in some namespaces, although + // browsers handle the attribute just fine. The W3C allows for aria-* attributes to be used in pre-HTML5 docs. + // http://www.w3.org/TR/wai-aria-primer/#ariahtml. Using setAttribute gets around this problem. + if (propName.indexOf('aria-') !== -1 || propName == 'role') { + el.setAttribute(propName, val); + } else { + el[propName] = val; } - } + }); + return el; }; @@ -673,7 +745,7 @@ vjs.obj = {}; * @param {Object} obj Object to use as prototype * @private */ - vjs.obj.create = Object.create || function(obj){ +vjs.obj.create = Object.create || function(obj){ //Create a new function called 'F' which is just an empty object. function F() {} @@ -730,7 +802,7 @@ vjs.obj.merge = function(obj1, obj2){ vjs.obj.deepMerge = function(obj1, obj2){ var key, val1, val2; - // make a copy of obj1 so we're not ovewriting original values. + // make a copy of obj1 so we're not overwriting original values. // like prototype.options_ and all sub options objects obj1 = vjs.obj.copy(obj1); @@ -773,6 +845,28 @@ vjs.obj.isPlain = function(obj){ && obj.constructor === Object; }; +/** + * Check if an object is Array +* Since instanceof Array will not work on arrays created in another frame we need to use Array.isArray, but since IE8 does not support Array.isArray we need this shim + * @param {Object} obj Object to check + * @return {Boolean} True if plain, false otherwise + * @private + */ +vjs.obj.isArray = Array.isArray || function(arr) { + return Object.prototype.toString.call(arr) === '[object Array]'; +}; + +/** + * Check to see whether the input is NaN or not. + * NaN is the only JavaScript construct that isn't equal to itself + * @param {Number} num Number to check + * @return {Boolean} True if NaN, false otherwise + * @private + */ +vjs.isNaN = function(num) { + return num !== num; +}; + /** * Bind (a.k.a proxy or Context). A simple method for changing the context of a function It also stores a unique id on the function so it can be easily removed from events @@ -804,7 +898,7 @@ vjs.bind = function(context, fn, uid) { /** * Element Data Store. Allows for binding data to an element without putting it directly on the element. - * Ex. Event listneres are stored here. + * Ex. Event listeners are stored here. * (also from jsninja.com, slightly modified and updated for closure compiler) * @type {Object} * @private @@ -836,6 +930,8 @@ vjs.getData = function(el){ var id = el[vjs.expando]; if (!id) { id = el[vjs.expando] = vjs.guid++; + } + if (!vjs.cache[id]) { vjs.cache[id] = {}; } return vjs.cache[id]; @@ -895,6 +991,17 @@ vjs.isEmpty = function(obj) { return true; }; +/** + * Check if an element has a CSS class + * @param {Element} element Element to check + * @param {String} classToCheck Classname to check + * @private + */ +vjs.hasClass = function(element, classToCheck){ + return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1); +}; + + /** * Add a CSS class name to an element * @param {Element} element Element to add class name to @@ -902,7 +1009,7 @@ vjs.isEmpty = function(obj) { * @private */ vjs.addClass = function(element, classToAdd){ - if ((' '+element.className+' ').indexOf(' '+classToAdd+' ') == -1) { + if (!vjs.hasClass(element, classToAdd)) { element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd; } }; @@ -916,7 +1023,7 @@ vjs.addClass = function(element, classToAdd){ vjs.removeClass = function(element, classToRemove){ var classNames, i; - if (element.className.indexOf(classToRemove) == -1) { return; } + if (!vjs.hasClass(element, classToRemove)) {return;} classNames = element.className.split(' '); @@ -937,6 +1044,13 @@ vjs.removeClass = function(element, classToRemove){ * @private */ vjs.TEST_VID = vjs.createEl('video'); +(function() { + var track = document.createElement('track'); + track.kind = 'captions'; + track.srclang = 'en'; + track.label = 'English'; + vjs.TEST_VID.appendChild(track); +})(); /** * Useragent for browser testing. @@ -990,19 +1104,37 @@ vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.A vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT); vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT); +vjs.IS_IE8 = (/MSIE\s8\.0/).test(vjs.USER_AGENT); vjs.TOUCH_ENABLED = !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch); +vjs.BACKGROUND_SIZE_SUPPORTED = 'backgroundSize' in vjs.TEST_VID.style; + +/** + * Apply attributes to an HTML element. + * @param {Element} el Target element. + * @param {Object=} attributes Element attributes to be applied. + * @private + */ +vjs.setElementAttributes = function(el, attributes){ + vjs.obj.each(attributes, function(attrName, attrValue) { + if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { + el.removeAttribute(attrName); + } else { + el.setAttribute(attrName, (attrValue === true ? '' : attrValue)); + } + }); +}; /** * Get an element's attribute values, as defined on the HTML tag - * Attributs are not the same as properties. They're defined on the tag + * Attributes are not the same as properties. They're defined on the tag * or with setAttribute (which shouldn't be used with HTML) * This will return true or false for boolean attributes. * @param {Element} tag Element from which to get tag attributes * @return {Object} * @private */ -vjs.getAttributeValues = function(tag){ +vjs.getElementAttributes = function(tag){ var obj, knownBooleans, attrs, attrName, attrVal; obj = {}; @@ -1074,7 +1206,7 @@ vjs.insertFirst = function(child, parent){ * @type {Object} * @private */ -vjs.support = {}; +vjs.browser = {}; /** * Shorthand for document.getElementById() @@ -1178,56 +1310,7 @@ vjs.createTimeRange = function(start, end){ }; /** - * Simple http request for retrieving external files (e.g. text tracks) - * @param {String} url URL of resource - * @param {Function=} onSuccess Success callback - * @param {Function=} onError Error callback - * @private - */ -vjs.get = function(url, onSuccess, onError){ - var local, request; - - if (typeof XMLHttpRequest === 'undefined') { - window.XMLHttpRequest = function () { - try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} - try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} - try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} - throw new Error('This browser does not support XMLHttpRequest.'); - }; - } - - request = new XMLHttpRequest(); - try { - request.open('GET', url); - } catch(e) { - onError(e); - } - - local = (url.indexOf('file:') === 0 || (window.location.href.indexOf('file:') === 0 && url.indexOf('http') === -1)); - - request.onreadystatechange = function() { - if (request.readyState === 4) { - if (request.status === 200 || local && request.status === 0) { - onSuccess(request.responseText); - } else { - if (onError) { - onError(); - } - } - } - }; - - try { - request.send(); - } catch(e) { - if (onError) { - onError(e); - } - } -}; - -/** - * Add to local storage (may removeable) + * Add to local storage (may removable) * @private */ vjs.setLocalStorage = function(key, value){ @@ -1250,7 +1333,7 @@ vjs.setLocalStorage = function(key, value){ }; /** - * Get abosolute version of relative URL. Used to tell flash correct URL. + * Get absolute version of relative URL. Used to tell flash correct URL. * http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue * @param {String} url URL to make absolute * @return {String} Absolute URL @@ -1269,48 +1352,338 @@ vjs.getAbsoluteURL = function(url){ return url; }; -// usage: log('inside coolFunc',this,arguments); -// http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ -vjs.log = function(){ - vjs.log.history = vjs.log.history || []; // store logs to an array for reference - vjs.log.history.push(arguments); - if(window.console){ - window.console.log(Array.prototype.slice.call(arguments)); + +/** + * Resolve and parse the elements of a URL + * @param {String} url The url to parse + * @return {Object} An object of url details + */ +vjs.parseUrl = function(url) { + var div, a, addToBody, props, details; + + props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; + + // add the url to an anchor and let the browser parse the URL + a = vjs.createEl('a', { href: url }); + + // IE8 (and 9?) Fix + // ie8 doesn't parse the URL correctly until the anchor is actually + // added to the body, and an innerHTML is needed to trigger the parsing + addToBody = (a.host === '' && a.protocol !== 'file:'); + if (addToBody) { + div = vjs.createEl('div'); + div.innerHTML = ''; + a = div.firstChild; + // prevent the div from affecting layout + div.setAttribute('style', 'display:none; position:absolute;'); + document.body.appendChild(div); + } + + // Copy the specific URL properties to a new object + // This is also needed for IE8 because the anchor loses its + // properties when it's removed from the dom + details = {}; + for (var i = 0; i < props.length; i++) { + details[props[i]] = a[props[i]]; + } + + // IE9 adds the port to the host property unlike everyone else. If + // a port identifier is added for standard ports, strip it. + if (details.protocol === 'http:') { + details.host = details.host.replace(/:80$/, ''); + } + if (details.protocol === 'https:') { + details.host = details.host.replace(/:443$/, ''); + } + + if (addToBody) { + document.body.removeChild(div); + } + + return details; +}; + +/** + * Log messages to the console and history based on the type of message + * + * @param {String} type The type of message, or `null` for `log` + * @param {[type]} args The args to be passed to the log + * @private + */ +function _logType(type, args){ + var argsArray, noop, console; + + // convert args to an array to get array functions + argsArray = Array.prototype.slice.call(args); + // if there's no console then don't try to output messages + // they will still be stored in vjs.log.history + // Was setting these once outside of this function, but containing them + // in the function makes it easier to test cases where console doesn't exist + noop = function(){}; + console = window['console'] || { + 'log': noop, + 'warn': noop, + 'error': noop + }; + + if (type) { + // add the type to the front of the message + argsArray.unshift(type.toUpperCase()+':'); + } else { + // default to log with no prefix + type = 'log'; + } + + // add to history + vjs.log.history.push(argsArray); + + // add console prefix after adding to history + argsArray.unshift('VIDEOJS:'); + + // call appropriate log function + if (console[type].apply) { + console[type].apply(console, argsArray); + } else { + // ie8 doesn't allow error.apply, but it will just join() the array anyway + console[type](argsArray.join(' ')); } +} + +/** + * Log plain debug messages + */ +vjs.log = function(){ + _logType(null, arguments); +}; + +/** + * Keep a history of log messages + * @type {Array} + */ +vjs.log.history = []; + +/** + * Log error messages + */ +vjs.log.error = function(){ + _logType('error', arguments); +}; + +/** + * Log warning messages + */ +vjs.log.warn = function(){ + _logType('warn', arguments); }; // Offset Left // getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ vjs.findPosition = function(el) { - var box, docEl, body, clientLeft, scrollLeft, left, clientTop, scrollTop, top; + var box, docEl, body, clientLeft, scrollLeft, left, clientTop, scrollTop, top; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0 + }; + } + + docEl = document.documentElement; + body = document.body; + + clientLeft = docEl.clientLeft || body.clientLeft || 0; + scrollLeft = window.pageXOffset || body.scrollLeft; + left = box.left + scrollLeft - clientLeft; + + clientTop = docEl.clientTop || body.clientTop || 0; + scrollTop = window.pageYOffset || body.scrollTop; + top = box.top + scrollTop - clientTop; + + // Android sometimes returns slightly off decimal values, so need to round + return { + left: vjs.round(left), + top: vjs.round(top) + }; +}; + +/** + * Array functions container + * @type {Object} + * @private + */ +vjs.arr = {}; - if (el.getBoundingClientRect && el.parentNode) { - box = el.getBoundingClientRect(); +/* + * Loops through an array and runs a function for each item inside it. + * @param {Array} array The array + * @param {Function} callback The function to be run for each item + * @param {*} thisArg The `this` binding of callback + * @returns {Array} The array + * @private + */ +vjs.arr.forEach = function(array, callback, thisArg) { + if (vjs.obj.isArray(array) && callback instanceof Function) { + for (var i = 0, len = array.length; i < len; ++i) { + callback.call(thisArg || vjs, array[i], i, array); } + } - if (!box) { - return { - left: 0, - top: 0 - }; + return array; +}; +/** + * Simple http request for retrieving external files (e.g. text tracks) + * + * ##### Example + * + * // using url string + * videojs.xhr('http://example.com/myfile.vtt', function(error, response, responseBody){}); + * + * // or options block + * videojs.xhr({ + * uri: 'http://example.com/myfile.vtt', + * method: 'GET', + * responseType: 'text' + * }, function(error, response, responseBody){ + * if (error) { + * // log the error + * } else { + * // successful, do something with the response + * } + * }); + * + * + * API is modeled after the Raynos/xhr, which we hope to use after + * getting browserify implemented. + * https://github.com/Raynos/xhr/blob/master/index.js + * + * @param {Object|String} options Options block or URL string + * @param {Function} callback The callback function + * @returns {Object} The request + */ +vjs.xhr = function(options, callback){ + var XHR, request, urlInfo, winLoc, fileUrl, crossOrigin, abortTimeout, successHandler, errorHandler; + + // If options is a string it's the url + if (typeof options === 'string') { + options = { + uri: options + }; + } + + // Merge with default options + videojs.util.mergeOptions({ + method: 'GET', + timeout: 45 * 1000 + }, options); + + callback = callback || function(){}; + + successHandler = function(){ + window.clearTimeout(abortTimeout); + callback(null, request, request.response || request.responseText); + }; + + errorHandler = function(err){ + window.clearTimeout(abortTimeout); + + if (!err || typeof err === 'string') { + err = new Error(err); } - docEl = document.documentElement; - body = document.body; + callback(err, request); + }; + + XHR = window.XMLHttpRequest; + + if (typeof XHR === 'undefined') { + // Shim XMLHttpRequest for older IEs + XHR = function () { + try { return new window.ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) {} + try { return new window.ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (f) {} + try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (g) {} + throw new Error('This browser does not support XMLHttpRequest.'); + }; + } - clientLeft = docEl.clientLeft || body.clientLeft || 0; - scrollLeft = window.pageXOffset || body.scrollLeft; - left = box.left + scrollLeft - clientLeft; + request = new XHR(); + // Store a reference to the url on the request instance + request.uri = options.uri; + + urlInfo = vjs.parseUrl(options.uri); + winLoc = window.location; + // Check if url is for another domain/origin + // IE8 doesn't know location.origin, so we won't rely on it here + crossOrigin = (urlInfo.protocol + urlInfo.host) !== (winLoc.protocol + winLoc.host); + + // XDomainRequest -- Use for IE if XMLHTTPRequest2 isn't available + // 'withCredentials' is only available in XMLHTTPRequest2 + // Also XDomainRequest has a lot of gotchas, so only use if cross domain + if (crossOrigin && window.XDomainRequest && !('withCredentials' in request)) { + request = new window.XDomainRequest(); + request.onload = successHandler; + request.onerror = errorHandler; + // These blank handlers need to be set to fix ie9 + // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ + request.onprogress = function(){}; + request.ontimeout = function(){}; + + // XMLHTTPRequest + } else { + fileUrl = (urlInfo.protocol == 'file:' || winLoc.protocol == 'file:'); - clientTop = docEl.clientTop || body.clientTop || 0; - scrollTop = window.pageYOffset || body.scrollTop; - top = box.top + scrollTop - clientTop; + request.onreadystatechange = function() { + if (request.readyState === 4) { + if (request.timedout) { + return errorHandler('timeout'); + } - // Android sometimes returns slightly off decimal values, so need to round - return { - left: vjs.round(left), - top: vjs.round(top) + if (request.status === 200 || fileUrl && request.status === 0) { + successHandler(); + } else { + errorHandler(); + } + } }; + + if (options.timeout) { + abortTimeout = window.setTimeout(function() { + if (request.readyState !== 4) { + request.timedout = true; + request.abort(); + } + }, options.timeout); + } + } + + // open the connection + try { + // Third arg is async, or ignored by XDomainRequest + request.open(options.method || 'GET', options.uri, true); + } catch(err) { + return errorHandler(err); + } + + // withCredentials only supported by XMLHttpRequest2 + if(options.withCredentials) { + request.withCredentials = true; + } + + if (options.responseType) { + request.responseType = options.responseType; + } + + // send the request + try { + request.send(); + } catch(err) { + return errorHandler(err); + } + + return request; }; /** * Utility functions namespace @@ -1320,10 +1693,9 @@ vjs.findPosition = function(el) { vjs.util = {}; /** - * Merge two options objects, - * recursively merging any plain object properties as well. - * Previously `deepMerge` - * + * Merge two options objects, recursively merging any plain object properties as + * well. Previously `deepMerge` + * * @param {Object} obj1 Object to override values in * @param {Object} obj2 Overriding object * @return {Object} New object -- obj1 and obj2 will be untouched @@ -1331,7 +1703,7 @@ vjs.util = {}; vjs.util.mergeOptions = function(obj1, obj2){ var key, val1, val2; - // make a copy of obj1 so we're not ovewriting original values. + // make a copy of obj1 so we're not overwriting original values. // like prototype.options_ and all sub options objects obj1 = vjs.obj.copy(obj1); @@ -1349,9 +1721,49 @@ vjs.util.mergeOptions = function(obj1, obj2){ } } return obj1; +};vjs.EventEmitter = function() { +}; + +vjs.EventEmitter.prototype.allowedEvents_ = { +}; + +vjs.EventEmitter.prototype.on = function(type, fn) { + // Remove the addEventListener alias before calling vjs.on + // so we don't get into an infinite type loop + var ael = this.addEventListener; + this.addEventListener = Function.prototype; + vjs.on(this, type, fn); + this.addEventListener = ael; +}; +vjs.EventEmitter.prototype.addEventListener = vjs.EventEmitter.prototype.on; + +vjs.EventEmitter.prototype.off = function(type, fn) { + vjs.off(this, type, fn); +}; +vjs.EventEmitter.prototype.removeEventListener = vjs.EventEmitter.prototype.off; + +vjs.EventEmitter.prototype.one = function(type, fn) { + vjs.one(this, type, fn); }; +vjs.EventEmitter.prototype.trigger = function(event) { + var type = event.type || event; + + if (typeof event === 'string') { + event = { + type: type + }; + } + event = vjs.fixEvent(event); + + if (this.allowedEvents_[type] && this['on' + type]) { + this['on' + type](event); + } + vjs.trigger(this, event); +}; +// The standard DOM EventTarget.dispatchEvent() is aliased to trigger() +vjs.EventEmitter.prototype.dispatchEvent = vjs.EventEmitter.prototype.trigger; /** * @fileoverview Player Component - Base class for all UI objects * @@ -1401,8 +1813,14 @@ vjs.Component = vjs.CoreObject.extend({ // Updated options with supplied options options = this.options(options); - // Get ID from options, element, or create using player ID and unique ID - this.id_ = options['id'] || ((options['el'] && options['el']['id']) ? options['el']['id'] : player.id() + '_component_' + vjs.guid++ ); + // Get ID from options or options element if one is supplied + this.id_ = options['id'] || (options['el'] && options['el']['id']); + + // If there was no ID from the options, generate one + if (!this.id_) { + // Don't require the player ID function in the case of mock players + this.id_ = ((player.id && player.id()) || 'no_player') + '_component_' + vjs.guid++; + } this.name_ = options['name'] || null; @@ -1549,6 +1967,15 @@ vjs.Component.prototype.createEl = function(tagName, attributes){ return vjs.createEl(tagName, attributes); }; +vjs.Component.prototype.localize = function(string){ + var lang = this.player_.language(), + languages = this.player_.languages(); + if (languages && languages[lang] && languages[lang][string]) { + return languages[lang][string]; + } + return string; +}; + /** * Get the component's DOM element * @@ -1699,17 +2126,17 @@ vjs.Component.prototype.getChild = function(name){ * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility} */ vjs.Component.prototype.addChild = function(child, options){ - var component, componentClass, componentName, componentId; + var component, componentClass, componentName; - // If string, create new component with options + // If child is a string, create new component with options if (typeof child === 'string') { - componentName = child; // Make sure options is at least an empty object to protect against errors options = options || {}; - // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.) + // If no componentClass in options, assume componentClass is the name lowercased + // (e.g. playButton) componentClass = options['componentClass'] || vjs.capitalize(componentName); // Set name through options @@ -1774,8 +2201,8 @@ vjs.Component.prototype.removeChild = function(component){ if (!childFound) return; - this.childIndex_[component.id] = null; - this.childNameIndex_[component.name] = null; + this.childIndex_[component.id()] = null; + this.childNameIndex_[component.name()] = null; var compEl = component.el(); if (compEl && compEl.parentNode === this.contentEl()) { @@ -1793,31 +2220,77 @@ vjs.Component.prototype.removeChild = function(component){ * myChildOption: true * } * } + * + * // Or when creating the component + * var myComp = new MyComponent(player, { + * children: { + * myChildComponent: { + * myChildOption: true + * } + * } + * }); + * + * The children option can also be an Array of child names or + * child options objects (that also include a 'name' key). + * + * var myComp = new MyComponent(player, { + * children: [ + * 'button', + * { + * name: 'button', + * someOtherOption: true + * } + * ] + * }); + * */ vjs.Component.prototype.initChildren = function(){ - var options = this.options_; - - if (options && options['children']) { - var self = this; + var parent, parentOptions, children, child, name, opts, handleAdd; + + parent = this; + parentOptions = parent.options(); + children = parentOptions['children']; + + if (children) { + handleAdd = function(name, opts){ + // Allow options for children to be set at the parent options + // e.g. videojs(id, { controlBar: false }); + // instead of videojs(id, { children: { controlBar: false }); + if (parentOptions[name] !== undefined) { + opts = parentOptions[name]; + } - // Loop through components and add them to the player - vjs.obj.each(options['children'], function(name, opts){ // Allow for disabling default components // e.g. vjs.options['children']['posterImage'] = false if (opts === false) return; - // Allow waiting to add components until a specific event is called - var tempAdd = function(){ - // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy. - self[name] = self.addChild(name, opts); - }; + // Create and add the child component. + // Add a direct reference to the child by name on the parent instance. + // If two of the same component are used, different names should be supplied + // for each + parent[name] = parent.addChild(name, opts); + }; - if (opts['loadEvent']) { - // this.one(opts.loadEvent, tempAdd) - } else { - tempAdd(); + // Allow for an array of children details to passed in the options + if (vjs.obj.isArray(children)) { + for (var i = 0; i < children.length; i++) { + child = children[i]; + + if (typeof child == 'string') { + // ['myComponent'] + name = child; + opts = {}; + } else { + // [{ name: 'myComponent', otherOption: true }] + name = child.name; + opts = child; + } + + handleAdd(name, opts); } - }); + } else { + vjs.obj.each(children, handleAdd); + } } }; @@ -1839,46 +2312,169 @@ vjs.Component.prototype.buildCSSClass = function(){ * Add an event listener to this component's element * * var myFunc = function(){ - * var myPlayer = this; + * var myComponent = this; * // Do something when the event is fired * }; * - * myPlayer.on("eventName", myFunc); + * myComponent.on('eventType', myFunc); + * + * The context of myFunc will be myComponent unless previously bound. * - * The context will be the component. + * Alternatively, you can add a listener to another element or component. * - * @param {String} type The event type e.g. 'click' - * @param {Function} fn The event listener - * @return {vjs.Component} self + * myComponent.on(otherElement, 'eventName', myFunc); + * myComponent.on(otherComponent, 'eventName', myFunc); + * + * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)` + * and `otherComponent.on('eventName', myFunc)` is that this way the listeners + * will be automatically cleaned up when either component is disposed. + * It will also bind myComponent as the context of myFunc. + * + * **NOTE**: When using this on elements in the page other than window + * and document (both permanent), if you remove the element from the DOM + * you need to call `vjs.trigger(el, 'dispose')` on it to clean up + * references to it and allow the browser to garbage collect it. + * + * @param {String|vjs.Component} first The event type or other component + * @param {Function|String} second The event handler or event type + * @param {Function} third The event handler + * @return {vjs.Component} self */ -vjs.Component.prototype.on = function(type, fn){ - vjs.on(this.el_, type, vjs.bind(this, fn)); +vjs.Component.prototype.on = function(first, second, third){ + var target, type, fn, removeOnDispose, cleanRemover, thisComponent; + + if (typeof first === 'string' || vjs.obj.isArray(first)) { + vjs.on(this.el_, first, vjs.bind(this, second)); + + // Targeting another component or element + } else { + target = first; + type = second; + fn = vjs.bind(this, third); + thisComponent = this; + + // When this component is disposed, remove the listener from the other component + removeOnDispose = function(){ + thisComponent.off(target, type, fn); + }; + // Use the same function ID so we can remove it later it using the ID + // of the original listener + removeOnDispose.guid = fn.guid; + this.on('dispose', removeOnDispose); + + // If the other component is disposed first we need to clean the reference + // to the other component in this component's removeOnDispose listener + // Otherwise we create a memory leak. + cleanRemover = function(){ + thisComponent.off('dispose', removeOnDispose); + }; + // Add the same function ID so we can easily remove it later + cleanRemover.guid = fn.guid; + + // Check if this is a DOM node + if (first.nodeName) { + // Add the listener to the other element + vjs.on(target, type, fn); + vjs.on(target, 'dispose', cleanRemover); + + // Should be a component + // Not using `instanceof vjs.Component` because it makes mock players difficult + } else if (typeof first.on === 'function') { + // Add the listener to the other component + target.on(type, fn); + target.on('dispose', cleanRemover); + } + } + return this; }; /** - * Remove an event listener from the component's element + * Remove an event listener from this component's element + * + * myComponent.off('eventType', myFunc); * - * myComponent.off("eventName", myFunc); + * If myFunc is excluded, ALL listeners for the event type will be removed. + * If eventType is excluded, ALL listeners will be removed from the component. * - * @param {String=} type Event type. Without type it will remove all listeners. - * @param {Function=} fn Event listener. Without fn it will remove all listeners for a type. + * Alternatively you can use `off` to remove listeners that were added to other + * elements or components using `myComponent.on(otherComponent...`. + * In this case both the event type and listener function are REQUIRED. + * + * myComponent.off(otherElement, 'eventType', myFunc); + * myComponent.off(otherComponent, 'eventType', myFunc); + * + * @param {String=|vjs.Component} first The event type or other component + * @param {Function=|String} second The listener function or event type + * @param {Function=} third The listener for other component * @return {vjs.Component} */ -vjs.Component.prototype.off = function(type, fn){ - vjs.off(this.el_, type, fn); +vjs.Component.prototype.off = function(first, second, third){ + var target, otherComponent, type, fn, otherEl; + + if (!first || typeof first === 'string' || vjs.obj.isArray(first)) { + vjs.off(this.el_, first, second); + } else { + target = first; + type = second; + // Ensure there's at least a guid, even if the function hasn't been used + fn = vjs.bind(this, third); + + // Remove the dispose listener on this component, + // which was given the same guid as the event listener + this.off('dispose', fn); + + if (first.nodeName) { + // Remove the listener + vjs.off(target, type, fn); + // Remove the listener for cleaning the dispose listener + vjs.off(target, 'dispose', fn); + } else { + target.off(type, fn); + target.off('dispose', fn); + } + } + return this; }; /** * Add an event listener to be triggered only once and then removed * - * @param {String} type Event type - * @param {Function} fn Event listener + * myComponent.one('eventName', myFunc); + * + * Alternatively you can add a listener to another element or component + * that will be triggered only once. + * + * myComponent.one(otherElement, 'eventName', myFunc); + * myComponent.one(otherComponent, 'eventName', myFunc); + * + * @param {String|vjs.Component} first The event type or other component + * @param {Function|String} second The listener function or event type + * @param {Function=} third The listener function for other component * @return {vjs.Component} */ -vjs.Component.prototype.one = function(type, fn) { - vjs.one(this.el_, type, vjs.bind(this, fn)); +vjs.Component.prototype.one = function(first, second, third) { + var target, type, fn, thisComponent, newFunc; + + if (typeof first === 'string' || vjs.obj.isArray(first)) { + vjs.one(this.el_, first, vjs.bind(this, second)); + } else { + target = first; + type = second; + fn = vjs.bind(this, third); + thisComponent = this; + + newFunc = function(){ + thisComponent.off(target, type, newFunc); + fn.apply(this, arguments); + }; + // Keep the same function ID so we can remove it later + newFunc.guid = fn.guid; + + this.on(target, type, newFunc); + } + return this; }; @@ -1886,13 +2482,13 @@ vjs.Component.prototype.one = function(type, fn) { * Trigger an event on an element * * myComponent.trigger('eventName'); + * myComponent.trigger({'type':'eventName'}); * - * @param {String} type The event type to trigger, e.g. 'click' - * @param {Event|Object} event The event object to be passed to the listener - * @return {vjs.Component} self + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute + * @return {vjs.Component} self */ -vjs.Component.prototype.trigger = function(type, event){ - vjs.trigger(this.el_, type, event); +vjs.Component.prototype.trigger = function(event){ + vjs.trigger(this.el_, event); return this; }; @@ -1912,7 +2508,7 @@ vjs.Component.prototype.isReady_; * * Allows for delaying ready. Override on a sub class prototype. * If you set this.isReadyOnInitFinish_ it will affect all components. - * Specially used when waiting for the Flash player to asynchrnously load. + * Specially used when waiting for the Flash player to asynchronously load. * * @type {Boolean} * @private @@ -1930,7 +2526,7 @@ vjs.Component.prototype.readyQueue_; /** * Bind a listener to the component's ready state * - * Different from event listeners in that if the ready event has already happend + * Different from event listeners in that if the ready event has already happened * it will trigger the function immediately. * * @param {Function} fn Ready listener @@ -1977,6 +2573,16 @@ vjs.Component.prototype.triggerReady = function(){ /* Display ============================================================================= */ +/** + * Check if a component's element has a CSS class name + * + * @param {String} classToCheck Classname to check + * @return {vjs.Component} + */ +vjs.Component.prototype.hasClass = function(classToCheck){ + return vjs.hasClass(this.el_, classToCheck); +}; + /** * Add a CSS class name to the component's element * @@ -2005,7 +2611,7 @@ vjs.Component.prototype.removeClass = function(classToRemove){ * @return {vjs.Component} */ vjs.Component.prototype.show = function(){ - this.el_.style.display = 'block'; + this.removeClass('vjs-hidden'); return this; }; @@ -2015,7 +2621,7 @@ vjs.Component.prototype.show = function(){ * @return {vjs.Component} */ vjs.Component.prototype.hide = function(){ - this.el_.style.display = 'none'; + this.addClass('vjs-hidden'); return this; }; @@ -2046,7 +2652,7 @@ vjs.Component.prototype.unlockShowing = function(){ /** * Disable component by making it unshowable * - * Currently private because we're movign towards more css-based states. + * Currently private because we're moving towards more css-based states. * @private */ vjs.Component.prototype.disable = function(){ @@ -2120,6 +2726,9 @@ vjs.Component.prototype.dimensions = function(width, height){ */ vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ if (num !== undefined) { + if (num === null || vjs.isNaN(num)) { + num = 0; + } // Check if using css width/height (% or px) and adjust if ((''+num).indexOf('%') !== -1 || (''+num).indexOf('px') !== -1) { @@ -2181,41 +2790,71 @@ vjs.Component.prototype.onResize; * * This is used to support toggling the controls through a tap on the video. * - * We're requireing them to be enabled because otherwise every component would + * We're requiring them to be enabled because otherwise every component would * have this extra overhead unnecessarily, on mobile devices where extra * overhead is especially bad. * @private */ vjs.Component.prototype.emitTapEvents = function(){ - var touchStart, touchTime, couldBeTap, noTap; + var touchStart, firstTouch, touchTime, couldBeTap, noTap, + xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold; // Track the start time so we can determine how long the touch lasted touchStart = 0; + firstTouch = null; + + // Maximum movement allowed during a touch event to still be considered a tap + // Other popular libs use anywhere from 2 (hammer.js) to 15, so 10 seems like a nice, round number. + tapMovementThreshold = 10; + + // The maximum length a touch can be while still being considered a tap + touchTimeThreshold = 200; this.on('touchstart', function(event) { - // Record start time so we can detect a tap vs. "touch and hold" - touchStart = new Date().getTime(); - // Reset couldBeTap tracking - couldBeTap = true; + // If more than one finger, don't consider treating this as a click + if (event.touches.length === 1) { + firstTouch = vjs.obj.copy(event.touches[0]); + // Record start time so we can detect a tap vs. "touch and hold" + touchStart = new Date().getTime(); + // Reset couldBeTap tracking + couldBeTap = true; + } + }); + + this.on('touchmove', function(event) { + // If more than one finger, don't consider treating this as a click + if (event.touches.length > 1) { + couldBeTap = false; + } else if (firstTouch) { + // Some devices will throw touchmoves for all but the slightest of taps. + // So, if we moved only a small distance, this could still be a tap + xdiff = event.touches[0].pageX - firstTouch.pageX; + ydiff = event.touches[0].pageY - firstTouch.pageY; + touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + if (touchDistance > tapMovementThreshold) { + couldBeTap = false; + } + } }); noTap = function(){ couldBeTap = false; }; // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s - this.on('touchmove', noTap); this.on('touchleave', noTap); this.on('touchcancel', noTap); // When the touch ends, measure how long it took and trigger the appropriate // event this.on('touchend', function(event) { + firstTouch = null; // Proceed only if the touchmove/leave/cancel event didn't happen if (couldBeTap === true) { // Measure how long the touch lasted touchTime = new Date().getTime() - touchStart; - // The touch needs to be quick in order to consider it a tap - if (touchTime < 250) { + // Make sure the touch was less than the threshold to be considered a tap + if (touchTime < touchTimeThreshold) { + event.preventDefault(); // Don't let browser turn this into a click this.trigger('tap'); // It may be good to copy the touchend event object and change the // type to tap, if the other event properties aren't exact after @@ -2251,6 +2890,11 @@ vjs.Component.prototype.emitTapEvents = function(){ vjs.Component.prototype.enableTouchActivity = function() { var report, touchHolding, touchEnd; + // Don't continue if the root player doesn't support reporting user activity + if (!this.player().reportUserActivity) { + return; + } + // listener for reporting that the user is active report = vjs.bind(this.player(), this.player().reportUserActivity); @@ -2259,15 +2903,15 @@ vjs.Component.prototype.enableTouchActivity = function() { // For as long as the they are touching the device or have their mouse down, // we consider them active even if they're not moving their finger or mouse. // So we want to continue to update that they are active - clearInterval(touchHolding); + this.clearInterval(touchHolding); // report at the same interval as activityCheck - touchHolding = setInterval(report, 250); + touchHolding = this.setInterval(report, 250); }); touchEnd = function(event) { report(); // stop the interval that maintains activity if the touch is holding - clearInterval(touchHolding); + this.clearInterval(touchHolding); }; this.on('touchmove', report); @@ -2275,6 +2919,83 @@ vjs.Component.prototype.enableTouchActivity = function() { this.on('touchcancel', touchEnd); }; +/** + * Creates timeout and sets up disposal automatically. + * @param {Function} fn The function to run after the timeout. + * @param {Number} timeout Number of ms to delay before executing specified function. + * @return {Number} Returns the timeout ID + */ +vjs.Component.prototype.setTimeout = function(fn, timeout) { + fn = vjs.bind(this, fn); + + // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. + var timeoutId = setTimeout(fn, timeout); + + var disposeFn = function() { + this.clearTimeout(timeoutId); + }; + + disposeFn.guid = 'vjs-timeout-'+ timeoutId; + + this.on('dispose', disposeFn); + + return timeoutId; +}; + + +/** + * Clears a timeout and removes the associated dispose listener + * @param {Number} timeoutId The id of the timeout to clear + * @return {Number} Returns the timeout ID + */ +vjs.Component.prototype.clearTimeout = function(timeoutId) { + clearTimeout(timeoutId); + + var disposeFn = function(){}; + disposeFn.guid = 'vjs-timeout-'+ timeoutId; + + this.off('dispose', disposeFn); + + return timeoutId; +}; + +/** + * Creates an interval and sets up disposal automatically. + * @param {Function} fn The function to run every N seconds. + * @param {Number} interval Number of ms to delay before executing specified function. + * @return {Number} Returns the interval ID + */ +vjs.Component.prototype.setInterval = function(fn, interval) { + fn = vjs.bind(this, fn); + + var intervalId = setInterval(fn, interval); + + var disposeFn = function() { + this.clearInterval(intervalId); + }; + + disposeFn.guid = 'vjs-interval-'+ intervalId; + + this.on('dispose', disposeFn); + + return intervalId; +}; + +/** + * Clears an interval and removes the associated dispose listener + * @param {Number} intervalId The id of the interval to clear + * @return {Number} Returns the interval ID + */ +vjs.Component.prototype.clearInterval = function(intervalId) { + clearInterval(intervalId); + + var disposeFn = function(){}; + disposeFn.guid = 'vjs-interval-'+ intervalId; + + this.off('dispose', disposeFn); + + return intervalId; +}; /* Button - Base class for all buttons ================================================================================ */ /** @@ -2292,23 +3013,9 @@ vjs.Button = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); - var touchstart = false; - this.on('touchstart', function(event) { - // Stop click and other mouse events from triggering also - event.preventDefault(); - touchstart = true; - }); - this.on('touchmove', function() { - touchstart = false; - }); - var self = this; - this.on('touchend', function(event) { - if (touchstart) { - self.onClick(event); - } - event.preventDefault(); - }); + this.emitTapEvents(); + this.on('tap', this.onClick); this.on('click', this.onClick); this.on('focus', this.onFocus); this.on('blur', this.onBlur); @@ -2316,16 +3023,34 @@ vjs.Button = vjs.Component.extend({ }); vjs.Button.prototype.createEl = function(type, props){ + var el; + // Add standard Aria and Tabindex info props = vjs.obj.merge({ className: this.buildCSSClass(), - innerHTML: '
' + (this.buttonText || 'Need Text') + '
', 'role': 'button', 'aria-live': 'polite', // let the screen reader user know that the text of the button may change tabIndex: 0 }, props); - return vjs.Component.prototype.createEl.call(this, type, props); + el = vjs.Component.prototype.createEl.call(this, type, props); + + // if innerHTML hasn't been overridden (bigPlayButton), add content elements + if (!props.innerHTML) { + this.contentEl_ = vjs.createEl('div', { + className: 'vjs-control-content' + }); + + this.controlText_ = vjs.createEl('span', { + className: 'vjs-control-text', + innerHTML: this.localize(this.buttonText) || 'Need Text' + }); + + this.contentEl_.appendChild(this.controlText_); + el.appendChild(this.contentEl_); + } + + return el; }; vjs.Button.prototype.buildCSSClass = function(){ @@ -2338,7 +3063,7 @@ vjs.Button.prototype.onClick = function(){}; // Focus - Add keyboard functionality to element vjs.Button.prototype.onFocus = function(){ - vjs.on(document, 'keyup', vjs.bind(this, this.onKeyPress)); + vjs.on(document, 'keydown', vjs.bind(this, this.onKeyPress)); }; // KeyPress (document level) - Trigger click when keys are pressed @@ -2352,7 +3077,7 @@ vjs.Button.prototype.onKeyPress = function(event){ // Blur - Remove keyboard triggers vjs.Button.prototype.onBlur = function(){ - vjs.off(document, 'keyup', vjs.bind(this, this.onKeyPress)); + vjs.off(document, 'keydown', vjs.bind(this, this.onKeyPress)); }; /* Slider ================================================================================ */ @@ -2372,22 +3097,14 @@ vjs.Slider = vjs.Component.extend({ this.bar = this.getChild(this.options_['barName']); this.handle = this.getChild(this.options_['handleName']); - player.on(this.playerEvent, vjs.bind(this, this.update)); - this.on('mousedown', this.onMouseDown); this.on('touchstart', this.onMouseDown); this.on('focus', this.onFocus); this.on('blur', this.onBlur); this.on('click', this.onClick); - this.player_.on('controlsvisible', vjs.bind(this, this.update)); - - // This is actually to fix the volume handle position. http://twitter.com/#!/gerritvanaaken/status/159046254519787520 - // this.player_.one('timeupdate', vjs.bind(this, this.update)); - - player.ready(vjs.bind(this, this.update)); - - this.boundEvents = {}; + this.on(player, 'controlsvisible', this.update); + this.on(player, this.playerEvent, this.update); } }); @@ -2409,24 +3126,27 @@ vjs.Slider.prototype.createEl = function(type, props) { vjs.Slider.prototype.onMouseDown = function(event){ event.preventDefault(); vjs.blockTextSelection(); + this.addClass('vjs-sliding'); - this.boundEvents.move = vjs.bind(this, this.onMouseMove); - this.boundEvents.end = vjs.bind(this, this.onMouseUp); - - vjs.on(document, 'mousemove', this.boundEvents.move); - vjs.on(document, 'mouseup', this.boundEvents.end); - vjs.on(document, 'touchmove', this.boundEvents.move); - vjs.on(document, 'touchend', this.boundEvents.end); + this.on(document, 'mousemove', this.onMouseMove); + this.on(document, 'mouseup', this.onMouseUp); + this.on(document, 'touchmove', this.onMouseMove); + this.on(document, 'touchend', this.onMouseUp); this.onMouseMove(event); }; +// To be overridden by a subclass +vjs.Slider.prototype.onMouseMove = function(){}; + vjs.Slider.prototype.onMouseUp = function() { vjs.unblockTextSelection(); - vjs.off(document, 'mousemove', this.boundEvents.move, false); - vjs.off(document, 'mouseup', this.boundEvents.end, false); - vjs.off(document, 'touchmove', this.boundEvents.move, false); - vjs.off(document, 'touchend', this.boundEvents.end, false); + this.removeClass('vjs-sliding'); + + this.off(document, 'mousemove', this.onMouseMove); + this.off(document, 'mouseup', this.onMouseUp); + this.off(document, 'touchmove', this.onMouseMove); + this.off(document, 'touchend', this.onMouseUp); this.update(); }; @@ -2446,7 +3166,12 @@ vjs.Slider.prototype.update = function(){ bar = this.bar; // Protect against no duration and other division issues - if (isNaN(progress)) { progress = 0; } + if (typeof progress !== 'number' || + progress !== progress || + progress < 0 || + progress === Infinity) { + progress = 0; + } barProgress = progress; @@ -2478,7 +3203,9 @@ vjs.Slider.prototype.update = function(){ } // Set the new bar width - bar.el().style.width = vjs.round(barProgress * 100, 2) + '%'; + if (bar) { + bar.el().style.width = vjs.round(barProgress * 100, 2) + '%'; + } }; vjs.Slider.prototype.calculateDistance = function(event){ @@ -2489,7 +3216,7 @@ vjs.Slider.prototype.calculateDistance = function(event){ boxW = boxH = el.offsetWidth; handle = this.handle; - if (this.options_.vertical) { + if (this.options()['vertical']) { boxY = box.top; if (event.changedTouches) { @@ -2531,21 +3258,21 @@ vjs.Slider.prototype.calculateDistance = function(event){ }; vjs.Slider.prototype.onFocus = function(){ - vjs.on(document, 'keyup', vjs.bind(this, this.onKeyPress)); + this.on(document, 'keydown', this.onKeyPress); }; vjs.Slider.prototype.onKeyPress = function(event){ - if (event.which == 37) { // Left Arrow + if (event.which == 37 || event.which == 40) { // Left and Down Arrows event.preventDefault(); this.stepBack(); - } else if (event.which == 39) { // Right Arrow + } else if (event.which == 38 || event.which == 39) { // Up and Right Arrows event.preventDefault(); this.stepForward(); } }; vjs.Slider.prototype.onBlur = function(){ - vjs.off(document, 'keyup', vjs.bind(this, this.onKeyPress)); + this.off(document, 'keydown', this.onKeyPress); }; /** @@ -2652,7 +3379,7 @@ vjs.MenuItem = vjs.Button.extend({ vjs.MenuItem.prototype.createEl = function(type, props){ return vjs.Button.prototype.createEl.call(this, 'li', vjs.obj.merge({ className: 'vjs-menu-item', - innerHTML: this.options_['label'] + innerHTML: this.localize(this.options_['label']) }, props)); }; @@ -2689,22 +3416,31 @@ vjs.MenuButton = vjs.Button.extend({ init: function(player, options){ vjs.Button.call(this, player, options); - this.menu = this.createMenu(); + this.update(); - // Add list to element - this.addChild(this.menu); - - // Automatically hide empty menu buttons - if (this.items && this.items.length === 0) { - this.hide(); - } - - this.on('keyup', this.onKeyPress); + this.on('keydown', this.onKeyPress); this.el_.setAttribute('aria-haspopup', true); this.el_.setAttribute('role', 'button'); } }); +vjs.MenuButton.prototype.update = function() { + var menu = this.createMenu(); + + if (this.menu) { + this.removeChild(this.menu); + } + + this.menu = menu; + this.addChild(menu); + + if (this.items && this.items.length === 0) { + this.hide(); + } else if (this.items && this.items.length > 1) { + this.show(); + } +}; + /** * Track the state of the menu button * @type {Boolean} @@ -2717,9 +3453,9 @@ vjs.MenuButton.prototype.createMenu = function(){ // Add a title list item to the top if (this.options().title) { - menu.el().appendChild(vjs.createEl('li', { + menu.contentEl().appendChild(vjs.createEl('li', { className: 'vjs-menu-title', - innerHTML: vjs.capitalize(this.kind_), + innerHTML: vjs.capitalize(this.options().title), tabindex: -1 })); } @@ -2770,7 +3506,6 @@ vjs.MenuButton.prototype.onClick = function(){ }; vjs.MenuButton.prototype.onKeyPress = function(event){ - event.preventDefault(); // Check for space bar (32) or enter (13) keys if (event.which == 32 || event.which == 13) { @@ -2779,11 +3514,13 @@ vjs.MenuButton.prototype.onKeyPress = function(event){ } else { this.pressButton(); } + event.preventDefault(); // Check for escape (27) key } else if (event.which == 27){ if (this.buttonPressed_){ this.unpressButton(); } + event.preventDefault(); } }; @@ -2801,7 +3538,157 @@ vjs.MenuButton.prototype.unpressButton = function(){ this.menu.unlockShowing(); this.el_.setAttribute('aria-pressed', false); }; +/** + * Custom MediaError to mimic the HTML5 MediaError + * @param {Number} code The media error code + */ +vjs.MediaError = function(code){ + if (typeof code === 'number') { + this.code = code; + } else if (typeof code === 'string') { + // default code is zero, so this is a custom error + this.message = code; + } else if (typeof code === 'object') { // object + vjs.obj.merge(this, code); + } + + if (!this.message) { + this.message = vjs.MediaError.defaultMessages[this.code] || ''; + } +}; + +/** + * The error code that refers two one of the defined + * MediaError types + * @type {Number} + */ +vjs.MediaError.prototype.code = 0; + +/** + * An optional message to be shown with the error. + * Message is not part of the HTML5 video spec + * but allows for more informative custom errors. + * @type {String} + */ +vjs.MediaError.prototype.message = ''; + +/** + * An optional status code that can be set by plugins + * to allow even more detail about the error. + * For example the HLS plugin might provide the specific + * HTTP status code that was returned when the error + * occurred, then allowing a custom error overlay + * to display more information. + * @type {[type]} + */ +vjs.MediaError.prototype.status = null; + +vjs.MediaError.errorTypes = [ + 'MEDIA_ERR_CUSTOM', // = 0 + 'MEDIA_ERR_ABORTED', // = 1 + 'MEDIA_ERR_NETWORK', // = 2 + 'MEDIA_ERR_DECODE', // = 3 + 'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4 + 'MEDIA_ERR_ENCRYPTED' // = 5 +]; + +vjs.MediaError.defaultMessages = { + 1: 'You aborted the video playback', + 2: 'A network error caused the video download to fail part-way.', + 3: 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.', + 4: 'The video could not be loaded, either because the server or network failed or because the format is not supported.', + 5: 'The video is encrypted and we do not have the keys to decrypt it.' +}; + +// Add types as properties on MediaError +// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; +for (var errNum = 0; errNum < vjs.MediaError.errorTypes.length; errNum++) { + vjs.MediaError[vjs.MediaError.errorTypes[errNum]] = errNum; + // values should be accessible on both the class and instance + vjs.MediaError.prototype[vjs.MediaError.errorTypes[errNum]] = errNum; +} +(function(){ + var apiMap, specApi, browserApi, i; + /** + * Store the browser-specific methods for the fullscreen API + * @type {Object|undefined} + * @private + */ + vjs.browser.fullscreenAPI; + + // browser API methods + // map approach from Screenful.js - https://github.com/sindresorhus/screenfull.js + apiMap = [ + // Spec: https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html + [ + 'requestFullscreen', + 'exitFullscreen', + 'fullscreenElement', + 'fullscreenEnabled', + 'fullscreenchange', + 'fullscreenerror' + ], + // WebKit + [ + 'webkitRequestFullscreen', + 'webkitExitFullscreen', + 'webkitFullscreenElement', + 'webkitFullscreenEnabled', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + ], + // Old WebKit (Safari 5.1) + [ + 'webkitRequestFullScreen', + 'webkitCancelFullScreen', + 'webkitCurrentFullScreenElement', + 'webkitCancelFullScreen', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + ], + // Mozilla + [ + 'mozRequestFullScreen', + 'mozCancelFullScreen', + 'mozFullScreenElement', + 'mozFullScreenEnabled', + 'mozfullscreenchange', + 'mozfullscreenerror' + ], + // Microsoft + [ + 'msRequestFullscreen', + 'msExitFullscreen', + 'msFullscreenElement', + 'msFullscreenEnabled', + 'MSFullscreenChange', + 'MSFullscreenError' + ] + ]; + + specApi = apiMap[0]; + + // determine the supported set of functions + for (i=0; i @@ -2839,6 +3726,9 @@ vjs.Player = vjs.Component.extend({ // Make sure tag ID exists tag.id = tag.id || 'vjs_video_' + vjs.guid++; + // Store the tag attributes used to restore html5 element + this.tagAttributes = tag && vjs.getElementAttributes(tag); + // Set Options // The options argument overrides options set in the video tag // which overrides globally set options. @@ -2846,13 +3736,20 @@ vjs.Player = vjs.Component.extend({ // (tag must exist before Player) options = vjs.obj.merge(this.getTagSettings(tag), options); + // Update Current Language + this.language_ = options['language'] || vjs.options['language']; + + // Update Supported Languages + this.languages_ = options['languages'] || vjs.options['languages']; + // Cache for video property values. this.cache_ = {}; // Set poster - this.poster_ = options['poster']; + this.poster_ = options['poster'] || ''; + // Set controls - this.controls_ = options['controls']; + this.controls_ = !!options['controls']; // Original tag settings stored in options // now remove immediately so native controls don't flash. // May be turned back on by HTML5 tech if nativeControlsForTouch is true @@ -2862,6 +3759,9 @@ vjs.Player = vjs.Component.extend({ // see enableTouchActivity in Component options.reportTouchActivity = false; + // Set isAudio based on whether or not an audio tag was used + this.isAudio(this.tag.nodeName.toLowerCase() === 'audio'); + // Run base component initializing with new options. // Builds the element through createEl() // Inits and embeds any child components in opts @@ -2875,35 +3775,16 @@ vjs.Player = vjs.Component.extend({ this.addClass('vjs-controls-disabled'); } + if (this.isAudio()) { + this.addClass('vjs-audio'); + } + // TODO: Make this smarter. Toggle user state between touching/mousing // using events, since devices can have both touch and mouse events. // if (vjs.TOUCH_ENABLED) { // this.addClass('vjs-touch-enabled'); // } - // Firstplay event implimentation. Not sold on the event yet. - // Could probably just check currentTime==0? - this.one('play', function(e){ - var fpEvent = { type: 'firstplay', target: this.el_ }; - // Using vjs.trigger so we can check if default was prevented - var keepGoing = vjs.trigger(this.el_, fpEvent); - - if (!keepGoing) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - } - }); - - this.on('ended', this.onEnded); - this.on('play', this.onPlay); - this.on('firstplay', this.onFirstPlay); - this.on('pause', this.onPause); - this.on('progress', this.onProgress); - this.on('durationchange', this.onDurationChange); - this.on('error', this.onError); - this.on('fullscreenchange', this.onFullscreenChange); - // Make player easily findable by ID vjs.players[this.id_] = this; @@ -2917,6 +3798,41 @@ vjs.Player = vjs.Component.extend({ } }); +/** + * The player's stored language code + * + * @type {String} + * @private + */ +vjs.Player.prototype.language_; + +/** + * The player's language code + * @param {String} languageCode The locale string + * @return {String} The locale string when getting + * @return {vjs.Player} self, when setting + */ +vjs.Player.prototype.language = function (languageCode) { + if (languageCode === undefined) { + return this.language_; + } + + this.language_ = languageCode; + return this; +}; + +/** + * The player's stored language dictionary + * + * @type {Object} + * @private + */ +vjs.Player.prototype.languages_; + +vjs.Player.prototype.languages = function(){ + return this.languages_; +}; + /** * Player instance options, surfaced using vjs.options * vjs.options = vjs.Player.prototype.options_ @@ -2946,10 +3862,6 @@ vjs.Player.prototype.dispose = function(){ if (this.tag && this.tag['player']) { this.tag['player'] = null; } if (this.el_ && this.el_['player']) { this.el_['player'] = null; } - // Ensure that tracking progress and time progress will stop and plater deleted - this.stopTrackingProgress(); - this.stopTrackingCurrentTime(); - if (this.tech) { this.tech.dispose(); } // Component dispose @@ -2957,12 +3869,24 @@ vjs.Player.prototype.dispose = function(){ }; vjs.Player.prototype.getTagSettings = function(tag){ - var options = { - 'sources': [], - 'tracks': [] - }; + var tagOptions, + dataSetup, + options = { + 'sources': [], + 'tracks': [] + }; - vjs.obj.merge(options, vjs.getAttributeValues(tag)); + tagOptions = vjs.getElementAttributes(tag); + dataSetup = tagOptions['data-setup']; + + // Check if data-setup attr exists. + if (dataSetup !== null){ + // Parse options JSON + // If empty string, make it a parsable json object. + vjs.obj.merge(tagOptions, vjs.JSON.parse(dataSetup || '{}')); + } + + vjs.obj.merge(options, tagOptions); // Get tag children settings if (tag.hasChildNodes()) { @@ -2975,9 +3899,9 @@ vjs.Player.prototype.getTagSettings = function(tag){ // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ childName = child.nodeName.toLowerCase(); if (childName === 'source') { - options['sources'].push(vjs.getAttributeValues(child)); + options['sources'].push(vjs.getElementAttributes(child)); } else if (childName === 'track') { - options['tracks'].push(vjs.getAttributeValues(child)); + options['tracks'].push(vjs.getElementAttributes(child)); } } } @@ -2986,40 +3910,27 @@ vjs.Player.prototype.getTagSettings = function(tag){ }; vjs.Player.prototype.createEl = function(){ - var el = this.el_ = vjs.Component.prototype.createEl.call(this, 'div'); - var tag = this.tag; + var + el = this.el_ = vjs.Component.prototype.createEl.call(this, 'div'), + tag = this.tag, + attrs; // Remove width/height attrs from tag so CSS can make it 100% width/height tag.removeAttribute('width'); tag.removeAttribute('height'); - // Empty video tag tracks so the built-in player doesn't use them also. - // This may not be fast enough to stop HTML5 browsers from reading the tags - // so we'll need to turn off any default tracks if we're manually doing - // captions and subtitles. videoElement.textTracks - if (tag.hasChildNodes()) { - var nodes, nodesLength, i, node, nodeName, removeNodes; - - nodes = tag.childNodes; - nodesLength = nodes.length; - removeNodes = []; - - while (nodesLength--) { - node = nodes[nodesLength]; - nodeName = node.nodeName.toLowerCase(); - if (nodeName === 'track') { - removeNodes.push(node); - } - } - for (i=0; i 0) { techOptions['startTime'] = this.cache_.currentTime; } @@ -3101,12 +4027,8 @@ vjs.Player.prototype.loadTech = function(techName, source){ vjs.Player.prototype.unloadTech = function(){ this.isReady_ = false; - this.tech.dispose(); - - // Turn off any manual progress or timeupdate tracking - if (this.manualProgress) { this.manualProgressOff(); } - if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } + this.tech.dispose(); this.tech = false; }; @@ -3125,99 +4047,57 @@ vjs.Player.prototype.unloadTech = function(){ // vjs.log('loadedTech') // }, -/* Fallbacks for unsupported event types -================================================================================ */ -// Manually trigger progress events based on changes to the buffered amount -// Many flash players and older HTML5 browsers don't send progress or progress-like events -vjs.Player.prototype.manualProgressOn = function(){ - this.manualProgress = true; - - // Trigger progress watching when a source begins loading - this.trackProgress(); +// /* Player event handlers (how the player reacts to certain events) +// ================================================================================ */ - // Watch for a native progress event call on the tech element - // In HTML5, some older versions don't support the progress event - // So we're assuming they don't, and turning off manual progress if they do. - // As opposed to doing user agent detection - this.tech.one('progress', function(){ +/** + * Fired when the user agent begins looking for media data + * @event loadstart + */ +vjs.Player.prototype.onLoadStart = function() { + // TODO: Update to use `emptied` event instead. See #1277. - // Update known progress support for this playback technology - this.features['progressEvents'] = true; + this.removeClass('vjs-ended'); - // Turn off manual progress tracking - this.player_.manualProgressOff(); - }); -}; + // reset the error state + this.error(null); -vjs.Player.prototype.manualProgressOff = function(){ - this.manualProgress = false; - this.stopTrackingProgress(); + // If it's already playing we want to trigger a firstplay event now. + // The firstplay event relies on both the play and loadstart events + // which can happen in any order for a new source + if (!this.paused()) { + this.trigger('firstplay'); + } else { + // reset the hasStarted state + this.hasStarted(false); + } }; -vjs.Player.prototype.trackProgress = function(){ +vjs.Player.prototype.hasStarted_ = false; - this.progressInterval = setInterval(vjs.bind(this, function(){ - // Don't trigger unless buffered amount is greater than last time - // log(this.cache_.bufferEnd, this.buffered().end(0), this.duration()) - /* TODO: update for multiple buffered regions */ - if (this.cache_.bufferEnd < this.buffered().end(0)) { - this.trigger('progress'); - } else if (this.bufferedPercent() == 1) { - this.stopTrackingProgress(); - this.trigger('progress'); // Last update +vjs.Player.prototype.hasStarted = function(hasStarted){ + if (hasStarted !== undefined) { + // only update if this is a new value + if (this.hasStarted_ !== hasStarted) { + this.hasStarted_ = hasStarted; + if (hasStarted) { + this.addClass('vjs-has-started'); + // trigger the firstplay event if this newly has played + this.trigger('firstplay'); + } else { + this.removeClass('vjs-has-started'); + } } - }), 500); + return this; + } + return this.hasStarted_; }; -vjs.Player.prototype.stopTrackingProgress = function(){ clearInterval(this.progressInterval); }; - -/*! Time Tracking -------------------------------------------------------------- */ -vjs.Player.prototype.manualTimeUpdatesOn = function(){ - this.manualTimeUpdates = true; - this.on('play', this.trackCurrentTime); - this.on('pause', this.stopTrackingCurrentTime); - // timeupdate is also called by .currentTime whenever current time is set - - // Watch for native timeupdate event - this.tech.one('timeupdate', function(){ - // Update known progress support for this playback technology - this.features['timeupdateEvents'] = true; - // Turn off manual progress tracking - this.player_.manualTimeUpdatesOff(); - }); -}; - -vjs.Player.prototype.manualTimeUpdatesOff = function(){ - this.manualTimeUpdates = false; - this.stopTrackingCurrentTime(); - this.off('play', this.trackCurrentTime); - this.off('pause', this.stopTrackingCurrentTime); -}; - -vjs.Player.prototype.trackCurrentTime = function(){ - if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } - this.currentTimeInterval = setInterval(vjs.bind(this, function(){ - this.trigger('timeupdate'); - }), 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 -}; - -// Turn off play progress tracking (when paused or dragging) -vjs.Player.prototype.stopTrackingCurrentTime = function(){ clearInterval(this.currentTimeInterval); }; - -// /* Player event handlers (how the player reacts to certain events) -// ================================================================================ */ - -/** - * Fired when the user agent begins looking for media data - * @event loadstart - */ -vjs.Player.prototype.onLoadStart; - -/** - * Fired when the player has initial duration and dimension information - * @event loadedmetadata - */ -vjs.Player.prototype.onLoadedMetaData; +/** + * Fired when the player has initial duration and dimension information + * @event loadedmetadata + */ +vjs.Player.prototype.onLoadedMetaData; /** * Fired when the player has downloaded data at the current playback position @@ -3236,8 +4116,46 @@ vjs.Player.prototype.onLoadedAllData; * @event play */ vjs.Player.prototype.onPlay = function(){ - vjs.removeClass(this.el_, 'vjs-paused'); - vjs.addClass(this.el_, 'vjs-playing'); + this.removeClass('vjs-ended'); + this.removeClass('vjs-paused'); + this.addClass('vjs-playing'); + + // hide the poster when the user hits play + // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play + this.hasStarted(true); +}; + +/** + * Fired whenever the media begins waiting + * @event waiting + */ +vjs.Player.prototype.onWaiting = function(){ + this.addClass('vjs-waiting'); +}; + +/** + * A handler for events that signal that waiting has ended + * which is not consistent between browsers. See #1351 + * @private + */ +vjs.Player.prototype.onWaitEnd = function(){ + this.removeClass('vjs-waiting'); +}; + +/** + * Fired whenever the player is jumping to a new time + * @event seeking + */ +vjs.Player.prototype.onSeeking = function(){ + this.addClass('vjs-seeking'); +}; + +/** + * Fired when the player has finished jumping to a new time + * @event seeked + */ +vjs.Player.prototype.onSeeked = function(){ + this.removeClass('vjs-seeking'); }; /** @@ -3264,14 +4182,14 @@ vjs.Player.prototype.onFirstPlay = function(){ * @event pause */ vjs.Player.prototype.onPause = function(){ - vjs.removeClass(this.el_, 'vjs-playing'); - vjs.addClass(this.el_, 'vjs-paused'); + this.removeClass('vjs-playing'); + this.addClass('vjs-paused'); }; /** * Fired when the current playback position has changed * - * During playback this is fired every 15-250 milliseconds, depnding on the + * During playback this is fired every 15-250 milliseconds, depending on the * playback technology in use. * @event timeupdate */ @@ -3293,9 +4211,12 @@ vjs.Player.prototype.onProgress = function(){ * @event ended */ vjs.Player.prototype.onEnded = function(){ + this.addClass('vjs-ended'); if (this.options_['loop']) { this.currentTime(0); this.play(); + } else if (!this.paused()) { + this.pause(); } }; @@ -3304,12 +4225,21 @@ vjs.Player.prototype.onEnded = function(){ * @event durationchange */ vjs.Player.prototype.onDurationChange = function(){ - // Allows for cacheing value instead of asking player each time. + // Allows for caching value instead of asking player each time. // We need to get the techGet response and check for a value so we don't // accidentally cause the stack to blow up. var duration = this.techGet('duration'); if (duration) { + if (duration < 0) { + duration = Infinity; + } this.duration(duration); + // Determine if the stream is live and propagate styles down to UI. + if (duration === Infinity) { + this.addClass('vjs-live'); + } else { + this.removeClass('vjs-live'); + } } }; @@ -3324,7 +4254,7 @@ vjs.Player.prototype.onVolumeChange; * @event fullscreenchange */ vjs.Player.prototype.onFullscreenChange = function() { - if (this.isFullScreen()) { + if (this.isFullscreen()) { this.addClass('vjs-fullscreen'); } else { this.removeClass('vjs-fullscreen'); @@ -3332,12 +4262,10 @@ vjs.Player.prototype.onFullscreenChange = function() { }; /** - * Fired when there is an error in playback + * Fired when an error occurs * @event error */ -vjs.Player.prototype.onError = function(e) { - vjs.log('Video Error', e); -}; +vjs.Player.prototype.onError; // /* Player API // ================================================================================ */ @@ -3373,7 +4301,6 @@ vjs.Player.prototype.techCall = function(method, arg){ // Get calls can't wait for the tech, and sometimes don't need to. vjs.Player.prototype.techGet = function(method){ - if (this.tech && this.tech.isReady_) { // Flash likes to die and reload when you hide or reposition it. @@ -3456,16 +4383,13 @@ vjs.Player.prototype.currentTime = function(seconds){ this.techCall('setCurrentTime', seconds); - // improve the accuracy of manual timeupdates - if (this.manualTimeUpdates) { this.trigger('timeupdate'); } - return this; } // cache last currentTime and return. default to 0 seconds // // Caching the currentTime is meant to prevent a massive amount of reads on the tech's - // currentTime when scrubbing, but may not provide much performace benefit afterall. + // currentTime when scrubbing, but may not provide much performance benefit afterall. // Should be tested. Also something has to read the actual current time or the cache will // never get updated. return this.cache_.currentTime = (this.techGet('currentTime') || 0); @@ -3485,7 +4409,7 @@ vjs.Player.prototype.currentTime = function(seconds){ vjs.Player.prototype.duration = function(seconds){ if (seconds !== undefined) { - // cache the last set value for optimiized scrubbing (esp. Flash) + // cache the last set value for optimized scrubbing (esp. Flash) this.cache_.duration = parseFloat(seconds); return this; @@ -3498,7 +4422,14 @@ vjs.Player.prototype.duration = function(seconds){ return this.cache_.duration || 0; }; -// Calculates how much time is left. Not in spec, but useful. +/** + * Calculates how much time is left. + * + * var timeLeft = myPlayer.remainingTime(); + * + * Not a native video element function, but useful + * @return {Number} The time remaining in seconds + */ vjs.Player.prototype.remainingTime = function(){ return this.duration() - this.currentTime(); }; @@ -3506,7 +4437,6 @@ vjs.Player.prototype.remainingTime = function(){ // http://dev.w3.org/html5/spec/video.html#dom-media-buffered // Buffered returns a timerange object. // Kind of like an array of portions of the video that have been downloaded. -// So far no browsers return more than one range (portion) /** * Get a TimeRange object with the times of the video that have been downloaded @@ -3529,19 +4459,13 @@ vjs.Player.prototype.remainingTime = function(){ * @return {Object} A mock TimeRange object (following HTML spec) */ vjs.Player.prototype.buffered = function(){ - var buffered = this.techGet('buffered'), - start = 0, - buflast = buffered.length - 1, - // Default end to 0 and store in values - end = this.cache_.bufferEnd = this.cache_.bufferEnd || 0; + var buffered = this.techGet('buffered'); - if (buffered && buflast >= 0 && buffered.end(buflast) !== end) { - end = buffered.end(buflast); - // Storing values allows them be overridden by setBufferedFromProgress - this.cache_.bufferEnd = end; + if (!buffered || !buffered.length) { + buffered = vjs.createTimeRange(0,0); } - return vjs.createTimeRange(start, end); + return buffered; }; /** @@ -3555,7 +4479,46 @@ vjs.Player.prototype.buffered = function(){ * @return {Number} A decimal between 0 and 1 representing the percent */ vjs.Player.prototype.bufferedPercent = function(){ - return (this.duration()) ? this.buffered().end(0) / this.duration() : 0; + var duration = this.duration(), + buffered = this.buffered(), + bufferedDuration = 0, + start, end; + + if (!duration) { + return 0; + } + + for (var i=0; i duration) { + end = duration; + } + + bufferedDuration += end - start; + } + + return bufferedDuration / duration; +}; + +/** + * Get the ending time of the last buffered time range + * + * This is used in the progress bar to encapsulate all time ranges. + * @return {Number} The end of the last buffered time range + */ +vjs.Player.prototype.bufferedEnd = function(){ + var buffered = this.buffered(), + duration = this.duration(), + end = buffered.end(buffered.length-1); + + if (end > duration) { + end = duration; + } + + return end; }; /** @@ -3612,7 +4575,7 @@ vjs.Player.prototype.muted = function(muted){ }; // Check if current tech can support native fullscreen -// (e.g. with built in controls lik iOS, so not our flash swf) +// (e.g. with built in controls like iOS, so not our flash swf) vjs.Player.prototype.supportsFullScreen = function(){ return this.techGet('supportsFullScreen') || false; }; @@ -3622,37 +4585,46 @@ vjs.Player.prototype.supportsFullScreen = function(){ * @type {Boolean} * @private */ -vjs.Player.prototype.isFullScreen_ = false; +vjs.Player.prototype.isFullscreen_ = false; /** * Check if the player is in fullscreen mode * * // get - * var fullscreenOrNot = myPlayer.isFullScreen(); + * var fullscreenOrNot = myPlayer.isFullscreen(); * * // set - * myPlayer.isFullScreen(true); // tell the player it's in fullscreen + * myPlayer.isFullscreen(true); // tell the player it's in fullscreen * - * NOTE: As of the latest HTML5 spec, isFullScreen is no longer an official - * property and instead document.fullscreenElement is used. But isFullScreen is + * NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official + * property and instead document.fullscreenElement is used. But isFullscreen is * still a valuable property for internal player workings. * * @param {Boolean=} isFS Update the player's fullscreen state * @return {Boolean} true if fullscreen, false if not * @return {vjs.Player} self, when setting */ -vjs.Player.prototype.isFullScreen = function(isFS){ +vjs.Player.prototype.isFullscreen = function(isFS){ if (isFS !== undefined) { - this.isFullScreen_ = isFS; + this.isFullscreen_ = !!isFS; return this; } - return this.isFullScreen_; + return this.isFullscreen_; +}; + +/** + * Old naming for isFullscreen() + * @deprecated for lowercase 's' version + */ +vjs.Player.prototype.isFullScreen = function(isFS){ + vjs.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")'); + return this.isFullscreen(isFS); }; /** * Increase the size of the video to full screen * - * myPlayer.requestFullScreen(); + * myPlayer.requestFullscreen(); * * In some browsers, full screen is not supported natively, so it enters * "full window mode", where the video fills the browser window. @@ -3663,31 +4635,32 @@ vjs.Player.prototype.isFullScreen = function(isFS){ * * @return {vjs.Player} self */ -vjs.Player.prototype.requestFullScreen = function(){ - var requestFullScreen = vjs.support.requestFullScreen; - this.isFullScreen(true); +vjs.Player.prototype.requestFullscreen = function(){ + var fsApi = vjs.browser.fullscreenAPI; - if (requestFullScreen) { + this.isFullscreen(true); + + if (fsApi) { // the browser supports going fullscreen at the element level so we can // take the controls fullscreen as well as the video // Trigger fullscreenchange event after change // We have to specifically add this each time, and remove - // when cancelling fullscreen. Otherwise if there's multiple + // when canceling fullscreen. Otherwise if there's multiple // players on a page, they would all be reacting to the same fullscreen // events - vjs.on(document, requestFullScreen.eventName, vjs.bind(this, function(e){ - this.isFullScreen(document[requestFullScreen.isFullScreen]); + vjs.on(document, fsApi['fullscreenchange'], vjs.bind(this, function(e){ + this.isFullscreen(document[fsApi.fullscreenElement]); // If cancelling fullscreen, remove event listener. - if (this.isFullScreen() === false) { - vjs.off(document, requestFullScreen.eventName, arguments.callee); + if (this.isFullscreen() === false) { + vjs.off(document, fsApi['fullscreenchange'], arguments.callee); } this.trigger('fullscreenchange'); })); - this.el_[requestFullScreen.requestFn](); + this.el_[fsApi.requestFullscreen](); } else if (this.tech.supportsFullScreen()) { // we can't take the video.js controls fullscreen but we can go fullscreen @@ -3703,20 +4676,30 @@ vjs.Player.prototype.requestFullScreen = function(){ return this; }; +/** + * Old naming for requestFullscreen + * @deprecated for lower case 's' version + */ +vjs.Player.prototype.requestFullScreen = function(){ + vjs.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")'); + return this.requestFullscreen(); +}; + + /** * Return the video to its normal size after having been in full screen mode * - * myPlayer.cancelFullScreen(); + * myPlayer.exitFullscreen(); * * @return {vjs.Player} self */ -vjs.Player.prototype.cancelFullScreen = function(){ - var requestFullScreen = vjs.support.requestFullScreen; - this.isFullScreen(false); +vjs.Player.prototype.exitFullscreen = function(){ + var fsApi = vjs.browser.fullscreenAPI; + this.isFullscreen(false); // Check for browser element fullscreen support - if (requestFullScreen) { - document[requestFullScreen.cancelFn](); + if (fsApi) { + document[fsApi.exitFullscreen](); } else if (this.tech.supportsFullScreen()) { this.techCall('exitFullScreen'); } else { @@ -3727,6 +4710,15 @@ vjs.Player.prototype.cancelFullScreen = function(){ return this; }; +/** + * Old naming for exitFullscreen + * @deprecated for exitFullscreen + */ +vjs.Player.prototype.cancelFullScreen = function(){ + vjs.log.warn('player.cancelFullScreen() has been deprecated, use player.exitFullscreen()'); + return this.exitFullscreen(); +}; + // When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us. vjs.Player.prototype.enterFullWindow = function(){ this.isFullWindow = true; @@ -3747,8 +4739,8 @@ vjs.Player.prototype.enterFullWindow = function(){ }; vjs.Player.prototype.fullWindowOnEscKey = function(event){ if (event.keyCode === 27) { - if (this.isFullScreen() === true) { - this.cancelFullScreen(); + if (this.isFullscreen() === true) { + this.exitFullscreen(); } else { this.exitFullWindow(); } @@ -3771,12 +4763,17 @@ vjs.Player.prototype.exitFullWindow = function(){ }; vjs.Player.prototype.selectSource = function(sources){ - // Loop through each playback technology in the options order for (var i=0,j=this.options_['techOrder'];i 0) { + // In milliseconds, if no more activity has occurred the + // user will be considered inactive + inactivityTimeout = this.setTimeout(function () { + // Protect against the case where the inactivityTimeout can trigger just + // before the next user activity is picked up by the activityCheck loop + // causing a flicker + if (!this.userActivity_) { + this.userActive(false); + } + }, timeout); + } } - }), 250); + }, 250); +}; - // Clean up the intervals when we kill the player - this.on('dispose', function(){ - clearInterval(activityCheck); - clearTimeout(inactivityTimeout); - }); +/** + * Gets or sets the current playback rate. + * @param {Boolean} rate New playback rate to set. + * @return {Number} Returns the new playback rate when setting + * @return {Number} Returns the current playback rate when getting + */ +vjs.Player.prototype.playbackRate = function(rate) { + if (rate !== undefined) { + this.techCall('setPlaybackRate', rate); + return this; + } + + if (this.tech && this.tech['featuresPlaybackRate']) { + return this.techGet('playbackRate'); + } else { + return 1.0; + } + +}; + +/** + * Store the current audio state + * @type {Boolean} + * @private + */ +vjs.Player.prototype.isAudio_ = false; + +/** + * Gets or sets the audio flag + * + * @param {Boolean} bool True signals that this is an audio player. + * @return {Boolean} Returns true if player is audio, false if not when getting + * @return {vjs.Player} Returns the player if setting + * @private + */ +vjs.Player.prototype.isAudio = function(bool) { + if (bool !== undefined) { + this.isAudio_ = !!bool; + return this; + } + + return this.isAudio_; +}; + +/** + * Returns the current state of network activity for the element, from + * the codes in the list below. + * - NETWORK_EMPTY (numeric value 0) + * The element has not yet been initialised. All attributes are in + * their initial states. + * - NETWORK_IDLE (numeric value 1) + * The element's resource selection algorithm is active and has + * selected a resource, but it is not actually using the network at + * this time. + * - NETWORK_LOADING (numeric value 2) + * The user agent is actively trying to download data. + * - NETWORK_NO_SOURCE (numeric value 3) + * The element's resource selection algorithm is active, but it has + * not yet found a resource to use. + * @return {Number} the current network activity state + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states + */ +vjs.Player.prototype.networkState = function(){ + return this.techGet('networkState'); +}; + +/** + * Returns a value that expresses the current state of the element + * with respect to rendering the current playback position, from the + * codes in the list below. + * - HAVE_NOTHING (numeric value 0) + * No information regarding the media resource is available. + * - HAVE_METADATA (numeric value 1) + * Enough of the resource has been obtained that the duration of the + * resource is available. + * - HAVE_CURRENT_DATA (numeric value 2) + * Data for the immediate current playback position is available. + * - HAVE_FUTURE_DATA (numeric value 3) + * Data for the immediate current playback position is available, as + * well as enough data for the user agent to advance the current + * playback position in the direction of playback. + * - HAVE_ENOUGH_DATA (numeric value 4) + * The user agent estimates that enough data is available for + * playback to proceed uninterrupted. + * @return {Number} the current playback rendering state + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate + */ +vjs.Player.prototype.readyState = function(){ + return this.techGet('readyState'); +}; + +/** + * Text tracks are tracks of timed text events. + * Captions - text displayed over the video for the hearing impaired + * Subtitles - text displayed over the video for those who don't understand language in the video + * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video + * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device + */ + +/** + * Get an array of associated text tracks. captions, subtitles, chapters, descriptions + * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks + * @return {Array} Array of track objects + */ +vjs.Player.prototype.textTracks = function(){ + // cannot use techGet directly because it checks to see whether the tech is ready. + // Flash is unlikely to be ready in time but textTracks should still work. + return this.tech && this.tech['textTracks'](); +}; + +vjs.Player.prototype.remoteTextTracks = function() { + return this.tech && this.tech['remoteTextTracks'](); +}; + +/** + * Add a text track + * In addition to the W3C settings we allow adding additional info through options. + * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack + * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata + * @param {String=} label Optional label + * @param {String=} language Optional language + */ +vjs.Player.prototype.addTextTrack = function(kind, label, language) { + return this.tech && this.tech['addTextTrack'](kind, label, language); +}; + +vjs.Player.prototype.addRemoteTextTrack = function(options) { + return this.tech && this.tech['addRemoteTextTrack'](options); +}; + +vjs.Player.prototype.removeRemoteTextTrack = function(track) { + this.tech && this.tech['removeRemoteTextTrack'](track); }; // Methods to add support for -// networkState: function(){ return this.techCall('networkState'); }, -// readyState: function(){ return this.techCall('readyState'); }, // initialTime: function(){ return this.techCall('initialTime'); }, // startOffsetTime: function(){ return this.techCall('startOffsetTime'); }, // played: function(){ return this.techCall('played'); }, @@ -4184,7 +5432,6 @@ vjs.Player.prototype.listenForUserActivity = function(){ // videoWidth: function(){ return this.techCall('videoWidth'); }, // videoHeight: function(){ return this.techCall('videoHeight'); }, // defaultPlaybackRate: function(){ return this.techCall('defaultPlaybackRate'); }, -// playbackRate: function(){ return this.techCall('playbackRate'); }, // mediaGroup: function(){ return this.techCall('mediaGroup'); }, // controller: function(){ return this.techCall('controller'); }, // defaultMuted: function(){ return this.techCall('defaultMuted'); } @@ -4192,52 +5439,6 @@ vjs.Player.prototype.listenForUserActivity = function(){ // TODO // currentSrcList: the array of sources including other formats and bitrates // playList: array of source lists in order of playback - -// RequestFullscreen API -(function(){ - var prefix, requestFS, div; - - div = document.createElement('div'); - - requestFS = {}; - - // Current W3C Spec - // http://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html#api - // Mozilla Draft: https://wiki.mozilla.org/Gecko:FullScreenAPI#fullscreenchange_event - // New: https://dvcs.w3.org/hg/fullscreen/raw-file/529a67b8d9f3/Overview.html - if (div.cancelFullscreen !== undefined) { - requestFS.requestFn = 'requestFullscreen'; - requestFS.cancelFn = 'exitFullscreen'; - requestFS.eventName = 'fullscreenchange'; - requestFS.isFullScreen = 'fullScreen'; - - // Webkit (Chrome/Safari) and Mozilla (Firefox) have working implementations - // that use prefixes and vary slightly from the new W3C spec. Specifically, - // using 'exit' instead of 'cancel', and lowercasing the 'S' in Fullscreen. - // Other browsers don't have any hints of which version they might follow yet, - // so not going to try to predict by looping through all prefixes. - } else { - - if (document.mozCancelFullScreen) { - prefix = 'moz'; - requestFS.isFullScreen = prefix + 'FullScreen'; - } else { - prefix = 'webkit'; - requestFS.isFullScreen = prefix + 'IsFullScreen'; - } - - if (div[prefix + 'RequestFullScreen']) { - requestFS.requestFn = prefix + 'RequestFullScreen'; - requestFS.cancelFn = prefix + 'CancelFullScreen'; - } - requestFS.eventName = prefix + 'fullscreenchange'; - } - - if (document[requestFS.cancelFn]) { - vjs.support.requestFullScreen = requestFS; - } - -})(); /** * Container of main controls * @param {vjs.Player|Object} player @@ -4256,11 +5457,16 @@ vjs.ControlBar.prototype.options_ = { 'timeDivider': {}, 'durationDisplay': {}, 'remainingTimeDisplay': {}, + 'liveDisplay': {}, 'progressControl': {}, 'fullscreenToggle': {}, 'volumeControl': {}, - 'muteToggle': {} - // 'volumeMenuButton': {} + 'muteToggle': {}, + // 'volumeMenuButton': {}, + 'playbackRateMenuButton': {}, + 'subtitlesButton': {}, + 'captionsButton': {}, + 'chaptersButton': {} } }; @@ -4269,6 +5475,34 @@ vjs.ControlBar.prototype.createEl = function(){ className: 'vjs-control-bar' }); }; +/** + * Displays the live indicator + * TODO - Future make it click to snap to live + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +vjs.LiveDisplay = vjs.Component.extend({ + init: function(player, options){ + vjs.Component.call(this, player, options); + } +}); + +vjs.LiveDisplay.prototype.createEl = function(){ + var el = vjs.Component.prototype.createEl.call(this, 'div', { + className: 'vjs-live-controls vjs-control' + }); + + this.contentEl_ = vjs.createEl('div', { + className: 'vjs-live-display', + innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE'), + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + + return el; +}; /** * Button to toggle between play and pause * @param {vjs.Player|Object} player @@ -4281,8 +5515,8 @@ vjs.PlayToggle = vjs.Button.extend({ init: function(player, options){ vjs.Button.call(this, player, options); - player.on('play', vjs.bind(this, this.onPlay)); - player.on('pause', vjs.bind(this, this.onPause)); + this.on(player, 'play', this.onPlay); + this.on(player, 'pause', this.onPause); } }); @@ -4303,16 +5537,16 @@ vjs.PlayToggle.prototype.onClick = function(){ // OnPlay - Add the vjs-playing class to the element so it can change appearance vjs.PlayToggle.prototype.onPlay = function(){ - vjs.removeClass(this.el_, 'vjs-paused'); - vjs.addClass(this.el_, 'vjs-playing'); - this.el_.children[0].children[0].innerHTML = 'Pause'; // change the button text to "Pause" + this.removeClass('vjs-paused'); + this.addClass('vjs-playing'); + this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" }; // OnPause - Add the vjs-paused class to the element so it can change appearance vjs.PlayToggle.prototype.onPause = function(){ - vjs.removeClass(this.el_, 'vjs-playing'); - vjs.addClass(this.el_, 'vjs-paused'); - this.el_.children[0].children[0].innerHTML = 'Play'; // change the button text to "Play" + this.removeClass('vjs-playing'); + this.addClass('vjs-paused'); + this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" }; /** * Displays the current time @@ -4325,7 +5559,7 @@ vjs.CurrentTimeDisplay = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); - player.on('timeupdate', vjs.bind(this, this.updateContent)); + this.on(player, 'timeupdate', this.updateContent); } }); @@ -4347,7 +5581,7 @@ vjs.CurrentTimeDisplay.prototype.createEl = function(){ vjs.CurrentTimeDisplay.prototype.updateContent = function(){ // Allows for smooth scrubbing, when player can't keep up. var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.contentEl_.innerHTML = 'Current Time ' + vjs.formatTime(time, this.player_.duration()); + this.contentEl_.innerHTML = '' + this.localize('Current Time') + ' ' + vjs.formatTime(time, this.player_.duration()); }; /** @@ -4366,7 +5600,7 @@ vjs.DurationDisplay = vjs.Component.extend({ // so the value cannot be written out using this method. // Once the order of durationchange and this.player_.duration() being set is figured out, // this can be updated. - player.on('timeupdate', vjs.bind(this, this.updateContent)); + this.on(player, 'timeupdate', this.updateContent); } }); @@ -4377,7 +5611,7 @@ vjs.DurationDisplay.prototype.createEl = function(){ this.contentEl_ = vjs.createEl('div', { className: 'vjs-duration-display', - innerHTML: 'Duration Time ' + '0:00', // label the duration time for screen reader users + innerHTML: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); @@ -4388,7 +5622,7 @@ vjs.DurationDisplay.prototype.createEl = function(){ vjs.DurationDisplay.prototype.updateContent = function(){ var duration = this.player_.duration(); if (duration) { - this.contentEl_.innerHTML = 'Duration Time ' + vjs.formatTime(duration); // label the duration time for screen reader users + this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + vjs.formatTime(duration); // label the duration time for screen reader users } }; @@ -4426,7 +5660,7 @@ vjs.RemainingTimeDisplay = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); - player.on('timeupdate', vjs.bind(this, this.updateContent)); + this.on(player, 'timeupdate', this.updateContent); } }); @@ -4437,7 +5671,7 @@ vjs.RemainingTimeDisplay.prototype.createEl = function(){ this.contentEl_ = vjs.createEl('div', { className: 'vjs-remaining-time-display', - innerHTML: 'Remaining Time ' + '-0:00', // label the remaining time for screen reader users + innerHTML: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes }); @@ -4447,7 +5681,7 @@ vjs.RemainingTimeDisplay.prototype.createEl = function(){ vjs.RemainingTimeDisplay.prototype.updateContent = function(){ if (this.player_.duration()) { - this.contentEl_.innerHTML = 'Remaining Time ' + '-'+ vjs.formatTime(this.player_.remainingTime()); + this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ vjs.formatTime(this.player_.remainingTime()); } // Allows for smooth scrubbing, when player can't keep up. @@ -4479,12 +5713,12 @@ vjs.FullscreenToggle.prototype.buildCSSClass = function(){ }; vjs.FullscreenToggle.prototype.onClick = function(){ - if (!this.player_.isFullScreen()) { - this.player_.requestFullScreen(); - this.el_.children[0].children[0].innerHTML = 'Non-Fullscreen'; // change the button text to "Non-Fullscreen" + if (!this.player_.isFullscreen()) { + this.player_.requestFullscreen(); + this.controlText_.innerHTML = this.localize('Non-Fullscreen'); } else { - this.player_.cancelFullScreen(); - this.el_.children[0].children[0].innerHTML = 'Fullscreen'; // change the button to "Fullscreen" + this.player_.exitFullscreen(); + this.controlText_.innerHTML = this.localize('Fullscreen'); } }; /** @@ -4525,7 +5759,7 @@ vjs.SeekBar = vjs.Slider.extend({ /** @constructor */ init: function(player, options){ vjs.Slider.call(this, player, options); - player.on('timeupdate', vjs.bind(this, this.updateARIAAttributes)); + this.on(player, 'timeupdate', this.updateARIAAttributes); player.ready(vjs.bind(this, this.updateARIAAttributes)); } }); @@ -4564,6 +5798,7 @@ vjs.SeekBar.prototype.onMouseDown = function(event){ vjs.Slider.prototype.onMouseDown.call(this, event); this.player_.scrubbing = true; + this.player_.addClass('vjs-scrubbing'); this.videoWasPlaying = !this.player_.paused(); this.player_.pause(); @@ -4583,6 +5818,7 @@ vjs.SeekBar.prototype.onMouseUp = function(event){ vjs.Slider.prototype.onMouseUp.call(this, event); this.player_.scrubbing = false; + this.player_.removeClass('vjs-scrubbing'); if (this.videoWasPlaying) { this.player_.play(); } @@ -4596,7 +5832,6 @@ vjs.SeekBar.prototype.stepBack = function(){ this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users }; - /** * Shows load progress * @@ -4608,21 +5843,52 @@ vjs.LoadProgressBar = vjs.Component.extend({ /** @constructor */ init: function(player, options){ vjs.Component.call(this, player, options); - player.on('progress', vjs.bind(this, this.update)); + this.on(player, 'progress', this.update); } }); vjs.LoadProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-load-progress', - innerHTML: 'Loaded: 0%' + innerHTML: '' + this.localize('Loaded') + ': 0%' }); }; vjs.LoadProgressBar.prototype.update = function(){ - if (this.el_.style) { this.el_.style.width = vjs.round(this.player_.bufferedPercent() * 100, 2) + '%'; } -}; + var i, start, end, part, + buffered = this.player_.buffered(), + duration = this.player_.duration(), + bufferedEnd = this.player_.bufferedEnd(), + children = this.el_.children, + // get the percent width of a time compared to the total end + percentify = function (time, end){ + var percent = (time / end) || 0; // no NaN + return (percent * 100) + '%'; + }; + + // update the width of the progress bar + this.el_.style.width = percentify(bufferedEnd, duration); + + // add child elements to represent the individual buffered time ranges + for (i = 0; i < buffered.length; i++) { + start = buffered.start(i), + end = buffered.end(i), + part = children[i]; + if (!part) { + part = this.el_.appendChild(vjs.createEl()); + } + + // set the percent based on the width of the progress bar (bufferedEnd) + part.style.left = percentify(start, bufferedEnd); + part.style.width = percentify(end - start, bufferedEnd); + } + + // remove unused buffered range elements + for (i = children.length; i > buffered.length; i--) { + this.el_.removeChild(children[i-1]); + } +}; /** * Shows play progress @@ -4641,7 +5907,7 @@ vjs.PlayProgressBar = vjs.Component.extend({ vjs.PlayProgressBar.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-play-progress', - innerHTML: 'Progress: 0%' + innerHTML: '' + this.localize('Progress') + ': 0%' }); }; @@ -4656,7 +5922,7 @@ vjs.PlayProgressBar.prototype.createEl = function(){ vjs.SeekHandle = vjs.SliderHandle.extend({ init: function(player, options) { vjs.SliderHandle.call(this, player, options); - player.on('timeupdate', vjs.bind(this, this.updateContent)); + this.on(player, 'timeupdate', this.updateContent); } }); @@ -4693,16 +5959,16 @@ vjs.VolumeControl = vjs.Component.extend({ vjs.Component.call(this, player, options); // hide volume controls when they're not supported by the current tech - if (player.tech && player.tech.features && player.tech.features['volumeControl'] === false) { + if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } - player.on('loadstart', vjs.bind(this, function(){ - if (player.tech.features && player.tech.features['volumeControl'] === false) { + this.on(player, 'loadstart', function(){ + if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } - })); + }); } }); @@ -4729,9 +5995,8 @@ vjs.VolumeBar = vjs.Slider.extend({ /** @constructor */ init: function(player, options){ vjs.Slider.call(this, player, options); - player.on('volumechange', vjs.bind(this, this.updateARIAAttributes)); + this.on(player, 'volumechange', this.updateARIAAttributes); player.ready(vjs.bind(this, this.updateARIAAttributes)); - setTimeout(vjs.bind(this, this.update), 0); // update when elements is in DOM } }); @@ -4833,26 +6098,27 @@ vjs.MuteToggle = vjs.Button.extend({ init: function(player, options){ vjs.Button.call(this, player, options); - player.on('volumechange', vjs.bind(this, this.update)); + this.on(player, 'volumechange', this.update); // hide mute toggle if the current tech doesn't support volume control - if (player.tech && player.tech.features && player.tech.features['volumeControl'] === false) { + if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } - player.on('loadstart', vjs.bind(this, function(){ - if (player.tech.features && player.tech.features['volumeControl'] === false) { + + this.on(player, 'loadstart', function(){ + if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } - })); + }); } }); vjs.MuteToggle.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-mute-control vjs-control', - innerHTML: '
Mute
' + innerHTML: '
' + this.localize('Mute') + '
' }); }; @@ -4876,12 +6142,12 @@ vjs.MuteToggle.prototype.update = function(){ // This causes unnecessary and confusing information for screen reader users. // This check is needed because this function gets called every time the volume level is changed. if(this.player_.muted()){ - if(this.el_.children[0].children[0].innerHTML!='Unmute'){ - this.el_.children[0].children[0].innerHTML = 'Unmute'; // change the button text to "Unmute" + if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" } } else { - if(this.el_.children[0].children[0].innerHTML!='Mute'){ - this.el_.children[0].children[0].innerHTML = 'Mute'; // change the button text to "Mute" + if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" } } @@ -4901,19 +6167,19 @@ vjs.VolumeMenuButton = vjs.MenuButton.extend({ vjs.MenuButton.call(this, player, options); // Same listeners as MuteToggle - player.on('volumechange', vjs.bind(this, this.update)); + this.on(player, 'volumechange', this.volumeUpdate); // hide mute toggle if the current tech doesn't support volume control - if (player.tech && player.tech.features && player.tech.features.volumeControl === false) { + if (player.tech && player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } - player.on('loadstart', vjs.bind(this, function(){ - if (player.tech.features && player.tech.features.volumeControl === false) { + this.on(player, 'loadstart', function(){ + if (player.tech['featuresVolumeControl'] === false) { this.addClass('vjs-hidden'); } else { this.removeClass('vjs-hidden'); } - })); + }); this.addClass('vjs-menu-button'); } }); @@ -4922,7 +6188,13 @@ vjs.VolumeMenuButton.prototype.createMenu = function(){ var menu = new vjs.Menu(this.player_, { contentElType: 'div' }); - var vc = new vjs.VolumeBar(this.player_, vjs.obj.merge({vertical: true}, this.options_.volumeBar)); + var vc = new vjs.VolumeBar(this.player_, this.options_['volumeBar']); + vc.on('focus', function() { + menu.lockShowing(); + }); + vc.on('blur', function() { + menu.unlockShowing(); + }); menu.addChild(vc); return menu; }; @@ -4935,43 +6207,170 @@ vjs.VolumeMenuButton.prototype.onClick = function(){ vjs.VolumeMenuButton.prototype.createEl = function(){ return vjs.Button.prototype.createEl.call(this, 'div', { className: 'vjs-volume-menu-button vjs-menu-button vjs-control', - innerHTML: '
Mute
' + innerHTML: '
' + this.localize('Mute') + '
' }); }; -vjs.VolumeMenuButton.prototype.update = vjs.MuteToggle.prototype.update; -/* Poster Image -================================================================================ */ +vjs.VolumeMenuButton.prototype.volumeUpdate = vjs.MuteToggle.prototype.update; /** - * The component that handles showing the poster image. + * The component for controlling the playback rate * * @param {vjs.Player|Object} player * @param {Object=} options * @constructor */ -vjs.PosterImage = vjs.Button.extend({ +vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({ /** @constructor */ init: function(player, options){ - vjs.Button.call(this, player, options); + vjs.MenuButton.call(this, player, options); + + this.updateVisibility(); + this.updateLabel(); + + this.on(player, 'loadstart', this.updateVisibility); + this.on(player, 'ratechange', this.updateLabel); + } +}); + +vjs.PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate'; +vjs.PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate'; + +vjs.PlaybackRateMenuButton.prototype.createEl = function(){ + var el = vjs.MenuButton.prototype.createEl.call(this); + + this.labelEl_ = vjs.createEl('div', { + className: 'vjs-playback-rate-value', + innerHTML: 1.0 + }); + + el.appendChild(this.labelEl_); + + return el; +}; + +// Menu creation +vjs.PlaybackRateMenuButton.prototype.createMenu = function(){ + var menu = new vjs.Menu(this.player()); + var rates = this.player().options()['playbackRates']; - if (player.poster()) { - this.src(player.poster()); + if (rates) { + for (var i = rates.length - 1; i >= 0; i--) { + menu.addChild( + new vjs.PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) + ); } + } - if (!player.poster() || !player.controls()) { - this.hide(); + return menu; +}; + +vjs.PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){ + // Current playback rate + this.el().setAttribute('aria-valuenow', this.player().playbackRate()); +}; + +vjs.PlaybackRateMenuButton.prototype.onClick = function(){ + // select next rate option + var currentRate = this.player().playbackRate(); + var rates = this.player().options()['playbackRates']; + // this will select first one if the last one currently selected + var newRate = rates[0]; + for (var i = 0; i currentRate) { + newRate = rates[i]; + break; } + } + this.player().playbackRate(newRate); +}; - player.on('posterchange', vjs.bind(this, function(){ - this.src(player.poster()); - })); +vjs.PlaybackRateMenuButton.prototype.playbackRateSupported = function(){ + return this.player().tech + && this.player().tech['featuresPlaybackRate'] + && this.player().options()['playbackRates'] + && this.player().options()['playbackRates'].length > 0 + ; +}; + +/** + * Hide playback rate controls when they're no playback rate options to select + */ +vjs.PlaybackRateMenuButton.prototype.updateVisibility = function(){ + if (this.playbackRateSupported()) { + this.removeClass('vjs-hidden'); + } else { + this.addClass('vjs-hidden'); + } +}; + +/** + * Update button label when rate changed + */ +vjs.PlaybackRateMenuButton.prototype.updateLabel = function(){ + if (this.playbackRateSupported()) { + this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; + } +}; + +/** + * The specific menu item type for selecting a playback rate + * + * @constructor + */ +vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({ + contentElType: 'button', + /** @constructor */ + init: function(player, options){ + var label = this.label = options['rate']; + var rate = this.rate = parseFloat(label, 10); + + // Modify options for parent MenuItem class's init. + options['label'] = label; + options['selected'] = rate === 1; + vjs.MenuItem.call(this, player, options); + + this.on(player, 'ratechange', this.update); + } +}); + +vjs.PlaybackRateMenuItem.prototype.onClick = function(){ + vjs.MenuItem.prototype.onClick.call(this); + this.player().playbackRate(this.rate); +}; + +vjs.PlaybackRateMenuItem.prototype.update = function(){ + this.selected(this.player().playbackRate() == this.rate); +}; +/* Poster Image +================================================================================ */ +/** + * The component that handles showing the poster image. + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +vjs.PosterImage = vjs.Button.extend({ + /** @constructor */ + init: function(player, options){ + vjs.Button.call(this, player, options); - player.on('play', vjs.bind(this, this.hide)); + this.update(); + player.on('posterchange', vjs.bind(this, this.update)); } }); -// use the test el to check for backgroundSize style support -var _backgroundSizeSupported = 'backgroundSize' in vjs.TEST_VID.style; +/** + * Clean up the poster image + */ +vjs.PosterImage.prototype.dispose = function(){ + this.player().off('posterchange', this.update); + vjs.Button.prototype.dispose.call(this); +}; +/** + * Create the poster image element + * @return {Element} + */ vjs.PosterImage.prototype.createEl = function(){ var el = vjs.createEl('div', { className: 'vjs-poster', @@ -4980,42 +6379,62 @@ vjs.PosterImage.prototype.createEl = function(){ tabIndex: -1 }); - if (!_backgroundSizeSupported) { - // setup an img element as a fallback for IE8 - el.appendChild(vjs.createEl('img')); + // To ensure the poster image resizes while maintaining its original aspect + // ratio, use a div with `background-size` when available. For browsers that + // do not support `background-size` (e.g. IE8), fall back on using a regular + // img element. + if (!vjs.BACKGROUND_SIZE_SUPPORTED) { + this.fallbackImg_ = vjs.createEl('img'); + el.appendChild(this.fallbackImg_); } return el; }; -vjs.PosterImage.prototype.src = function(url){ - var el = this.el(); +/** + * Event handler for updates to the player's poster source + */ +vjs.PosterImage.prototype.update = function(){ + var url = this.player().poster(); + + this.setSrc(url); - // getter - // can't think of a need for a getter here - // see #838 if on is needed in the future - // still don't want a getter to set src as undefined - if (url === undefined) { - return; + // If there's no poster source we should display:none on this component + // so it's not still clickable or right-clickable + if (url) { + this.show(); + } else { + this.hide(); } +}; - // setter - // To ensure the poster image resizes while maintaining its original aspect - // ratio, use a div with `background-size` when available. For browsers that - // do not support `background-size` (e.g. IE8), fall back on using a regular - // img element. - if (_backgroundSizeSupported) { - el.style.backgroundImage = 'url("' + url + '")'; +/** + * Set the poster source depending on the display method + */ +vjs.PosterImage.prototype.setSrc = function(url){ + var backgroundImage; + + if (this.fallbackImg_) { + this.fallbackImg_.src = url; } else { - el.firstChild.src = url; + backgroundImage = ''; + // Any falsey values should stay as an empty string, otherwise + // this will throw an extra error + if (url) { + backgroundImage = 'url("' + url + '")'; + } + + this.el_.style.backgroundImage = backgroundImage; } }; +/** + * Event handler for clicks on the poster image + */ vjs.PosterImage.prototype.onClick = function(){ - // Only accept clicks when controls are enabled - if (this.player().controls()) { - this.player_.play(); - } + // We don't want a click to trigger playback when controls are disabled + // but CSS should be hiding the poster to prevent that from happening + this.player_.play(); }; /* Loading Spinner ================================================================================ */ @@ -5031,24 +6450,25 @@ vjs.LoadingSpinner = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); - player.on('canplay', vjs.bind(this, this.hide)); - player.on('canplaythrough', vjs.bind(this, this.hide)); - player.on('playing', vjs.bind(this, this.hide)); - player.on('seeking', vjs.bind(this, this.show)); + // MOVING DISPLAY HANDLING TO CSS + + // player.on('canplay', vjs.bind(this, this.hide)); + // player.on('canplaythrough', vjs.bind(this, this.hide)); + // player.on('playing', vjs.bind(this, this.hide)); + // player.on('seeking', vjs.bind(this, this.show)); // in some browsers seeking does not trigger the 'playing' event, // so we also need to trap 'seeked' if we are going to set a // 'seeking' event - player.on('seeked', vjs.bind(this, this.hide)); + // player.on('seeked', vjs.bind(this, this.hide)); - player.on('error', vjs.bind(this, this.show)); - player.on('ended', vjs.bind(this, this.hide)); + // player.on('ended', vjs.bind(this, this.hide)); // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing // player.on('stalled', vjs.bind(this, this.show)); - player.on('waiting', vjs.bind(this, this.show)); + // player.on('waiting', vjs.bind(this, this.show)); } }); @@ -5080,6 +6500,39 @@ vjs.BigPlayButton.prototype.createEl = function(){ vjs.BigPlayButton.prototype.onClick = function(){ this.player_.play(); }; +/** + * Display that an error has occurred making the video unplayable + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +vjs.ErrorDisplay = vjs.Component.extend({ + init: function(player, options){ + vjs.Component.call(this, player, options); + + this.update(); + this.on(player, 'error', this.update); + } +}); + +vjs.ErrorDisplay.prototype.createEl = function(){ + var el = vjs.Component.prototype.createEl.call(this, 'div', { + className: 'vjs-error-display' + }); + + this.contentEl_ = vjs.createEl('div'); + el.appendChild(this.contentEl_); + + return el; +}; + +vjs.ErrorDisplay.prototype.update = function(){ + if (this.player().error()) { + this.contentEl_.innerHTML = this.localize(this.player().error().message); + } +}; +(function() { + var createTrackHelper; /** * @fileoverview Media Technology Controller - Base class for media playback * technology controllers like Flash and HTML5 @@ -5100,7 +6553,23 @@ vjs.MediaTechController = vjs.Component.extend({ options.reportTouchActivity = false; vjs.Component.call(this, player, options, ready); + // Manually track progress in cases where the browser/flash player doesn't report it. + if (!this['featuresProgressEvents']) { + this.manualProgressOn(); + } + + // Manually track timeupdates in cases where the browser/flash player doesn't report it. + if (!this['featuresTimeupdateEvents']) { + this.manualTimeUpdatesOn(); + } + this.initControlsListeners(); + + if (!this['featuresNativeTextTracks']) { + this.emulateTextTracks(); + } + + this.initTextTrackListeners(); } }); @@ -5125,24 +6594,32 @@ vjs.MediaTechController = vjs.Component.extend({ * any controls will still keep the user active */ vjs.MediaTechController.prototype.initControlsListeners = function(){ - var player, tech, activateControls, deactivateControls; + var player, activateControls; - tech = this; player = this.player(); - var activateControls = function(){ + activateControls = function(){ if (player.controls() && !player.usingNativeControls()) { - tech.addControlsListeners(); + this.addControlsListeners(); } }; - deactivateControls = vjs.bind(tech, tech.removeControlsListeners); - // Set up event listeners once the tech is ready and has an element to apply // listeners to this.ready(activateControls); - player.on('controlsenabled', activateControls); - player.on('controlsdisabled', deactivateControls); + this.on(player, 'controlsenabled', activateControls); + this.on(player, 'controlsdisabled', this.removeControlsListeners); + + // if we're loading the playback object after it has started loading or playing the + // video (often with autoplay on) then the loadstart event has already fired and we + // need to fire it manually because many things rely on it. + // Long term we might consider how we would do this for other events like 'canplay' + // that may also have fired. + this.ready(function(){ + if (this.networkState && this.networkState() > 0) { + this.player().trigger('loadstart'); + } + }); }; vjs.MediaTechController.prototype.addControlsListeners = function(){ @@ -5158,8 +6635,6 @@ vjs.MediaTechController.prototype.addControlsListeners = function(){ // so we'll check if the controls were already showing before reporting user // activity this.on('touchstart', function(event) { - // Stop the mouse events from also happening - event.preventDefault(); userWasActive = this.player_.userActive(); }); @@ -5169,6 +6644,11 @@ vjs.MediaTechController.prototype.addControlsListeners = function(){ } }); + this.on('touchend', function(event) { + // Stop the mouse events from also happening + event.preventDefault(); + }); + // Turn on component tap events this.emitTapEvents(); @@ -5221,88 +6701,439 @@ vjs.MediaTechController.prototype.onTap = function(){ this.player().userActive(!this.player().userActive()); }; -/** - * Provide a default setPoster method for techs - * - * Poster support for techs should be optional, so we don't want techs to - * break if they don't have a way to set a poster. - */ -vjs.MediaTechController.prototype.setPoster = function(){}; - -vjs.MediaTechController.prototype.features = { - 'volumeControl': true, +/* Fallbacks for unsupported event types +================================================================================ */ +// Manually trigger progress events based on changes to the buffered amount +// Many flash players and older HTML5 browsers don't send progress or progress-like events +vjs.MediaTechController.prototype.manualProgressOn = function(){ + this.manualProgress = true; - // Resizing plugins using request fullscreen reloads the plugin - 'fullscreenResize': false, + // Trigger progress watching when a source begins loading + this.trackProgress(); +}; - // Optional events that we can manually mimic with timers - // currently not triggered by video-js-swf - 'progressEvents': false, - 'timeupdateEvents': false +vjs.MediaTechController.prototype.manualProgressOff = function(){ + this.manualProgress = false; + this.stopTrackingProgress(); }; -vjs.media = {}; +vjs.MediaTechController.prototype.trackProgress = function(){ + this.progressInterval = this.setInterval(function(){ + // Don't trigger unless buffered amount is greater than last time -/** - * List of default API methods for any MediaTechController - * @type {String} - */ -vjs.media.ApiMethods = 'play,pause,paused,currentTime,setCurrentTime,duration,buffered,volume,setVolume,muted,setMuted,width,height,supportsFullScreen,enterFullScreen,src,load,currentSrc,preload,setPreload,autoplay,setAutoplay,loop,setLoop,error,networkState,readyState,seeking,initialTime,startOffsetTime,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks,defaultPlaybackRate,playbackRate,mediaGroup,controller,controls,defaultMuted'.split(','); -// Create placeholder methods for each that warn when a method isn't supported by the current playback technology + var bufferedPercent = this.player().bufferedPercent(); -function createMethod(methodName){ - return function(){ - throw new Error('The "'+methodName+'" method is not available on the playback technology\'s API'); - }; -} + if (this.bufferedPercent_ != bufferedPercent) { + this.player().trigger('progress'); + } -for (var i = vjs.media.ApiMethods.length - 1; i >= 0; i--) { - var methodName = vjs.media.ApiMethods[i]; - vjs.MediaTechController.prototype[vjs.media.ApiMethods[i]] = createMethod(methodName); -} -/** - * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API - */ + this.bufferedPercent_ = bufferedPercent; -/** - * HTML5 Media Controller - Wrapper for HTML5 Media API - * @param {vjs.Player|Object} player - * @param {Object=} options - * @param {Function=} ready - * @constructor - */ -vjs.Html5 = vjs.MediaTechController.extend({ - /** @constructor */ - init: function(player, options, ready){ - // volume cannot be changed from 1 on iOS - this.features['volumeControl'] = vjs.Html5.canControlVolume(); + if (bufferedPercent === 1) { + this.stopTrackingProgress(); + } + }, 500); +}; +vjs.MediaTechController.prototype.stopTrackingProgress = function(){ this.clearInterval(this.progressInterval); }; - // In iOS, if you move a video element in the DOM, it breaks video playback. - this.features['movingMediaElementInDOM'] = !vjs.IS_IOS; +/*! Time Tracking -------------------------------------------------------------- */ +vjs.MediaTechController.prototype.manualTimeUpdatesOn = function(){ + var player = this.player_; - // HTML video is able to automatically resize when going to fullscreen - this.features['fullscreenResize'] = true; + this.manualTimeUpdates = true; - vjs.MediaTechController.call(this, player, options, ready); - this.setupTriggers(); + this.on(player, 'play', this.trackCurrentTime); + this.on(player, 'pause', this.stopTrackingCurrentTime); + // timeupdate is also called by .currentTime whenever current time is set - var source = options['source']; + // Watch for native timeupdate event + this.one('timeupdate', function(){ + // Update known progress support for this playback technology + this['featuresTimeupdateEvents'] = true; + // Turn off manual progress tracking + this.manualTimeUpdatesOff(); + }); +}; - // If the element source is already set, we may have missed the loadstart event, and want to trigger it. - // We don't want to set the source again and interrupt playback. - if (source && this.el_.currentSrc === source.src && this.el_.networkState > 0) { - player.trigger('loadstart'); +vjs.MediaTechController.prototype.manualTimeUpdatesOff = function(){ + var player = this.player_; - // Otherwise set the source if one was provided. - } else if (source) { - this.el_.src = source.src; - } + this.manualTimeUpdates = false; + this.stopTrackingCurrentTime(); + this.off(player, 'play', this.trackCurrentTime); + this.off(player, 'pause', this.stopTrackingCurrentTime); +}; - // Determine if native controls should be used - // Our goal should be to get the custom controls on mobile solid everywhere - // so we can remove this all together. Right now this will block custom +vjs.MediaTechController.prototype.trackCurrentTime = function(){ + if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } + this.currentTimeInterval = this.setInterval(function(){ + this.player().trigger('timeupdate'); + }, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 +}; + +// Turn off play progress tracking (when paused or dragging) +vjs.MediaTechController.prototype.stopTrackingCurrentTime = function(){ + this.clearInterval(this.currentTimeInterval); + + // #1002 - if the video ends right before the next timeupdate would happen, + // the progress bar won't make it all the way to the end + this.player().trigger('timeupdate'); +}; + +vjs.MediaTechController.prototype.dispose = function() { + // Turn off any manual progress or timeupdate tracking + if (this.manualProgress) { this.manualProgressOff(); } + + if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } + + vjs.Component.prototype.dispose.call(this); +}; + +vjs.MediaTechController.prototype.setCurrentTime = function() { + // improve the accuracy of manual timeupdates + if (this.manualTimeUpdates) { this.player().trigger('timeupdate'); } +}; + +// TODO: Consider looking at moving this into the text track display directly +// https://github.com/videojs/video.js/issues/1863 +vjs.MediaTechController.prototype.initTextTrackListeners = function() { + var player = this.player_, + tracks, + textTrackListChanges = function() { + var textTrackDisplay = player.getChild('textTrackDisplay'), + controlBar; + + if (textTrackDisplay) { + textTrackDisplay.updateDisplay(); + } + }; + + tracks = this.textTracks(); + + if (!tracks) { + return; + } + + tracks.addEventListener('removetrack', textTrackListChanges); + tracks.addEventListener('addtrack', textTrackListChanges); + + this.on('dispose', vjs.bind(this, function() { + tracks.removeEventListener('removetrack', textTrackListChanges); + tracks.removeEventListener('addtrack', textTrackListChanges); + })); +}; + +vjs.MediaTechController.prototype.emulateTextTracks = function() { + var player = this.player_, + textTracksChanges, + tracks, + script; + + if (!window['WebVTT']) { + script = document.createElement('script'); + script.src = player.options()['vtt.js'] || '../node_modules/vtt.js/dist/vtt.js'; + player.el().appendChild(script); + window['WebVTT'] = true; + } + + tracks = this.textTracks(); + if (!tracks) { + return; + } + + textTracksChanges = function() { + var i, track, textTrackDisplay; + + textTrackDisplay = player.getChild('textTrackDisplay'), + + textTrackDisplay.updateDisplay(); + + for (i = 0; i < this.length; i++) { + track = this[i]; + track.removeEventListener('cuechange', vjs.bind(textTrackDisplay, textTrackDisplay.updateDisplay)); + if (track.mode === 'showing') { + track.addEventListener('cuechange', vjs.bind(textTrackDisplay, textTrackDisplay.updateDisplay)); + } + } + }; + + tracks.addEventListener('change', textTracksChanges); + + this.on('dispose', vjs.bind(this, function() { + tracks.removeEventListener('change', textTracksChanges); + })); +}; + +/** + * Provide default methods for text tracks. + * + * Html5 tech overrides these. + */ + +/** + * List of associated text tracks + * @type {Array} + * @private + */ +vjs.MediaTechController.prototype.textTracks_; + +vjs.MediaTechController.prototype.textTracks = function() { + this.player_.textTracks_ = this.player_.textTracks_ || new vjs.TextTrackList(); + return this.player_.textTracks_; +}; + +vjs.MediaTechController.prototype.remoteTextTracks = function() { + this.player_.remoteTextTracks_ = this.player_.remoteTextTracks_ || new vjs.TextTrackList(); + return this.player_.remoteTextTracks_; +}; + +createTrackHelper = function(self, kind, label, language, options) { + var tracks = self.textTracks(), + track; + + options = options || {}; + + options['kind'] = kind; + if (label) { + options['label'] = label; + } + if (language) { + options['language'] = language; + } + options['player'] = self.player_; + + track = new vjs.TextTrack(options); + tracks.addTrack_(track); + + return track; +}; + +vjs.MediaTechController.prototype.addTextTrack = function(kind, label, language) { + if (!kind) { + throw new Error('TextTrack kind is required but was not provided'); + } + + return createTrackHelper(this, kind, label, language); +}; + +vjs.MediaTechController.prototype.addRemoteTextTrack = function(options) { + var track = createTrackHelper(this, options['kind'], options['label'], options['language'], options); + this.remoteTextTracks().addTrack_(track); + return { + track: track + }; +}; + +vjs.MediaTechController.prototype.removeRemoteTextTrack = function(track) { + this.textTracks().removeTrack_(track); + this.remoteTextTracks().removeTrack_(track); +}; + +/** + * Provide a default setPoster method for techs + * + * Poster support for techs should be optional, so we don't want techs to + * break if they don't have a way to set a poster. + */ +vjs.MediaTechController.prototype.setPoster = function(){}; + +vjs.MediaTechController.prototype['featuresVolumeControl'] = true; + +// Resizing plugins using request fullscreen reloads the plugin +vjs.MediaTechController.prototype['featuresFullscreenResize'] = false; +vjs.MediaTechController.prototype['featuresPlaybackRate'] = false; + +// Optional events that we can manually mimic with timers +// currently not triggered by video-js-swf +vjs.MediaTechController.prototype['featuresProgressEvents'] = false; +vjs.MediaTechController.prototype['featuresTimeupdateEvents'] = false; + +vjs.MediaTechController.prototype['featuresNativeTextTracks'] = false; + +/** + * A functional mixin for techs that want to use the Source Handler pattern. + * + * ##### EXAMPLE: + * + * videojs.MediaTechController.withSourceHandlers.call(MyTech); + * + */ +vjs.MediaTechController.withSourceHandlers = function(Tech){ + /** + * Register a source handler + * Source handlers are scripts for handling specific formats. + * The source handler pattern is used for adaptive formats (HLS, DASH) that + * manually load video data and feed it into a Source Buffer (Media Source Extensions) + * @param {Function} handler The source handler + * @param {Boolean} first Register it before any existing handlers + */ + Tech.registerSourceHandler = function(handler, index){ + var handlers = Tech.sourceHandlers; + + if (!handlers) { + handlers = Tech.sourceHandlers = []; + } + + if (index === undefined) { + // add to the end of the list + index = handlers.length; + } + + handlers.splice(index, 0, handler); + }; + + /** + * Return the first source handler that supports the source + * TODO: Answer question: should 'probably' be prioritized over 'maybe' + * @param {Object} source The source object + * @returns {Object} The first source handler that supports the source + * @returns {null} Null if no source handler is found + */ + Tech.selectSourceHandler = function(source){ + var handlers = Tech.sourceHandlers || [], + can; + + for (var i = 0; i < handlers.length; i++) { + can = handlers[i].canHandleSource(source); + + if (can) { + return handlers[i]; + } + } + + return null; + }; + + /** + * Check if the tech can support the given source + * @param {Object} srcObj The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ + Tech.canPlaySource = function(srcObj){ + var sh = Tech.selectSourceHandler(srcObj); + + if (sh) { + return sh.canHandleSource(srcObj); + } + + return ''; + }; + + /** + * Create a function for setting the source using a source object + * and source handlers. + * Should never be called unless a source handler was found. + * @param {Object} source A source object with src and type keys + * @return {vjs.MediaTechController} self + */ + Tech.prototype.setSource = function(source){ + var sh = Tech.selectSourceHandler(source); + + if (!sh) { + // Fall back to a native source hander when unsupported sources are + // deliberately set + if (Tech.nativeSourceHandler) { + sh = Tech.nativeSourceHandler; + } else { + vjs.log.error('No source hander found for the current source.'); + } + } + + // Dispose any existing source handler + this.disposeSourceHandler(); + this.off('dispose', this.disposeSourceHandler); + + this.currentSource_ = source; + this.sourceHandler_ = sh.handleSource(source, this); + this.on('dispose', this.disposeSourceHandler); + + return this; + }; + + /** + * Clean up any existing source handler + */ + Tech.prototype.disposeSourceHandler = function(){ + if (this.sourceHandler_ && this.sourceHandler_.dispose) { + this.sourceHandler_.dispose(); + } + }; + +}; + +vjs.media = {}; + +})(); +/** + * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API + */ + +/** + * HTML5 Media Controller - Wrapper for HTML5 Media API + * @param {vjs.Player|Object} player + * @param {Object=} options + * @param {Function=} ready + * @constructor + */ +vjs.Html5 = vjs.MediaTechController.extend({ + /** @constructor */ + init: function(player, options, ready){ + var nodes, nodesLength, i, node, nodeName, removeNodes; + + if (options['nativeCaptions'] === false || options['nativeTextTracks'] === false) { + this['featuresNativeTextTracks'] = false; + } + + vjs.MediaTechController.call(this, player, options, ready); + + this.setupTriggers(); + + var source = options['source']; + + // Set the source if one is provided + // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) + // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source + // anyway so the error gets fired. + if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) { + this.setSource(source); + } + + if (this.el_.hasChildNodes()) { + + nodes = this.el_.childNodes; + nodesLength = nodes.length; + removeNodes = []; + + while (nodesLength--) { + node = nodes[nodesLength]; + nodeName = node.nodeName.toLowerCase(); + if (nodeName === 'track') { + if (!this['featuresNativeTextTracks']) { + // Empty video tag tracks so the built-in player doesn't use them also. + // This may not be fast enough to stop HTML5 browsers from reading the tags + // so we'll need to turn off any default tracks if we're manually doing + // captions and subtitles. videoElement.textTracks + removeNodes.push(node); + } else { + this.remoteTextTracks().addTrack_(node['track']); + } + } + } + + for (i=0; i= 0; i--) { - var attr = attrs[i]; - if (player.options_[attr] !== null) { - el[attr] = player.options_[attr]; + var settingsAttrs = ['autoplay','preload','loop','muted']; + for (i = settingsAttrs.length - 1; i >= 0; i--) { + var attr = settingsAttrs[i]; + var overwriteAttrs = {}; + if (typeof player.options_[attr] !== 'undefined') { + overwriteAttrs[attr] = player.options_[attr]; } + vjs.setElementAttributes(el, overwriteAttrs); } return el; // jenniisawesome = true; }; + +vjs.Html5.prototype.hideCaptions = function() { + var tracks = this.el_.querySelectorAll('track'), + track, + i = tracks.length, + kinds = { + 'captions': 1, + 'subtitles': 1 + }; + + while (i--) { + track = tracks[i].track; + if ((track && track['kind'] in kinds) && + (!tracks[i]['default'])) { + track.mode = 'disabled'; + } + } +}; + // Make video events trigger player events // May seem verbose here, but makes other APIs possible. +// Triggers removed using this.off when disposed vjs.Html5.prototype.setupTriggers = function(){ for (var i = vjs.Html5.Events.length - 1; i >= 0; i--) { - vjs.on(this.el_, vjs.Html5.Events[i], vjs.bind(this.player_, this.eventHandler)); + this.on(vjs.Html5.Events[i], this.eventHandler); } }; -// Triggers removed using this.off when disposed -vjs.Html5.prototype.eventHandler = function(e){ - this.trigger(e); +vjs.Html5.prototype.eventHandler = function(evt){ + // In the case of an error on the video element, set the error prop + // on the player and let the player handle triggering the event. On + // some platforms, error events fire that do not cause the error + // property on the video element to be set. See #1465 for an example. + if (evt.type == 'error' && this.error()) { + this.player().error(this.error().code); + + // in some cases we pass the event directly to the player + } else { + // No need for media events to bubble up. + evt.bubbles = false; - // No need for media events to bubble up. - e.stopPropagation(); + this.player().trigger(evt); + } }; vjs.Html5.prototype.useNativeControls = function(){ @@ -5454,6 +7346,20 @@ vjs.Html5.prototype.supportsFullScreen = function(){ vjs.Html5.prototype.enterFullScreen = function(){ var video = this.el_; + + if ('webkitDisplayingFullscreen' in video) { + this.one('webkitbeginfullscreen', function() { + this.player_.isFullscreen(true); + + this.one('webkitendfullscreen', function() { + this.player_.isFullscreen(false); + this.player_.trigger('fullscreenchange'); + }); + + this.player_.trigger('fullscreenchange'); + }); + } + if (video.paused && video.networkState <= video.HAVE_METADATA) { // attempt to prime the video element for programmatic access // this isn't necessary on the desktop but shouldn't hurt @@ -5461,7 +7367,7 @@ vjs.Html5.prototype.enterFullScreen = function(){ // playing and pausing synchronously during the transition to fullscreen // can get iOS ~6.1 devices into a play/pause loop - setTimeout(function(){ + this.setTimeout(function(){ video.pause(); video.webkitEnterFullScreen(); }, 0); @@ -5469,10 +7375,25 @@ vjs.Html5.prototype.enterFullScreen = function(){ video.webkitEnterFullScreen(); } }; + vjs.Html5.prototype.exitFullScreen = function(){ this.el_.webkitExitFullScreen(); }; -vjs.Html5.prototype.src = function(src){ this.el_.src = src; }; + + +vjs.Html5.prototype.src = function(src) { + if (src === undefined) { + return this.el_.src; + } else { + // Setting src through `src` instead of `setSrc` will be deprecated + this.setSrc(src); + } +}; + +vjs.Html5.prototype.setSrc = function(src) { + this.el_.src = src; +}; + vjs.Html5.prototype.load = function(){ this.el_.load(); }; vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; }; @@ -5496,55 +7417,278 @@ vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; }; vjs.Html5.prototype.ended = function(){ return this.el_.ended; }; vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; }; -/* HTML5 Support Testing ---------------------------------------------------- */ +vjs.Html5.prototype.playbackRate = function(){ return this.el_.playbackRate; }; +vjs.Html5.prototype.setPlaybackRate = function(val){ this.el_.playbackRate = val; }; -vjs.Html5.isSupported = function(){ - // ie9 with no Media Player is a LIAR! (#984) - try { - vjs.TEST_VID['volume'] = 0.5; - } catch (e) { - return false; +vjs.Html5.prototype.networkState = function(){ return this.el_.networkState; }; +vjs.Html5.prototype.readyState = function(){ return this.el_.readyState; }; + +vjs.Html5.prototype.textTracks = function() { + if (!this['featuresNativeTextTracks']) { + return vjs.MediaTechController.prototype.textTracks.call(this); } - return !!vjs.TEST_VID.canPlayType; + return this.el_.textTracks; }; - -vjs.Html5.canPlaySource = function(srcObj){ - // IE9 on Windows 7 without MediaPlayer throws an error here - // https://github.com/videojs/video.js/issues/519 - try { - return !!vjs.TEST_VID.canPlayType(srcObj.type); - } catch(e) { - return ''; +vjs.Html5.prototype.addTextTrack = function(kind, label, language) { + if (!this['featuresNativeTextTracks']) { + return vjs.MediaTechController.prototype.addTextTrack.call(this, kind, label, language); } - // TODO: Check Type - // If no Type, check ext - // Check Media Type -}; -vjs.Html5.canControlVolume = function(){ - var volume = vjs.TEST_VID.volume; - vjs.TEST_VID.volume = (volume / 2) + 0.1; - return volume !== vjs.TEST_VID.volume; + return this.el_.addTextTrack(kind, label, language); }; -// HTML5 Feature detection and Device Fixes --------------------------------- // -(function() { - var canPlayType, - mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i, - mp4RE = /^video\/mp4/i; +vjs.Html5.prototype.addRemoteTextTrack = function(options) { + if (!this['featuresNativeTextTracks']) { + return vjs.MediaTechController.prototype.addRemoteTextTrack.call(this, options); + } - vjs.Html5.patchCanPlayType = function() { - // Android 4.0 and above can play HLS to some extent but it reports being unable to do so - if (vjs.ANDROID_VERSION >= 4.0) { - if (!canPlayType) { - canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; - } + var track = document.createElement('track'); + options = options || {}; - vjs.TEST_VID.constructor.prototype.canPlayType = function(type) { - if (type && mpegurlRE.test(type)) { - return 'maybe'; - } + if (options['kind']) { + track['kind'] = options['kind']; + } + if (options['label']) { + track['label'] = options['label']; + } + if (options['language'] || options['srclang']) { + track['srclang'] = options['language'] || options['srclang']; + } + if (options['default']) { + track['default'] = options['default']; + } + if (options['id']) { + track['id'] = options['id']; + } + if (options['src']) { + track['src'] = options['src']; + } + + this.el().appendChild(track); + + if (track.track['kind'] === 'metadata') { + track['track']['mode'] = 'hidden'; + } else { + track['track']['mode'] = 'disabled'; + } + + track['onload'] = function() { + var tt = track['track']; + if (track.readyState >= 2) { + if (tt['kind'] === 'metadata' && tt['mode'] !== 'hidden') { + tt['mode'] = 'hidden'; + } else if (tt['kind'] !== 'metadata' && tt['mode'] !== 'disabled') { + tt['mode'] = 'disabled'; + } + track['onload'] = null; + } + }; + + this.remoteTextTracks().addTrack_(track.track); + + return track; +}; + +vjs.Html5.prototype.removeRemoteTextTrack = function(track) { + if (!this['featuresNativeTextTracks']) { + return vjs.MediaTechController.prototype.removeRemoteTextTrack.call(this, track); + } + + var tracks, i; + + this.remoteTextTracks().removeTrack_(track); + + tracks = this.el()['querySelectorAll']('track'); + + for (i = 0; i < tracks.length; i++) { + if (tracks[i] === track || tracks[i]['track'] === track) { + tracks[i]['parentNode']['removeChild'](tracks[i]); + break; + } + } +}; + +/* HTML5 Support Testing ---------------------------------------------------- */ + +/** + * Check if HTML5 video is supported by this browser/device + * @return {Boolean} + */ +vjs.Html5.isSupported = function(){ + // IE9 with no Media Player is a LIAR! (#984) + try { + vjs.TEST_VID['volume'] = 0.5; + } catch (e) { + return false; + } + + return !!vjs.TEST_VID.canPlayType; +}; + +// Add Source Handler pattern functions to this tech +vjs.MediaTechController.withSourceHandlers(vjs.Html5); + +/** + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * @param {Object} source The source object + * @param {vjs.Html5} tech The instance of the HTML5 tech + */ +vjs.Html5.nativeSourceHandler = {}; + +/** + * Check if the video element can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Html5.nativeSourceHandler.canHandleSource = function(source){ + var match, ext; + + function canPlayType(type){ + // IE9 on Windows 7 without MediaPlayer throws an error here + // https://github.com/videojs/video.js/issues/519 + try { + return vjs.TEST_VID.canPlayType(type); + } catch(e) { + return ''; + } + } + + // If a type was provided we should rely on that + if (source.type) { + return canPlayType(source.type); + } else if (source.src) { + // If no type, fall back to checking 'video/[EXTENSION]' + match = source.src.match(/\.([^.\/\?]+)(\?[^\/]+)?$/i); + ext = match && match[1]; + + return canPlayType('video/'+ext); + } + + return ''; +}; + +/** + * Pass the source to the video element + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Html5} tech The instance of the Html5 tech + */ +vjs.Html5.nativeSourceHandler.handleSource = function(source, tech){ + tech.setSrc(source.src); +}; + +/** + * Clean up the source handler when disposing the player or switching sources.. + * (no cleanup is needed when supporting the format natively) + */ +vjs.Html5.nativeSourceHandler.dispose = function(){}; + +// Register the native source handler +vjs.Html5.registerSourceHandler(vjs.Html5.nativeSourceHandler); + +/** + * Check if the volume can be changed in this browser/device. + * Volume cannot be changed in a lot of mobile devices. + * Specifically, it can't be changed from 1 on iOS. + * @return {Boolean} + */ +vjs.Html5.canControlVolume = function(){ + var volume = vjs.TEST_VID.volume; + vjs.TEST_VID.volume = (volume / 2) + 0.1; + return volume !== vjs.TEST_VID.volume; +}; + +/** + * Check if playbackRate is supported in this browser/device. + * @return {[type]} [description] + */ +vjs.Html5.canControlPlaybackRate = function(){ + var playbackRate = vjs.TEST_VID.playbackRate; + vjs.TEST_VID.playbackRate = (playbackRate / 2) + 0.1; + return playbackRate !== vjs.TEST_VID.playbackRate; +}; + +/** + * Check to see if native text tracks are supported by this browser/device + * @return {Boolean} + */ +vjs.Html5.supportsNativeTextTracks = function() { + var supportsTextTracks; + + // Figure out native text track support + // If mode is a number, we cannot change it because it'll disappear from view. + // Browsers with numeric modes include IE10 and older (<=2013) samsung android models. + // Firefox isn't playing nice either with modifying the mode + // TODO: Investigate firefox: https://github.com/videojs/video.js/issues/1862 + supportsTextTracks = !!vjs.TEST_VID.textTracks; + if (supportsTextTracks && vjs.TEST_VID.textTracks.length > 0) { + supportsTextTracks = typeof vjs.TEST_VID.textTracks[0]['mode'] !== 'number'; + } + if (supportsTextTracks && vjs.IS_FIREFOX) { + supportsTextTracks = false; + } + + return supportsTextTracks; +}; + +/** + * Set the tech's volume control support status + * @type {Boolean} + */ +vjs.Html5.prototype['featuresVolumeControl'] = vjs.Html5.canControlVolume(); + +/** + * Set the tech's playbackRate support status + * @type {Boolean} + */ +vjs.Html5.prototype['featuresPlaybackRate'] = vjs.Html5.canControlPlaybackRate(); + +/** + * Set the tech's status on moving the video element. + * In iOS, if you move a video element in the DOM, it breaks video playback. + * @type {Boolean} + */ +vjs.Html5.prototype['movingMediaElementInDOM'] = !vjs.IS_IOS; + +/** + * Set the the tech's fullscreen resize support status. + * HTML video is able to automatically resize when going to fullscreen. + * (No longer appears to be used. Can probably be removed.) + */ +vjs.Html5.prototype['featuresFullscreenResize'] = true; + +/** + * Set the tech's progress event support status + * (this disables the manual progress events of the MediaTechController) + */ +vjs.Html5.prototype['featuresProgressEvents'] = true; + +/** + * Sets the tech's status on native text track support + * @type {Boolean} + */ +vjs.Html5.prototype['featuresNativeTextTracks'] = vjs.Html5.supportsNativeTextTracks(); + +// HTML5 Feature detection and Device Fixes --------------------------------- // +(function() { + var canPlayType, + mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i, + mp4RE = /^video\/mp4/i; + + vjs.Html5.patchCanPlayType = function() { + // Android 4.0 and above can play HLS to some extent but it reports being unable to do so + if (vjs.ANDROID_VERSION >= 4.0) { + if (!canPlayType) { + canPlayType = vjs.TEST_VID.constructor.prototype.canPlayType; + } + + vjs.TEST_VID.constructor.prototype.canPlayType = function(type) { + if (type && mpegurlRE.test(type)) { + return 'maybe'; + } return canPlayType.call(this, type); }; } @@ -5669,38 +7813,18 @@ vjs.Flash = vjs.MediaTechController.extend({ // Merge default attributes with ones passed in attributes = vjs.obj.merge({ 'id': objId, - 'name': objId, // Both ID and Name needed or swf to identifty itself + 'name': objId, // Both ID and Name needed or swf to identify itself 'class': 'vjs-tech' - }, options['attributes']), - - lastSeekTarget + }, options['attributes']) ; // If source was supplied pass as a flash var. if (source) { - if (source.type && vjs.Flash.isStreamingType(source.type)) { - var parts = vjs.Flash.streamToParts(source.src); - flashVars['rtmpConnection'] = encodeURIComponent(parts.connection); - flashVars['rtmpStream'] = encodeURIComponent(parts.stream); - } - else { - flashVars['src'] = encodeURIComponent(vjs.getAbsoluteURL(source.src)); - } + this.ready(function(){ + this.setSource(source); + }); } - this['setCurrentTime'] = function(time){ - lastSeekTarget = time; - this.el_.vjs_setProperty('currentTime', time); - }; - this['currentTime'] = function(time){ - // when seeking make the reported time keep up with the requested time - // by reading the time we're seeking to - if (this.seeking()) { - return lastSeekTarget; - } - return this.el_.vjs_getProperty('currentTime'); - }; - // Add placeholder to player div vjs.insertFirst(placeHolder, parentEl); @@ -5710,7 +7834,7 @@ vjs.Flash = vjs.MediaTechController.extend({ this.ready(function(){ this.load(); this.play(); - this.currentTime(options['startTime']); + this['currentTime'](options['startTime']); }); } @@ -5718,141 +7842,18 @@ vjs.Flash = vjs.MediaTechController.extend({ // bugzilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=836786 if (vjs.IS_FIREFOX) { this.ready(function(){ - vjs.on(this.el(), 'mousemove', vjs.bind(this, function(){ + this.on('mousemove', function(){ // since it's a custom event, don't bubble higher than the player this.player().trigger({ 'type':'mousemove', 'bubbles': false }); - })); + }); }); } - // Flash iFrame Mode - // In web browsers there are multiple instances where changing the parent element or visibility of a plugin causes the plugin to reload. - // - Firefox just about always. https://bugzilla.mozilla.org/show_bug.cgi?id=90268 (might be fixed by version 13) - // - Webkit when hiding the plugin - // - Webkit and Firefox when using requestFullScreen on a parent element - // Loading the flash plugin into a dynamically generated iFrame gets around most of these issues. - // Issues that remain include hiding the element and requestFullScreen in Firefox specifically - - // There's on particularly annoying issue with this method which is that Firefox throws a security error on an offsite Flash object loaded into a dynamically created iFrame. - // Even though the iframe was inserted into a page on the web, Firefox + Flash considers it a local app trying to access an internet file. - // I tried mulitple ways of setting the iframe src attribute but couldn't find a src that worked well. Tried a real/fake source, in/out of domain. - // Also tried a method from stackoverflow that caused a security error in all browsers. http://stackoverflow.com/questions/2486901/how-to-set-document-domain-for-a-dynamically-generated-iframe - // In the end the solution I found to work was setting the iframe window.location.href right before doing a document.write of the Flash object. - // The only downside of this it seems to trigger another http request to the original page (no matter what's put in the href). Not sure why that is. - - // NOTE (2012-01-29): Cannot get Firefox to load the remote hosted SWF into a dynamically created iFrame - // Firefox 9 throws a security error, unleess you call location.href right before doc.write. - // Not sure why that even works, but it causes the browser to look like it's continuously trying to load the page. - // Firefox 3.6 keeps calling the iframe onload function anytime I write to it, causing an endless loop. - - if (options['iFrameMode'] === true && !vjs.IS_FIREFOX) { - - // Create iFrame with vjs-tech class so it's 100% width/height - var iFrm = vjs.createEl('iframe', { - 'id': objId + '_iframe', - 'name': objId + '_iframe', - 'className': 'vjs-tech', - 'scrolling': 'no', - 'marginWidth': 0, - 'marginHeight': 0, - 'frameBorder': 0 - }); - - // Update ready function names in flash vars for iframe window - flashVars['readyFunction'] = 'ready'; - flashVars['eventProxyFunction'] = 'events'; - flashVars['errorEventProxyFunction'] = 'errors'; - - // Tried multiple methods to get this to work in all browsers - - // Tried embedding the flash object in the page first, and then adding a place holder to the iframe, then replacing the placeholder with the page object. - // The goal here was to try to load the swf URL in the parent page first and hope that got around the firefox security error - // var newObj = vjs.Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); - // (in onload) - // var temp = vjs.createEl('a', { id:'asdf', innerHTML: 'asdf' } ); - // iDoc.body.appendChild(temp); - - // Tried embedding the flash object through javascript in the iframe source. - // This works in webkit but still triggers the firefox security error - // iFrm.src = 'javascript: document.write('"+vjs.Flash.getEmbedCode(options['swf'], flashVars, params, attributes)+"');"; - - // Tried an actual local iframe just to make sure that works, but it kills the easiness of the CDN version if you require the user to host an iframe - // We should add an option to host the iframe locally though, because it could help a lot of issues. - // iFrm.src = "iframe.html"; - - // Wait until iFrame has loaded to write into it. - vjs.on(iFrm, 'load', vjs.bind(this, function(){ - - var iDoc, - iWin = iFrm.contentWindow; - - // The one working method I found was to use the iframe's document.write() to create the swf object - // This got around the security issue in all browsers except firefox. - // I did find a hack where if I call the iframe's window.location.href='', it would get around the security error - // However, the main page would look like it was loading indefinitely (URL bar loading spinner would never stop) - // Plus Firefox 3.6 didn't work no matter what I tried. - // if (vjs.USER_AGENT.match('Firefox')) { - // iWin.location.href = ''; - // } - - // Get the iFrame's document depending on what the browser supports - iDoc = iFrm.contentDocument ? iFrm.contentDocument : iFrm.contentWindow.document; - - // Tried ensuring both document domains were the same, but they already were, so that wasn't the issue. - // Even tried adding /. that was mentioned in a browser security writeup - // document.domain = document.domain+'/.'; - // iDoc.domain = document.domain+'/.'; - - // Tried adding the object to the iframe doc's innerHTML. Security error in all browsers. - // iDoc.body.innerHTML = swfObjectHTML; - - // Tried appending the object to the iframe doc's body. Security error in all browsers. - // iDoc.body.appendChild(swfObject); - - // Using document.write actually got around the security error that browsers were throwing. - // Again, it's a dynamically generated (same domain) iframe, loading an external Flash swf. - // Not sure why that's a security issue, but apparently it is. - iDoc.write(vjs.Flash.getEmbedCode(options['swf'], flashVars, params, attributes)); - - // Setting variables on the window needs to come after the doc write because otherwise they can get reset in some browsers - // So far no issues with swf ready event being called before it's set on the window. - iWin['player'] = this.player_; - - // Create swf ready function for iFrame window - iWin['ready'] = vjs.bind(this.player_, function(currSwf){ - var el = iDoc.getElementById(currSwf), - player = this, - tech = player.tech; - - // Update reference to playback technology element - tech.el_ = el; - - // Make sure swf is actually ready. Sometimes the API isn't actually yet. - vjs.Flash.checkReady(tech); - }); - - // Create event listener for all swf events - iWin['events'] = vjs.bind(this.player_, function(swfID, eventName){ - var player = this; - if (player && player.techName === 'flash') { - player.trigger(eventName); - } - }); - - // Create error listener for all swf errors - iWin['errors'] = vjs.bind(this.player_, function(swfID, eventName){ - vjs.log('Flash Error', eventName); - }); + // native click events on the SWF aren't triggered on IE11, Win8.1RT + // use stageclick events triggered from inside the SWF instead + player.on('stageclick', player.reportUserActivity); - })); - - // Replace placeholder with iFrame (it will load now) - placeHolder.parentNode.replaceChild(iFrm, placeHolder); - - // If not using iFrame mode, embed as normal object - } else { - vjs.Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); - } + this.el_ = vjs.Flash.embed(options['swf'], placeHolder, flashVars, params, attributes); } }); @@ -5870,39 +7871,47 @@ vjs.Flash.prototype.pause = function(){ vjs.Flash.prototype.src = function(src){ if (src === undefined) { - return this.currentSrc(); + return this['currentSrc'](); } - if (vjs.Flash.isStreamingSrc(src)) { - src = vjs.Flash.streamToParts(src); - this.setRtmpConnection(src.connection); - this.setRtmpStream(src.stream); - } else { - // Make sure source URL is abosolute. - src = vjs.getAbsoluteURL(src); - this.el_.vjs_src(src); - } + // Setting src through `src` not `setSrc` will be deprecated + return this.setSrc(src); +}; + +vjs.Flash.prototype.setSrc = function(src){ + // Make sure source URL is absolute. + src = vjs.getAbsoluteURL(src); + this.el_.vjs_src(src); // Currently the SWF doesn't autoplay if you load a source later. // e.g. Load player w/ no source, wait 2s, set src. if (this.player_.autoplay()) { var tech = this; - setTimeout(function(){ tech.play(); }, 0); + this.setTimeout(function(){ tech.play(); }, 0); } }; -vjs.Flash.prototype.currentSrc = function(){ - var src = this.el_.vjs_getProperty('currentSrc'); - // no src, check and see if RTMP - if (src == null) { - var connection = this['rtmpConnection'](), - stream = this['rtmpStream'](); +vjs.Flash.prototype['setCurrentTime'] = function(time){ + this.lastSeekTarget_ = time; + this.el_.vjs_setProperty('currentTime', time); + vjs.MediaTechController.prototype.setCurrentTime.call(this); +}; - if (connection && stream) { - src = vjs.Flash.streamFromParts(connection, stream); - } +vjs.Flash.prototype['currentTime'] = function(time){ + // when seeking make the reported time keep up with the requested time + // by reading the time we're seeking to + if (this.seeking()) { + return this.lastSeekTarget_ || 0; + } + return this.el_.vjs_getProperty('currentTime'); +}; + +vjs.Flash.prototype['currentSrc'] = function(){ + if (this.currentSource_) { + return this.currentSource_.src; + } else { + return this.el_.vjs_getProperty('currentSrc'); } - return src; }; vjs.Flash.prototype.load = function(){ @@ -5912,7 +7921,7 @@ vjs.Flash.prototype.load = function(){ vjs.Flash.prototype.poster = function(){ this.el_.vjs_getProperty('poster'); }; -vjs.Flash.prototype.setPoster = function(){ +vjs.Flash.prototype['setPoster'] = function(){ // poster images are not handled by the Flash tech so make this a no-op }; @@ -5928,32 +7937,22 @@ vjs.Flash.prototype.enterFullScreen = function(){ return false; }; - -// Create setters and getters for attributes -var api = vjs.Flash.prototype, +(function(){ + // Create setters and getters for attributes + var api = vjs.Flash.prototype, readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','), - readOnly = 'error,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks'.split(','); + readOnly = 'error,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight'.split(','), // Overridden: buffered, currentTime, currentSrc + i; -/** - * @this {*} - * @private - */ -var createSetter = function(attr){ - var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); - api['set'+attrUpper] = function(val){ return this.el_.vjs_setProperty(attr, val); }; -}; - -/** - * @this {*} - * @private - */ -var createGetter = function(attr){ - api[attr] = function(){ return this.el_.vjs_getProperty(attr); }; -}; + function createSetter(attr){ + var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); + api['set'+attrUpper] = function(val){ return this.el_.vjs_setProperty(attr, val); }; + } + function createGetter(attr) { + api[attr] = function(){ return this.el_.vjs_getProperty(attr); }; + } -(function(){ - var i; // Create getter and setters for all read/write attributes for (i = 0; i < readWrite.length; i++) { createGetter(readWrite[i]); @@ -5973,19 +7972,59 @@ vjs.Flash.isSupported = function(){ // return swfobject.hasFlashPlayerVersion('10'); }; -vjs.Flash.canPlaySource = function(srcObj){ +// Add Source Handler pattern functions to this tech +vjs.MediaTechController.withSourceHandlers(vjs.Flash); + +/** + * The default native source handler. + * This simply passes the source to the video element. Nothing fancy. + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.nativeSourceHandler = {}; + +/** + * Check Flash can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Flash.nativeSourceHandler.canHandleSource = function(source){ var type; - if (!srcObj.type) { + if (!source.type) { return ''; } - type = srcObj.type.replace(/;.*/,'').toLowerCase(); - if (type in vjs.Flash.formats || type in vjs.Flash.streamingFormats) { + // Strip code information from the type because we don't get that specific + type = source.type.replace(/;.*/,'').toLowerCase(); + + if (type in vjs.Flash.formats) { return 'maybe'; } + + return ''; +}; + +/** + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.nativeSourceHandler.handleSource = function(source, tech){ + tech.setSrc(source.src); }; +/** + * Clean up the source handler when disposing the player or switching sources.. + * (no cleanup is needed when supporting the format natively) + */ +vjs.Flash.nativeSourceHandler.dispose = function(){}; + +// Register the native source handler +vjs.Flash.registerSourceHandler(vjs.Flash.nativeSourceHandler); + vjs.Flash.formats = { 'video/flv': 'FLV', 'video/x-flv': 'FLV', @@ -5993,45 +8032,41 @@ vjs.Flash.formats = { 'video/m4v': 'MP4' }; -vjs.Flash.streamingFormats = { - 'rtmp/mp4': 'MP4', - 'rtmp/flv': 'FLV' -}; - vjs.Flash['onReady'] = function(currSwf){ - var el = vjs.el(currSwf); + var el, player; - // Get player from box - // On firefox reloads, el might already have a player - var player = el['player'] || el.parentNode['player'], - tech = player.tech; + el = vjs.el(currSwf); - // Reference player on tech element - el['player'] = player; + // get player from the player div property + player = el && el.parentNode && el.parentNode['player']; - // Update reference to playback technology element - tech.el_ = el; - - vjs.Flash.checkReady(tech); + // if there is no el or player then the tech has been disposed + // and the tech element was removed from the player div + if (player) { + // reference player on tech element + el['player'] = player; + // check that the flash object is really ready + vjs.Flash['checkReady'](player.tech); + } }; -// The SWF isn't alwasy ready when it says it is. Sometimes the API functions still need to be added to the object. +// The SWF isn't always ready when it says it is. Sometimes the API functions still need to be added to the object. // If it's not ready, we set a timeout to check again shortly. -vjs.Flash.checkReady = function(tech){ +vjs.Flash['checkReady'] = function(tech){ + // stop worrying if the tech has been disposed + if (!tech.el()) { + return; + } - // Check if API property exists + // check if API property exists if (tech.el().vjs_getProperty) { - - // If so, tell tech it's ready + // tell tech it's ready tech.triggerReady(); - - // Otherwise wait longer. } else { - - setTimeout(function(){ - vjs.Flash.checkReady(tech); + // wait longer + this.setTimeout(function(){ + vjs.Flash['checkReady'](tech); }, 50); - } }; @@ -6044,8 +8079,15 @@ vjs.Flash['onEvent'] = function(swfID, eventName){ // Log errors from the swf vjs.Flash['onError'] = function(swfID, err){ var player = vjs.el(swfID)['player']; - player.trigger('error'); - vjs.log('Flash Error', err, swfID); + var msg = 'FLASH: '+err; + + if (err == 'srcnotfound') { + player.error({ code: 4, message: msg }); + + // errors we haven't categorized into the media errors + } else { + player.error(msg); + } }; // Flash Version Check @@ -6092,7 +8134,7 @@ vjs.Flash.embed = function(swf, placeHolder, flashVars, params, attributes){ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ - var objTag = '' + paramsString + ''; }; +vjs.Flash.streamingFormats = { + 'rtmp/mp4': 'MP4', + 'rtmp/flv': 'FLV' +}; vjs.Flash.streamFromParts = function(connection, stream) { return connection + '&' + stream; @@ -6182,6 +8228,42 @@ vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i; vjs.Flash.isStreamingSrc = function(src) { return vjs.Flash.RTMP_RE.test(src); }; + +/** + * A source handler for RTMP urls + * @type {Object} + */ +vjs.Flash.rtmpSourceHandler = {}; + +/** + * Check Flash can handle the source natively + * @param {Object} source The source object + * @return {String} 'probably', 'maybe', or '' (empty string) + */ +vjs.Flash.rtmpSourceHandler.canHandleSource = function(source){ + if (vjs.Flash.isStreamingType(source.type) || vjs.Flash.isStreamingSrc(source.src)) { + return 'maybe'; + } + + return ''; +}; + +/** + * Pass the source to the flash object + * Adaptive source handlers will have more complicated workflows before passing + * video data to the video element + * @param {Object} source The source object + * @param {vjs.Flash} tech The instance of the Flash tech + */ +vjs.Flash.rtmpSourceHandler.handleSource = function(source, tech){ + var srcParts = vjs.Flash.streamToParts(source.src); + + tech['setRtmpConnection'](srcParts.connection); + tech['setRtmpStream'](srcParts.stream); +}; + +// Register the native source handler +vjs.Flash.registerSourceHandler(vjs.Flash.rtmpSourceHandler); /** * The Media Loader is the component that decides which playback technology to load * when the player is initialized. @@ -6215,736 +8297,723 @@ vjs.MediaLoader = vjs.Component.extend({ } } }); -/** - * @fileoverview Text Tracks - * Text tracks are tracks of timed text events. - * Captions - text displayed over the video for the hearing impared - * Subtitles - text displayed over the video for those who don't understand langauge in the video - * Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video - * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device - */ - -// Player Additions - Functions add to the player object for easier access to tracks - -/** - * List of associated text tracks - * @type {Array} - * @private +/* + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode + * + * enum TextTrackMode { "disabled", "hidden", "showing" }; */ -vjs.Player.prototype.textTracks_; +vjs.TextTrackMode = { + 'disabled': 'disabled', + 'hidden': 'hidden', + 'showing': 'showing' +}; -/** - * Get an array of associated text tracks. captions, subtitles, chapters, descriptions - * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks - * @return {Array} Array of track objects - * @private +/* + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackkind + * + * enum TextTrackKind { "subtitles", "captions", "descriptions", "chapters", "metadata" }; */ -vjs.Player.prototype.textTracks = function(){ - this.textTracks_ = this.textTracks_ || []; - return this.textTracks_; +vjs.TextTrackKind = { + 'subtitles': 'subtitles', + 'captions': 'captions', + 'descriptions': 'descriptions', + 'chapters': 'chapters', + 'metadata': 'metadata' }; - -/** - * Add a text track - * In addition to the W3C settings we allow adding additional info through options. - * http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack - * @param {String} kind Captions, subtitles, chapters, descriptions, or metadata - * @param {String=} label Optional label - * @param {String=} language Optional language - * @param {Object=} options Additional track options, like src - * @private +(function() { +/* + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack + * + * interface TextTrack : EventTarget { + * readonly attribute TextTrackKind kind; + * readonly attribute DOMString label; + * readonly attribute DOMString language; + * + * readonly attribute DOMString id; + * readonly attribute DOMString inBandMetadataTrackDispatchType; + * + * attribute TextTrackMode mode; + * + * readonly attribute TextTrackCueList? cues; + * readonly attribute TextTrackCueList? activeCues; + * + * void addCue(TextTrackCue cue); + * void removeCue(TextTrackCue cue); + * + * attribute EventHandler oncuechange; + * }; */ -vjs.Player.prototype.addTextTrack = function(kind, label, language, options){ - var tracks = this.textTracks_ = this.textTracks_ || []; - options = options || {}; - options['kind'] = kind; - options['label'] = label; - options['language'] = language; +vjs.TextTrack = function(options) { + var tt, id, mode, kind, label, language, cues, activeCues, timeupdateHandler, changed, prop; - // HTML5 Spec says default to subtitles. - // Uppercase first letter to match class names - var Kind = vjs.capitalize(kind || 'subtitles'); + options = options || {}; - // Create correct texttrack class. CaptionsTrack, etc. - var track = new window['videojs'][Kind + 'Track'](this, options); + if (!options['player']) { + throw new Error('A player was not provided.'); + } - tracks.push(track); + tt = this; + if (vjs.IS_IE8) { + tt = document.createElement('custom'); - // If track.dflt() is set, start showing immediately - // TODO: Add a process to deterime the best track to show for the specific kind - // Incase there are mulitple defaulted tracks of the same kind - // Or the user has a set preference of a specific language that should override the default - // if (track.dflt()) { - // this.ready(vjs.bind(track, track.show)); - // } + for (prop in vjs.TextTrack.prototype) { + tt[prop] = vjs.TextTrack.prototype[prop]; + } + } - return track; -}; + tt.player_ = options['player']; -/** - * Add an array of text tracks. captions, subtitles, chapters, descriptions - * Track objects will be stored in the player.textTracks() array - * @param {Array} trackList Array of track elements or objects (fake track elements) - * @private - */ -vjs.Player.prototype.addTextTracks = function(trackList){ - var trackObj; + mode = vjs.TextTrackMode[options['mode']] || 'disabled'; + kind = vjs.TextTrackKind[options['kind']] || 'subtitles'; + label = options['label'] || ''; + language = options['language'] || options['srclang'] || ''; + id = options['id'] || 'vjs_text_track_' + vjs.guid++; - for (var i = 0; i < trackList.length; i++) { - trackObj = trackList[i]; - this.addTextTrack(trackObj['kind'], trackObj['label'], trackObj['language'], trackObj); + if (kind === 'metadata' || kind === 'chapters') { + mode = 'hidden'; } - return this; -}; - -// Show a text track -// disableSameKind: disable all other tracks of the same kind. Value should be a track kind (captions, etc.) -vjs.Player.prototype.showTextTrack = function(id, disableSameKind){ - var tracks = this.textTracks_, - i = 0, - j = tracks.length, - track, showTrack, kind; + tt.cues_ = []; + tt.activeCues_ = []; - // Find Track with same ID - for (;i 0) { - track.disable(); + changed = false; + timeupdateHandler = vjs.bind(tt, function() { + this['activeCues']; + if (changed) { + this['trigger']('cuechange'); + changed = false; } + }); + if (mode !== 'disabled') { + tt.player_.on('timeupdate', timeupdateHandler); } - // Get track kind from shown track or disableSameKind - kind = (showTrack) ? showTrack.kind() : ((disableSameKind) ? disableSameKind : false); + Object.defineProperty(tt, 'kind', { + get: function() { + return kind; + }, + set: Function.prototype + }); - // Trigger trackchange event, captionstrackchange, subtitlestrackchange, etc. - if (kind) { - this.trigger(kind+'trackchange'); - } + Object.defineProperty(tt, 'label', { + get: function() { + return label; + }, + set: Function.prototype + }); - return this; -}; + Object.defineProperty(tt, 'language', { + get: function() { + return language; + }, + set: Function.prototype + }); -/** - * The base class for all text tracks - * - * Handles the parsing, hiding, and showing of text track cues - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -vjs.TextTrack = vjs.Component.extend({ - /** @constructor */ - init: function(player, options){ - vjs.Component.call(this, player, options); + Object.defineProperty(tt, 'id', { + get: function() { + return id; + }, + set: Function.prototype + }); - // Apply track info to track object - // Options will often be a track element + Object.defineProperty(tt, 'mode', { + get: function() { + return mode; + }, + set: function(newMode) { + if (!vjs.TextTrackMode[newMode]) { + return; + } + mode = newMode; + if (mode === 'showing') { + this.player_.on('timeupdate', timeupdateHandler); + } + this.trigger('modechange'); + } + }); - // Build ID if one doesn't exist - this.id_ = options['id'] || ('vjs_' + options['kind'] + '_' + options['language'] + '_' + vjs.guid++); - this.src_ = options['src']; - // 'default' is a reserved keyword in js so we use an abbreviated version - this.dflt_ = options['default'] || options['dflt']; - this.title_ = options['title']; - this.language_ = options['srclang']; - this.label_ = options['label']; - this.cues_ = []; - this.activeCues_ = []; - this.readyState_ = 0; - this.mode_ = 0; + Object.defineProperty(tt, 'cues', { + get: function() { + if (!this.loaded_) { + return null; + } - this.player_.on('fullscreenchange', vjs.bind(this, this.adjustFontSize)); - } -}); + return cues; + }, + set: Function.prototype + }); -/** - * Track kind value. Captions, subtitles, etc. - * @private - */ -vjs.TextTrack.prototype.kind_; + Object.defineProperty(tt, 'activeCues', { + get: function() { + var i, l, active, ct, cue; -/** - * Get the track kind value - * @return {String} - */ -vjs.TextTrack.prototype.kind = function(){ - return this.kind_; -}; + if (!this.loaded_) { + return null; + } -/** - * Track src value - * @private - */ -vjs.TextTrack.prototype.src_; + if (this['cues'].length === 0) { + return activeCues; // nothing to do + } -/** - * Get the track src value - * @return {String} - */ -vjs.TextTrack.prototype.src = function(){ - return this.src_; -}; + ct = this.player_.currentTime(); + i = 0; + l = this['cues'].length; + active = []; + + for (; i < l; i++) { + cue = this['cues'][i]; + if (cue['startTime'] <= ct && cue['endTime'] >= ct) { + active.push(cue); + } else if (cue['startTime'] === cue['endTime'] && cue['startTime'] <= ct && cue['startTime'] + 0.5 >= ct) { + active.push(cue); + } + } -/** - * Track default value - * If default is used, subtitles/captions to start showing - * @private - */ -vjs.TextTrack.prototype.dflt_; + changed = false; -/** - * Get the track default value. ('default' is a reserved keyword) - * @return {Boolean} - */ -vjs.TextTrack.prototype.dflt = function(){ - return this.dflt_; -}; + if (active.length !== this.activeCues_.length) { + changed = true; + } else { + for (i = 0; i < active.length; i++) { + if (indexOf.call(this.activeCues_, active[i]) === -1) { + changed = true; + } + } + } -/** - * Track title value - * @private - */ -vjs.TextTrack.prototype.title_; + this.activeCues_ = active; + activeCues.setCues_(this.activeCues_); -/** - * Get the track title value - * @return {String} - */ -vjs.TextTrack.prototype.title = function(){ - return this.title_; -}; + return activeCues; + }, + set: Function.prototype + }); -/** - * Language - two letter string to represent track language, e.g. 'en' for English - * Spec def: readonly attribute DOMString language; - * @private - */ -vjs.TextTrack.prototype.language_; + if (options.src) { + loadTrack(options.src, tt); + } else { + tt.loaded_ = true; + } -/** - * Get the track language value - * @return {String} - */ -vjs.TextTrack.prototype.language = function(){ - return this.language_; + if (vjs.IS_IE8) { + return tt; + } }; -/** - * Track label e.g. 'English' - * Spec def: readonly attribute DOMString label; - * @private - */ -vjs.TextTrack.prototype.label_; +vjs.TextTrack.prototype = vjs.obj.create(vjs.EventEmitter.prototype); +vjs.TextTrack.prototype.constructor = vjs.TextTrack; -/** - * Get the track label value - * @return {String} +/* + * cuechange - One or more cues in the track have become active or stopped being active. */ -vjs.TextTrack.prototype.label = function(){ - return this.label_; +vjs.TextTrack.prototype.allowedEvents_ = { + 'cuechange': 'cuechange' }; -/** - * All cues of the track. Cues have a startTime, endTime, text, and other properties. - * Spec def: readonly attribute TextTrackCueList cues; - * @private - */ -vjs.TextTrack.prototype.cues_; - -/** - * Get the track cues - * @return {Array} - */ -vjs.TextTrack.prototype.cues = function(){ - return this.cues_; -}; +vjs.TextTrack.prototype.addCue = function(cue) { + var tracks = this.player_.textTracks(), + i = 0; -/** - * ActiveCues is all cues that are currently showing - * Spec def: readonly attribute TextTrackCueList activeCues; - * @private - */ -vjs.TextTrack.prototype.activeCues_; + if (tracks) { + for (; i < tracks.length; i++) { + if (tracks[i] !== this) { + tracks[i].removeCue(cue); + } + } + } -/** - * Get the track active cues - * @return {Array} - */ -vjs.TextTrack.prototype.activeCues = function(){ - return this.activeCues_; + this.cues_.push(cue); + this['cues'].setCues_(this.cues_); }; -/** - * ReadyState describes if the text file has been loaded - * const unsigned short NONE = 0; - * const unsigned short LOADING = 1; - * const unsigned short LOADED = 2; - * const unsigned short ERROR = 3; - * readonly attribute unsigned short readyState; - * @private - */ -vjs.TextTrack.prototype.readyState_; - -/** - * Get the track readyState - * @return {Number} - */ -vjs.TextTrack.prototype.readyState = function(){ - return this.readyState_; -}; +vjs.TextTrack.prototype.removeCue = function(removeCue) { + var i = 0, + l = this.cues_.length, + cue, + removed = false; -/** - * Mode describes if the track is showing, hidden, or disabled - * const unsigned short OFF = 0; - * const unsigned short HIDDEN = 1; (still triggering cuechange events, but not visible) - * const unsigned short SHOWING = 2; - * attribute unsigned short mode; - * @private - */ -vjs.TextTrack.prototype.mode_; + for (; i < l; i++) { + cue = this.cues_[i]; + if (cue === removeCue) { + this.cues_.splice(i, 1); + removed = true; + } + } -/** - * Get the track mode - * @return {Number} - */ -vjs.TextTrack.prototype.mode = function(){ - return this.mode_; + if (removed) { + this.cues.setCues_(this.cues_); + } }; -/** - * Change the font size of the text track to make it larger when playing in fullscreen mode - * and restore it to its normal size when not in fullscreen mode. +/* + * Downloading stuff happens below this point */ -vjs.TextTrack.prototype.adjustFontSize = function(){ - if (this.player_.isFullScreen()) { - // Scale the font by the same factor as increasing the video width to the full screen window width. - // Additionally, multiply that factor by 1.4, which is the default font size for - // the caption track (from the CSS) - this.el_.style.fontSize = screen.width / this.player_.width() * 1.4 * 100 + '%'; - } else { - // Change the font size of the text track back to its original non-fullscreen size - this.el_.style.fontSize = ''; +var loadTrack, parseCues, indexOf; + +loadTrack = function(src, track) { + vjs.xhr(src, vjs.bind(this, function(err, response, responseBody){ + if (err) { + return vjs.log.error(err); } -}; -/** - * Create basic div to hold cue text - * @return {Element} - */ -vjs.TextTrack.prototype.createEl = function(){ - return vjs.Component.prototype.createEl.call(this, 'div', { - className: 'vjs-' + this.kind_ + ' vjs-text-track' - }); + + track.loaded_ = true; + parseCues(responseBody, track); + })); }; -/** - * Show: Mode Showing (2) - * Indicates that the text track is active. If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. - * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. - * In addition, for text tracks whose kind is subtitles or captions, the cues are being displayed over the video as appropriate; - * for text tracks whose kind is descriptions, the user agent is making the cues available to the user in a non-visual fashion; - * and for text tracks whose kind is chapters, the user agent is making available to the user a mechanism by which the user can navigate to any point in the media resource by selecting a cue. - * The showing by default state is used in conjunction with the default attribute on track elements to indicate that the text track was enabled due to that attribute. - * This allows the user agent to override the state if a later track is discovered that is more appropriate per the user's preferences. - */ -vjs.TextTrack.prototype.show = function(){ - this.activate(); +parseCues = function(srcContent, track) { + if (typeof window['WebVTT'] !== 'function') { + //try again a bit later + return window.setTimeout(function() { + parseCues(srcContent, track); + }, 25); + } - this.mode_ = 2; + var parser = new window['WebVTT']['Parser'](window, window['vttjs'], window['WebVTT']['StringDecoder']()); + + parser['oncue'] = function(cue) { + track.addCue(cue); + }; + parser['onparsingerror'] = function(error) { + vjs.log.error(error); + }; - // Show element. - vjs.Component.prototype.show.call(this); + parser['parse'](srcContent); + parser['flush'](); }; -/** - * Hide: Mode Hidden (1) - * Indicates that the text track is active, but that the user agent is not actively displaying the cues. - * If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily. - * The user agent is maintaining a list of which cues are active, and events are being fired accordingly. - */ -vjs.TextTrack.prototype.hide = function(){ - // When hidden, cues are still triggered. Disable to stop triggering. - this.activate(); +indexOf = function(searchElement, fromIndex) { - this.mode_ = 1; + var k; - // Hide element. - vjs.Component.prototype.hide.call(this); -}; + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } -/** - * Disable: Mode Off/Disable (0) - * Indicates that the text track is not active. Other than for the purposes of exposing the track in the DOM, the user agent is ignoring the text track. - * No cues are active, no events are fired, and the user agent will not attempt to obtain the track's cues. - */ -vjs.TextTrack.prototype.disable = function(){ - // If showing, hide. - if (this.mode_ == 2) { this.hide(); } + var O = Object(this); - // Stop triggering cues - this.deactivate(); + var len = O.length >>> 0; - // Switch Mode to Off - this.mode_ = 0; -}; + if (len === 0) { + return -1; + } -/** - * Turn on cue tracking. Tracks that are showing OR hidden are active. - */ -vjs.TextTrack.prototype.activate = function(){ - // Load text file if it hasn't been yet. - if (this.readyState_ === 0) { this.load(); } + var n = +fromIndex || 0; + + if (Math.abs(n) === Infinity) { + n = 0; + } - // Only activate if not already active. - if (this.mode_ === 0) { - // Update current cue on timeupdate - // Using unique ID for bind function so other tracks don't remove listener - this.player_.on('timeupdate', vjs.bind(this, this.update, this.id_)); + if (n >= len) { + return -1; + } - // Reset cue time on media end - this.player_.on('ended', vjs.bind(this, this.reset, this.id_)); + k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); - // Add to display - if (this.kind_ === 'captions' || this.kind_ === 'subtitles') { - this.player_.getChild('textTrackDisplay').addChild(this); + while (k < len) { + if (k in O && O[k] === searchElement) { + return k; } + k++; } + return -1; }; -/** - * Turn off cue tracking. +})(); +/* + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist + * + * interface TextTrackList : EventTarget { + * readonly attribute unsigned long length; + * getter TextTrack (unsigned long index); + * TextTrack? getTrackById(DOMString id); + * + * attribute EventHandler onchange; + * attribute EventHandler onaddtrack; + * attribute EventHandler onremovetrack; + * }; */ -vjs.TextTrack.prototype.deactivate = function(){ - // Using unique ID for bind function so other tracks don't remove listener - this.player_.off('timeupdate', vjs.bind(this, this.update, this.id_)); - this.player_.off('ended', vjs.bind(this, this.reset, this.id_)); - this.reset(); // Reset - - // Remove from display - this.player_.getChild('textTrackDisplay').removeChild(this); -}; +vjs.TextTrackList = function(tracks) { + var list = this, + prop, + i = 0; -// A readiness state -// One of the following: -// -// Not loaded -// Indicates that the text track is known to exist (e.g. it has been declared with a track element), but its cues have not been obtained. -// -// Loading -// Indicates that the text track is loading and there have been no fatal errors encountered so far. Further cues might still be added to the track. -// -// Loaded -// Indicates that the text track has been loaded with no fatal errors. No new cues will be added to the track except if the text track corresponds to a MutableTextTrack object. -// -// Failed to load -// Indicates that the text track was enabled, but when the user agent attempted to obtain it, this failed in some way (e.g. URL could not be resolved, network error, unknown text track format). Some or all of the cues are likely missing and will not be obtained. -vjs.TextTrack.prototype.load = function(){ + if (vjs.IS_IE8) { + list = document.createElement('custom'); - // Only load if not loaded yet. - if (this.readyState_ === 0) { - this.readyState_ = 1; - vjs.get(this.src_, vjs.bind(this, this.parseCues), vjs.bind(this, this.onError)); + for (prop in vjs.TextTrackList.prototype) { + list[prop] = vjs.TextTrackList.prototype[prop]; + } } -}; + tracks = tracks || []; + list.tracks_ = []; -vjs.TextTrack.prototype.onError = function(err){ - this.error = err; - this.readyState_ = 3; - this.trigger('error'); -}; + Object.defineProperty(list, 'length', { + get: function() { + return this.tracks_.length; + } + }); -// Parse the WebVTT text format for cue times. -// TODO: Separate parser into own class so alternative timed text formats can be used. (TTML, DFXP) -vjs.TextTrack.prototype.parseCues = function(srcContent) { - var cue, time, text, - lines = srcContent.split('\n'), - line = '', id; + for (; i < tracks.length; i++) { + list.addTrack_(tracks[i]); + } - for (var i=1, j=lines.length; i') == -1) { - id = line; - // Advance to next line for timing. - line = vjs.trim(lines[++i]); - } else { - id = this.cues_.length; - } +// emulate attribute EventHandler support to allow for feature detection +(function() { + var event; - // First line - Number - cue = { - id: id, // Cue Number - index: this.cues_.length // Position in Array - }; + for (event in vjs.TextTrackList.prototype.allowedEvents_) { + vjs.TextTrackList.prototype['on' + event] = null; + } +})(); - // Timing line - time = line.split(' --> '); - cue.startTime = this.parseCueTime(time[0]); - cue.endTime = this.parseCueTime(time[1]); +vjs.TextTrackList.prototype.addTrack_ = function(track) { + var index = this.tracks_.length; + if (!(''+index in this)) { + Object.defineProperty(this, index, { + get: function() { + return this.tracks_[index]; + } + }); + } - // Additional lines - Cue Text - text = []; + track.addEventListener('modechange', vjs.bind(this, function() { + this.trigger('change'); + })); + this.tracks_.push(track); - // Loop until a blank line or end of lines - // Assumeing trim('') returns false for blank lines - while (lines[++i] && (line = vjs.trim(lines[i]))) { - text.push(line); - } + this.trigger({ + type: 'addtrack', + track: track + }); +}; - cue.text = text.join('
'); +vjs.TextTrackList.prototype.removeTrack_ = function(rtrack) { + var i = 0, + l = this.length, + result = null, + track; - // Add this cue - this.cues_.push(cue); + for (; i < l; i++) { + track = this[i]; + if (track === rtrack) { + this.tracks_.splice(i, 1); + break; } } - this.readyState_ = 2; - this.trigger('loaded'); + this.trigger({ + type: 'removetrack', + track: rtrack + }); }; +vjs.TextTrackList.prototype.getTrackById = function(id) { + var i = 0, + l = this.length, + result = null, + track; -vjs.TextTrack.prototype.parseCueTime = function(timeText) { - var parts = timeText.split(':'), - time = 0, - hours, minutes, other, seconds, ms; - - // Check if optional hours place is included - // 00:00:00.000 vs. 00:00.000 - if (parts.length == 3) { - hours = parts[0]; - minutes = parts[1]; - other = parts[2]; - } else { - hours = 0; - minutes = parts[0]; - other = parts[1]; - } - - // Break other (seconds, milliseconds, and flags) by spaces - // TODO: Make additional cue layout settings work with flags - other = other.split(/\s+/); - // Remove seconds. Seconds is the first part before any spaces. - seconds = other.splice(0,1)[0]; - // Could use either . or , for decimal - seconds = seconds.split(/\.|,/); - // Get milliseconds - ms = parseFloat(seconds[1]); - seconds = seconds[0]; - - // hours => seconds - time += parseFloat(hours) * 3600; - // minutes => seconds - time += parseFloat(minutes) * 60; - // Add seconds - time += parseFloat(seconds); - // Add milliseconds - if (ms) { time += ms/1000; } - - return time; -}; - -// Update active cues whenever timeupdate events are triggered on the player. -vjs.TextTrack.prototype.update = function(){ - if (this.cues_.length > 0) { - - // Get curent player time - var time = this.player_.currentTime(); - - // Check if the new time is outside the time box created by the the last update. - if (this.prevChange === undefined || time < this.prevChange || this.nextChange <= time) { - var cues = this.cues_, - - // Create a new time box for this state. - newNextChange = this.player_.duration(), // Start at beginning of the timeline - newPrevChange = 0, // Start at end - - reverse = false, // Set the direction of the loop through the cues. Optimized the cue check. - newCues = [], // Store new active cues. - - // Store where in the loop the current active cues are, to provide a smart starting point for the next loop. - firstActiveIndex, lastActiveIndex, - cue, i; // Loop vars - - // Check if time is going forwards or backwards (scrubbing/rewinding) - // If we know the direction we can optimize the starting position and direction of the loop through the cues array. - if (time >= this.nextChange || this.nextChange === undefined) { // NextChange should happen - // Forwards, so start at the index of the first active cue and loop forward - i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0; - } else { - // Backwards, so start at the index of the last active cue and loop backward - reverse = true; - i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1; - } - - while (true) { // Loop until broken - cue = cues[i]; - - // Cue ended at this point - if (cue.endTime <= time) { - newPrevChange = Math.max(newPrevChange, cue.endTime); - - if (cue.active) { - cue.active = false; - } - - // No earlier cues should have an active start time. - // Nevermind. Assume first cue could have a duration the same as the video. - // In that case we need to loop all the way back to the beginning. - // if (reverse && cue.startTime) { break; } + for (; i < l; i++) { + track = this[i]; + if (track.id === id) { + result = track; + break; + } + } - // Cue hasn't started - } else if (time < cue.startTime) { - newNextChange = Math.min(newNextChange, cue.startTime); + return result; +}; +/* + * https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist + * + * interface TextTrackCueList { + * readonly attribute unsigned long length; + * getter TextTrackCue (unsigned long index); + * TextTrackCue? getCueById(DOMString id); + * }; + */ - if (cue.active) { - cue.active = false; - } +vjs.TextTrackCueList = function(cues) { + var list = this, + prop; - // No later cues should have an active start time. - if (!reverse) { break; } + if (vjs.IS_IE8) { + list = document.createElement('custom'); - // Cue is current - } else { + for (prop in vjs.TextTrackCueList.prototype) { + list[prop] = vjs.TextTrackCueList.prototype[prop]; + } + } - if (reverse) { - // Add cue to front of array to keep in time order - newCues.splice(0,0,cue); + vjs.TextTrackCueList.prototype.setCues_.call(list, cues); - // If in reverse, the first current cue is our lastActiveCue - if (lastActiveIndex === undefined) { lastActiveIndex = i; } - firstActiveIndex = i; - } else { - // Add cue to end of array - newCues.push(cue); + Object.defineProperty(list, 'length', { + get: function() { + return this.length_; + } + }); - // If forward, the first current cue is our firstActiveIndex - if (firstActiveIndex === undefined) { firstActiveIndex = i; } - lastActiveIndex = i; - } + if (vjs.IS_IE8) { + return list; + } +}; - newNextChange = Math.min(newNextChange, cue.endTime); - newPrevChange = Math.max(newPrevChange, cue.startTime); +vjs.TextTrackCueList.prototype.setCues_ = function(cues) { + var oldLength = this.length || 0, + i = 0, + l = cues.length, + defineProp; - cue.active = true; - } + this.cues_ = cues; + this.length_ = cues.length; - if (reverse) { - // Reverse down the array of cues, break if at first - if (i === 0) { break; } else { i--; } - } else { - // Walk up the array fo cues, break if at last - if (i === cues.length - 1) { break; } else { i++; } + defineProp = function(i) { + if (!(''+i in this)) { + Object.defineProperty(this, '' + i, { + get: function() { + return this.cues_[i]; } + }); + } + }; - } - - this.activeCues_ = newCues; - this.nextChange = newNextChange; - this.prevChange = newPrevChange; - this.firstActiveIndex = firstActiveIndex; - this.lastActiveIndex = lastActiveIndex; - - this.updateDisplay(); - - this.trigger('cuechange'); + if (oldLength < l) { + i = oldLength; + for(; i < l; i++) { + defineProp.call(this, i); } } }; -// Add cue HTML to display -vjs.TextTrack.prototype.updateDisplay = function(){ - var cues = this.activeCues_, - html = '', - i=0,j=cues.length; +vjs.TextTrackCueList.prototype.getCueById = function(id) { + var i = 0, + l = this.length, + result = null, + cue; - for (;i'; + for (; i < l; i++) { + cue = this[i]; + if (cue.id === id) { + result = cue; + break; + } } - this.el_.innerHTML = html; + return result; }; +(function() { +'use strict'; -// Set all loop helper values back -vjs.TextTrack.prototype.reset = function(){ - this.nextChange = 0; - this.prevChange = this.player_.duration(); - this.firstActiveIndex = 0; - this.lastActiveIndex = 0; -}; +/* Text Track Display +============================================================================= */ +// Global container for both subtitle and captions text. Simple div container. -// Create specific track types /** - * The track component for managing the hiding and showing of captions + * The component for displaying text track cues * * @constructor */ -vjs.CaptionsTrack = vjs.TextTrack.extend(); -vjs.CaptionsTrack.prototype.kind_ = 'captions'; -// Exporting here because Track creation requires the track kind -// to be available on global object. e.g. new window['videojs'][Kind + 'Track'] +vjs.TextTrackDisplay = vjs.Component.extend({ + /** @constructor */ + init: function(player, options, ready){ + vjs.Component.call(this, player, options, ready); -/** - * The track component for managing the hiding and showing of subtitles - * - * @constructor - */ -vjs.SubtitlesTrack = vjs.TextTrack.extend(); -vjs.SubtitlesTrack.prototype.kind_ = 'subtitles'; - -/** - * The track component for managing the hiding and showing of chapters - * - * @constructor - */ -vjs.ChaptersTrack = vjs.TextTrack.extend(); -vjs.ChaptersTrack.prototype.kind_ = 'chapters'; - - -/* Text Track Display -============================================================================= */ -// Global container for both subtitle and captions text. Simple div container. - -/** - * The component for displaying text track cues - * - * @constructor - */ -vjs.TextTrackDisplay = vjs.Component.extend({ - /** @constructor */ - init: function(player, options, ready){ - vjs.Component.call(this, player, options, ready); + player.on('loadstart', vjs.bind(this, this.toggleDisplay)); // This used to be called during player init, but was causing an error // if a track should show by default and the display hadn't loaded yet. // Should probably be moved to an external track loader when we support // tracks that don't need a display. - if (player.options_['tracks'] && player.options_['tracks'].length > 0) { - this.player_.addTextTracks(player.options_['tracks']); - } + player.ready(vjs.bind(this, function() { + if (player.tech && player.tech['featuresNativeTextTracks']) { + this.hide(); + return; + } + + var i, tracks, track; + + player.on('fullscreenchange', vjs.bind(this, this.updateDisplay)); + + tracks = player.options_['tracks'] || []; + for (i = 0; i < tracks.length; i++) { + track = tracks[i]; + this.player_.addRemoteTextTrack(track); + } + })); } }); +vjs.TextTrackDisplay.prototype.toggleDisplay = function() { + if (this.player_.tech && this.player_.tech['featuresNativeTextTracks']) { + this.hide(); + } else { + this.show(); + } +}; + vjs.TextTrackDisplay.prototype.createEl = function(){ return vjs.Component.prototype.createEl.call(this, 'div', { className: 'vjs-text-track-display' }); }; +vjs.TextTrackDisplay.prototype.clearDisplay = function() { + if (typeof window['WebVTT'] === 'function') { + window['WebVTT']['processCues'](window, [], this.el_); + } +}; + +// Add cue HTML to display +var constructColor = function(color, opacity) { + return 'rgba(' + + // color looks like "#f0e" + parseInt(color[1] + color[1], 16) + ',' + + parseInt(color[2] + color[2], 16) + ',' + + parseInt(color[3] + color[3], 16) + ',' + + opacity + ')'; +}; +var darkGray = '#222'; +var lightGray = '#ccc'; +var fontMap = { + monospace: 'monospace', + sansSerif: 'sans-serif', + serif: 'serif', + monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', + monospaceSerif: '"Courier New", monospace', + proportionalSansSerif: 'sans-serif', + proportionalSerif: 'serif', + casual: '"Comic Sans MS", Impact, fantasy', + script: '"Monotype Corsiva", cursive', + smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' +}; +var tryUpdateStyle = function(el, style, rule) { + // some style changes will throw an error, particularly in IE8. Those should be noops. + try { + el.style[style] = rule; + } catch (e) {} +}; + +vjs.TextTrackDisplay.prototype.updateDisplay = function() { + var tracks = this.player_.textTracks(), + i = 0, + track; + + this.clearDisplay(); + + if (!tracks) { + return; + } + + for (; i < tracks.length; i++) { + track = tracks[i]; + if (track['mode'] === 'showing') { + this.updateForTrack(track); + } + } +}; + +vjs.TextTrackDisplay.prototype.updateForTrack = function(track) { + if (typeof window['WebVTT'] !== 'function' || !track['activeCues']) { + return; + } + + var i = 0, + property, + cueDiv, + overrides = this.player_['textTrackSettings'].getValues(), + fontSize, + cues = []; + + for (; i < track['activeCues'].length; i++) { + cues.push(track['activeCues'][i]); + } + + window['WebVTT']['processCues'](window, track['activeCues'], this.el_); + + i = cues.length; + while (i--) { + cueDiv = cues[i].displayState; + if (overrides.color) { + cueDiv.firstChild.style.color = overrides.color; + } + if (overrides.textOpacity) { + tryUpdateStyle(cueDiv.firstChild, + 'color', + constructColor(overrides.color || '#fff', + overrides.textOpacity)); + } + if (overrides.backgroundColor) { + cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; + } + if (overrides.backgroundOpacity) { + tryUpdateStyle(cueDiv.firstChild, + 'backgroundColor', + constructColor(overrides.backgroundColor || '#000', + overrides.backgroundOpacity)); + } + if (overrides.windowColor) { + if (overrides.windowOpacity) { + tryUpdateStyle(cueDiv, + 'backgroundColor', + constructColor(overrides.windowColor, overrides.windowOpacity)); + } else { + cueDiv.style.backgroundColor = overrides.windowColor; + } + } + if (overrides.edgeStyle) { + if (overrides.edgeStyle === 'dropshadow') { + cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray; + } else if (overrides.edgeStyle === 'raised') { + cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray; + } else if (overrides.edgeStyle === 'depressed') { + cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray; + } else if (overrides.edgeStyle === 'uniform') { + cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray; + } + } + if (overrides.fontPercent && overrides.fontPercent !== 1) { + fontSize = window.parseFloat(cueDiv.style.fontSize); + cueDiv.style.fontSize = (fontSize * overrides.fontPercent) + 'px'; + cueDiv.style.height = 'auto'; + cueDiv.style.top = 'auto'; + cueDiv.style.bottom = '2px'; + } + if (overrides.fontFamily && overrides.fontFamily !== 'default') { + if (overrides.fontFamily === 'small-caps') { + cueDiv.firstChild.style.fontVariant = 'small-caps'; + } else { + cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; + } + } + } +}; + /** * The specific menu item type for selecting a language within a text track kind @@ -6954,24 +9023,98 @@ vjs.TextTrackDisplay.prototype.createEl = function(){ vjs.TextTrackMenuItem = vjs.MenuItem.extend({ /** @constructor */ init: function(player, options){ - var track = this.track = options['track']; + var track = this.track = options['track'], + tracks = player.textTracks(), + changeHandler, + event; + + if (tracks) { + changeHandler = vjs.bind(this, function() { + var selected = this.track['mode'] === 'showing', + track, + i, + l; + + if (this instanceof vjs.OffTextTrackMenuItem) { + selected = true; + + i = 0, + l = tracks.length; + + for (; i < l; i++) { + track = tracks[i]; + if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') { + selected = false; + break; + } + } + } + + this.selected(selected); + }); + tracks.addEventListener('change', changeHandler); + player.on('dispose', function() { + tracks.removeEventListener('change', changeHandler); + }); + } // Modify options for parent MenuItem class's init. - options['label'] = track.label(); - options['selected'] = track.dflt(); + options['label'] = track['label'] || track['language'] || 'Unknown'; + options['selected'] = track['default'] || track['mode'] === 'showing'; vjs.MenuItem.call(this, player, options); - this.player_.on(track.kind() + 'trackchange', vjs.bind(this, this.update)); + // iOS7 doesn't dispatch change events to TextTrackLists when an + // associated track's mode changes. Without something like + // Object.observe() (also not present on iOS7), it's not + // possible to detect changes to the mode attribute and polyfill + // the change event. As a poor substitute, we manually dispatch + // change events whenever the controls modify the mode. + if (tracks && tracks.onchange === undefined) { + this.on(['tap', 'click'], function() { + if (typeof window.Event !== 'object') { + // Android 2.3 throws an Illegal Constructor error for window.Event + try { + event = new window.Event('change'); + } catch(err){} + } + + if (!event) { + event = document.createEvent('Event'); + event.initEvent('change', true, true); + } + + tracks.dispatchEvent(event); + }); + } } }); vjs.TextTrackMenuItem.prototype.onClick = function(){ + var kind = this.track['kind'], + tracks = this.player_.textTracks(), + mode, + track, + i = 0; + vjs.MenuItem.prototype.onClick.call(this); - this.player_.showTextTrack(this.track.id_, this.track.kind()); -}; -vjs.TextTrackMenuItem.prototype.update = function(){ - this.selected(this.track.mode() == 2); + if (!tracks) { + return; + } + + for (; i < tracks.length; i++) { + track = tracks[i]; + + if (track['kind'] !== kind) { + continue; + } + + if (track === this.track) { + track['mode'] = 'showing'; + } else { + track['mode'] = 'disabled'; + } + } }; /** @@ -6985,35 +9128,34 @@ vjs.OffTextTrackMenuItem = vjs.TextTrackMenuItem.extend({ // Create pseudo track info // Requires options['kind'] options['track'] = { - kind: function() { return options['kind']; }, - player: player, - label: function(){ return options['kind'] + ' off'; }, - dflt: function(){ return false; }, - mode: function(){ return false; } + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' off', + 'default': false, + 'mode': 'disabled' }; vjs.TextTrackMenuItem.call(this, player, options); this.selected(true); } }); -vjs.OffTextTrackMenuItem.prototype.onClick = function(){ - vjs.TextTrackMenuItem.prototype.onClick.call(this); - this.player_.showTextTrack(this.track.id_, this.track.kind()); -}; - -vjs.OffTextTrackMenuItem.prototype.update = function(){ - var tracks = this.player_.textTracks(), - i=0, j=tracks.length, track, - off = true; +vjs.CaptionSettingsMenuItem = vjs.TextTrackMenuItem.extend({ + init: function(player, options) { + options['track'] = { + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' settings', + 'default': false, + mode: 'disabled' + }; - for (;i threshold) { + this.show(); + } else { + this.hide(); + } +}; + /** * The button component for toggling and selecting subtitles * @@ -7128,11 +9290,17 @@ vjs.ChaptersButton.prototype.className = 'vjs-chapters-button'; // Create a menu item for each text track vjs.ChaptersButton.prototype.createItems = function(){ - var items = [], track; + var items = [], track, tracks; + + tracks = this.player_.textTracks(); - for (var i = 0; i < this.player_.textTracks().length; i++) { - track = this.player_.textTracks()[i]; - if (track.kind() === this.kind_) { + if (!tracks) { + return items; + } + + for (var i = 0; i < tracks.length; i++) { + track = tracks[i]; + if (track['kind'] === this.kind_) { items.push(new vjs.TextTrackMenuItem(this.player_, { 'track': track })); @@ -7143,19 +9311,23 @@ vjs.ChaptersButton.prototype.createItems = function(){ }; vjs.ChaptersButton.prototype.createMenu = function(){ - var tracks = this.player_.textTracks(), + var tracks = this.player_.textTracks() || [], i = 0, - j = tracks.length, + l = tracks.length, track, chaptersTrack, items = this.items = []; - for (;i 0) { @@ -7210,10 +9385,10 @@ vjs.ChaptersTrackMenuItem = vjs.MenuItem.extend({ // Modify options for parent MenuItem class's init. options['label'] = cue.text; - options['selected'] = (cue.startTime <= currentTime && currentTime < cue.endTime); + options['selected'] = (cue['startTime'] <= currentTime && currentTime < cue['endTime']); vjs.MenuItem.call(this, player, options); - track.on('cuechange', vjs.bind(this, this.update)); + track.addEventListener('cuechange', vjs.bind(this, this.update)); } }); @@ -7228,22 +9403,293 @@ vjs.ChaptersTrackMenuItem.prototype.update = function(){ currentTime = this.player_.currentTime(); // vjs.log(currentTime, cue.startTime); - this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); + this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']); }; +})(); +(function() { + 'use strict'; -// Add Buttons to controlBar -vjs.obj.merge(vjs.ControlBar.prototype.options_['children'], { - 'subtitlesButton': {}, - 'captionsButton': {}, - 'chaptersButton': {} -}); + vjs.TextTrackSettings = vjs.Component.extend({ + init: function(player, options) { + vjs.Component.call(this, player, options); + this.hide(); + + vjs.on(this.el().querySelector('.vjs-done-button'), 'click', vjs.bind(this, function() { + this.saveSettings(); + this.hide(); + })); + + vjs.on(this.el().querySelector('.vjs-default-button'), 'click', vjs.bind(this, function() { + this.el().querySelector('.vjs-fg-color > select').selectedIndex = 0; + this.el().querySelector('.vjs-bg-color > select').selectedIndex = 0; + this.el().querySelector('.window-color > select').selectedIndex = 0; + this.el().querySelector('.vjs-text-opacity > select').selectedIndex = 0; + this.el().querySelector('.vjs-bg-opacity > select').selectedIndex = 0; + this.el().querySelector('.vjs-window-opacity > select').selectedIndex = 0; + this.el().querySelector('.vjs-edge-style select').selectedIndex = 0; + this.el().querySelector('.vjs-font-family select').selectedIndex = 0; + this.el().querySelector('.vjs-font-percent select').selectedIndex = 2; + this.updateDisplay(); + })); + + vjs.on(this.el().querySelector('.vjs-fg-color > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-bg-color > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.window-color > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-text-opacity > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-bg-opacity > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-window-opacity > select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-font-percent select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-edge-style select'), 'change', vjs.bind(this, this.updateDisplay)); + vjs.on(this.el().querySelector('.vjs-font-family select'), 'change', vjs.bind(this, this.updateDisplay)); + + if (player.options()['persistTextTrackSettings']) { + this.restoreSettings(); + } + } + }); + + vjs.TextTrackSettings.prototype.createEl = function() { + return vjs.Component.prototype.createEl.call(this, 'div', { + className: 'vjs-caption-settings vjs-modal-overlay', + innerHTML: captionOptionsMenuTemplate() + }); + }; + + vjs.TextTrackSettings.prototype.getValues = function() { + var el, bgOpacity, textOpacity, windowOpacity, textEdge, fontFamily, fgColor, bgColor, windowColor, result, name, fontPercent; + + el = this.el(); + + textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select')); + fontFamily = getSelectedOptionValue(el.querySelector('.vjs-font-family select')); + fgColor = getSelectedOptionValue(el.querySelector('.vjs-fg-color > select')); + textOpacity = getSelectedOptionValue(el.querySelector('.vjs-text-opacity > select')); + bgColor = getSelectedOptionValue(el.querySelector('.vjs-bg-color > select')); + bgOpacity = getSelectedOptionValue(el.querySelector('.vjs-bg-opacity > select')); + windowColor = getSelectedOptionValue(el.querySelector('.window-color > select')); + windowOpacity = getSelectedOptionValue(el.querySelector('.vjs-window-opacity > select')); + fontPercent = window['parseFloat'](getSelectedOptionValue(el.querySelector('.vjs-font-percent > select'))); + + result = { + 'backgroundOpacity': bgOpacity, + 'textOpacity': textOpacity, + 'windowOpacity': windowOpacity, + 'edgeStyle': textEdge, + 'fontFamily': fontFamily, + 'color': fgColor, + 'backgroundColor': bgColor, + 'windowColor': windowColor, + 'fontPercent': fontPercent + }; + for (name in result) { + if (result[name] === '' || result[name] === 'none' || (name === 'fontPercent' && result[name] === 1.00)) { + delete result[name]; + } + } + return result; + }; + + vjs.TextTrackSettings.prototype.setValues = function(values) { + var el = this.el(), fontPercent; + + setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle); + setSelectedOption(el.querySelector('.vjs-font-family select'), values.fontFamily); + setSelectedOption(el.querySelector('.vjs-fg-color > select'), values.color); + setSelectedOption(el.querySelector('.vjs-text-opacity > select'), values.textOpacity); + setSelectedOption(el.querySelector('.vjs-bg-color > select'), values.backgroundColor); + setSelectedOption(el.querySelector('.vjs-bg-opacity > select'), values.backgroundOpacity); + setSelectedOption(el.querySelector('.window-color > select'), values.windowColor); + setSelectedOption(el.querySelector('.vjs-window-opacity > select'), values.windowOpacity); + + fontPercent = values.fontPercent; + + if (fontPercent) { + fontPercent = fontPercent.toFixed(2); + } + + setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent); + }; + + vjs.TextTrackSettings.prototype.restoreSettings = function() { + var values; + try { + values = JSON.parse(window.localStorage.getItem('vjs-text-track-settings')); + } catch (e) {} + + if (values) { + this.setValues(values); + } + }; + + vjs.TextTrackSettings.prototype.saveSettings = function() { + var values; + + if (!this.player_.options()['persistTextTrackSettings']) { + return; + } + + values = this.getValues(); + try { + if (!vjs.isEmpty(values)) { + window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(values)); + } else { + window.localStorage.removeItem('vjs-text-track-settings'); + } + } catch (e) {} + }; + + vjs.TextTrackSettings.prototype.updateDisplay = function() { + var ttDisplay = this.player_.getChild('textTrackDisplay'); + if (ttDisplay) { + ttDisplay.updateDisplay(); + } + }; + + function getSelectedOptionValue(target) { + var selectedOption; + // not all browsers support selectedOptions, so, fallback to options + if (target.selectedOptions) { + selectedOption = target.selectedOptions[0]; + } else if (target.options) { + selectedOption = target.options[target.options.selectedIndex]; + } + + return selectedOption.value; + } + + function setSelectedOption(target, value) { + var i, option; + + if (!value) { + return; + } + + for (i = 0; i < target.options.length; i++) { + option = target.options[i]; + if (option.value === value) { + break; + } + } + + target.selectedIndex = i; + } + + function captionOptionsMenuTemplate() { + return '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + // vjs-fg-color + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + // vjs-bg-color + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + // vjs-window-color + '
' + // vjs-tracksettings + '
' + + '
' + + '' + + '' + + '
' + // vjs-font-percent + '
' + + '' + + '' + + '
' + // vjs-edge-style + '
' + + '' + + '' + + '
' + // vjs-font-family + '
' + + '
' + + '
' + + '' + + '' + + '
'; + } -// vjs.Cue = vjs.Component.extend({ -// /** @constructor */ -// init: function(player, options){ -// vjs.Component.call(this, player, options); -// } -// }); +})(); /** * @fileoverview Add JSON support * @suppress {undefinedVars} @@ -7261,7 +9707,7 @@ vjs.obj.merge(vjs.ControlBar.prototype.options_['children'], { */ vjs.JSON; -if (typeof window.JSON !== 'undefined' && window.JSON.parse === 'function') { +if (typeof window.JSON !== 'undefined' && typeof window.JSON.parse === 'function') { vjs.JSON = window.JSON; } else { @@ -7326,33 +9772,48 @@ if (typeof window.JSON !== 'undefined' && window.JSON.parse === 'function') { // Automatically set up any tags that have a data-setup attribute vjs.autoSetup = function(){ - var options, vid, player, - vids = document.getElementsByTagName('video'); + var options, mediaEl, player, i, e; + + // One day, when we stop supporting IE8, go back to this, but in the meantime...*hack hack hack* + // var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); + // var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); + // var mediaEls = vids.concat(audios); + + // Because IE8 doesn't support calling slice on a node list, we need to loop through each list of elements + // to build up a new, combined list of elements. + var vids = document.getElementsByTagName('video'); + var audios = document.getElementsByTagName('audio'); + var mediaEls = []; + if (vids && vids.length > 0) { + for(i=0, e=vids.length; i 0) { + for(i=0, e=audios.length; i 0) { + if (mediaEls && mediaEls.length > 0) { - for (var i=0,j=vids.length; i 100) { + throw new Error("Position must be between 0 and 100."); + } + _position = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "positionAlign", extend({}, baseObj, { + get: function() { + return _positionAlign; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _positionAlign = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "size", extend({}, baseObj, { + get: function() { + return _size; + }, + set: function(value) { + if (value < 0 || value > 100) { + throw new Error("Size must be between 0 and 100."); + } + _size = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "align", extend({}, baseObj, { + get: function() { + return _align; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _align = setting; + this.hasBeenReset = true; + } + })); + + /** + * Other spec defined properties + */ + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state + cue.displayState = undefined; + + if (isIE8) { + return cue; + } + } + + /** + * VTTCue methods + */ + + VTTCue.prototype.getCueAsHTML = function() { + // Assume WebVTT.convertCueToDOMTree is on the global. + return WebVTT.convertCueToDOMTree(window, this.text); + }; + + root.VTTCue = root.VTTCue || VTTCue; + vttjs.VTTCue = VTTCue; +}(this, (this.vttjs || {}))); + +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function(root, vttjs) { + + var scrollSetting = { + "": true, + "up": true, + }; + + function findScrollSetting(value) { + if (typeof value !== "string") { + return false; + } + var scroll = scrollSetting[value.toLowerCase()]; + return scroll ? value.toLowerCase() : false; + } + + function isValidPercentValue(value) { + return typeof value === "number" && (value >= 0 && value <= 100); + } + + // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface + function VTTRegion() { + var _width = 100; + var _lines = 3; + var _regionAnchorX = 0; + var _regionAnchorY = 100; + var _viewportAnchorX = 0; + var _viewportAnchorY = 100; + var _scroll = ""; + + Object.defineProperties(this, { + "width": { + enumerable: true, + get: function() { + return _width; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("Width must be between 0 and 100."); + } + _width = value; + } + }, + "lines": { + enumerable: true, + get: function() { + return _lines; + }, + set: function(value) { + if (typeof value !== "number") { + throw new TypeError("Lines must be set to a number."); + } + _lines = value; + } + }, + "regionAnchorY": { + enumerable: true, + get: function() { + return _regionAnchorY; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("RegionAnchorX must be between 0 and 100."); + } + _regionAnchorY = value; + } + }, + "regionAnchorX": { + enumerable: true, + get: function() { + return _regionAnchorX; + }, + set: function(value) { + if(!isValidPercentValue(value)) { + throw new Error("RegionAnchorY must be between 0 and 100."); + } + _regionAnchorX = value; + } + }, + "viewportAnchorY": { + enumerable: true, + get: function() { + return _viewportAnchorY; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("ViewportAnchorY must be between 0 and 100."); + } + _viewportAnchorY = value; + } + }, + "viewportAnchorX": { + enumerable: true, + get: function() { + return _viewportAnchorX; + }, + set: function(value) { + if (!isValidPercentValue(value)) { + throw new Error("ViewportAnchorX must be between 0 and 100."); + } + _viewportAnchorX = value; + } + }, + "scroll": { + enumerable: true, + get: function() { + return _scroll; + }, + set: function(value) { + var setting = findScrollSetting(value); + // Have to check for false as an empty string is a legal value. + if (setting === false) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _scroll = setting; + } + } + }); + } + + root.VTTRegion = root.VTTRegion || VTTRegion; + vttjs.VTTRegion = VTTRegion; +}(this, (this.vttjs || {}))); + +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +(function(global) { + + var _objCreate = Object.create || (function() { + function F() {} + return function(o) { + if (arguments.length !== 1) { + throw new Error('Object.create shim only accepts one parameter.'); + } + F.prototype = o; + return new F(); + }; + })(); + + // Creates a new ParserError object from an errorData object. The errorData + // object should have default code and message properties. The default message + // property can be overriden by passing in a message parameter. + // See ParsingError.Errors below for acceptable errors. + function ParsingError(errorData, message) { + this.name = "ParsingError"; + this.code = errorData.code; + this.message = message || errorData.message; + } + ParsingError.prototype = _objCreate(Error.prototype); + ParsingError.prototype.constructor = ParsingError; + + // ParsingError metadata for acceptable ParsingErrors. + ParsingError.Errors = { + BadSignature: { + code: 0, + message: "Malformed WebVTT signature." + }, + BadTimeStamp: { + code: 1, + message: "Malformed time stamp." + } + }; + + // Try to parse input as a time stamp. + function parseTimeStamp(input) { + + function computeSeconds(h, m, s, f) { + return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; + } + + var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/); + if (!m) { + return null; + } + + if (m[3]) { + // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] + return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); + } else if (m[1] > 59) { + // Timestamp takes the form of [hours]:[minutes].[milliseconds] + // First position is hours as it's over 59. + return computeSeconds(m[1], m[2], 0, m[4]); + } else { + // Timestamp takes the form of [minutes]:[seconds].[milliseconds] + return computeSeconds(0, m[1], m[2], m[4]); + } + } + + // A settings object holds key/value pairs and will ignore anything but the first + // assignment to a specific key. + function Settings() { + this.values = _objCreate(null); + } + + Settings.prototype = { + // Only accept the first assignment to any key. + set: function(k, v) { + if (!this.get(k) && v !== "") { + this.values[k] = v; + } + }, + // Return the value for a key, or a default value. + // If 'defaultKey' is passed then 'dflt' is assumed to be an object with + // a number of possible default values as properties where 'defaultKey' is + // the key of the property that will be chosen; otherwise it's assumed to be + // a single value. + get: function(k, dflt, defaultKey) { + if (defaultKey) { + return this.has(k) ? this.values[k] : dflt[defaultKey]; + } + return this.has(k) ? this.values[k] : dflt; + }, + // Check whether we have a value for a key. + has: function(k) { + return k in this.values; + }, + // Accept a setting if its one of the given alternatives. + alt: function(k, v, a) { + for (var n = 0; n < a.length; ++n) { + if (v === a[n]) { + this.set(k, v); + break; + } + } + }, + // Accept a setting if its a valid (signed) integer. + integer: function(k, v) { + if (/^-?\d+$/.test(v)) { // integer + this.set(k, parseInt(v, 10)); + } + }, + // Accept a setting if its a valid percentage. + percent: function(k, v) { + var m; + if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) { + v = parseFloat(v); + if (v >= 0 && v <= 100) { + this.set(k, v); + return true; + } + } + return false; + } + }; + + // Helper function to parse input into groups separated by 'groupDelim', and + // interprete each group as a key/value pair separated by 'keyValueDelim'. + function parseOptions(input, callback, keyValueDelim, groupDelim) { + var groups = groupDelim ? input.split(groupDelim) : [input]; + for (var i in groups) { + if (typeof groups[i] !== "string") { + continue; + } + var kv = groups[i].split(keyValueDelim); + if (kv.length !== 2) { + continue; + } + var k = kv[0]; + var v = kv[1]; + callback(k, v); + } + } + + function parseCue(input, cue, regionList) { + // Remember the original input if we need to throw an error. + var oInput = input; + // 4.1 WebVTT timestamp + function consumeTimeStamp() { + var ts = parseTimeStamp(input); + if (ts === null) { + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput); + } + // Remove time stamp from input. + input = input.replace(/^[^\sa-zA-Z-]+/, ""); + return ts; + } + + // 4.4.2 WebVTT cue settings + function consumeCueSettings(input, cue) { + var settings = new Settings(); + + parseOptions(input, function (k, v) { + switch (k) { + case "region": + // Find the last region we parsed with the same region id. + for (var i = regionList.length - 1; i >= 0; i--) { + if (regionList[i].id === v) { + settings.set(k, regionList[i].region); + break; + } + } + break; + case "vertical": + settings.alt(k, v, ["rl", "lr"]); + break; + case "line": + var vals = v.split(","), + vals0 = vals[0]; + settings.integer(k, vals0); + settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; + settings.alt(k, vals0, ["auto"]); + if (vals.length === 2) { + settings.alt("lineAlign", vals[1], ["start", "middle", "end"]); + } + break; + case "position": + vals = v.split(","); + settings.percent(k, vals[0]); + if (vals.length === 2) { + settings.alt("positionAlign", vals[1], ["start", "middle", "end"]); + } + break; + case "size": + settings.percent(k, v); + break; + case "align": + settings.alt(k, v, ["start", "middle", "end", "left", "right"]); + break; + } + }, /:/, /\s/); + + // Apply default values for any missing fields. + cue.region = settings.get("region", null); + cue.vertical = settings.get("vertical", ""); + cue.line = settings.get("line", "auto"); + cue.lineAlign = settings.get("lineAlign", "start"); + cue.snapToLines = settings.get("snapToLines", true); + cue.size = settings.get("size", 100); + cue.align = settings.get("align", "middle"); + cue.position = settings.get("position", { + start: 0, + left: 0, + middle: 50, + end: 100, + right: 100 + }, cue.align); + cue.positionAlign = settings.get("positionAlign", { + start: "start", + left: "start", + middle: "middle", + end: "end", + right: "end" + }, cue.align); + } + + function skipWhitespace() { + input = input.replace(/^\s+/, ""); + } + + // 4.1 WebVTT cue timings. + skipWhitespace(); + cue.startTime = consumeTimeStamp(); // (1) collect cue start time + skipWhitespace(); + if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed time stamp (time stamps must be separated by '-->'): " + + oInput); + } + input = input.substr(3); + skipWhitespace(); + cue.endTime = consumeTimeStamp(); // (5) collect cue end time + + // 4.1 WebVTT cue settings list. + skipWhitespace(); + consumeCueSettings(input, cue); + } + + var ESCAPE = { + "&": "&", + "<": "<", + ">": ">", + "‎": "\u200e", + "‏": "\u200f", + " ": "\u00a0" + }; + + var TAG_NAME = { + c: "span", + i: "i", + b: "b", + u: "u", + ruby: "ruby", + rt: "rt", + v: "span", + lang: "span" + }; + + var TAG_ANNOTATION = { + v: "title", + lang: "lang" + }; + + var NEEDS_PARENT = { + rt: "ruby" + }; + + // Parse content into a document fragment. + function parseContent(window, input) { + function nextToken() { + // Check for end-of-string. + if (!input) { + return null; + } + + // Consume 'n' characters from the input. + function consume(result) { + input = input.substr(result.length); + return result; + } + + var m = input.match(/^([^<]*)(<[^>]+>?)?/); + // If there is some text before the next tag, return it, otherwise return + // the tag. + return consume(m[1] ? m[1] : m[2]); + } + + // Unescape a string 's'. + function unescape1(e) { + return ESCAPE[e]; + } + function unescape(s) { + while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) { + s = s.replace(m[0], unescape1); + } + return s; + } + + function shouldAdd(current, element) { + return !NEEDS_PARENT[element.localName] || + NEEDS_PARENT[element.localName] === current.localName; + } + + // Create an element for this tag. + function createElement(type, annotation) { + var tagName = TAG_NAME[type]; + if (!tagName) { + return null; + } + var element = window.document.createElement(tagName); + element.localName = tagName; + var name = TAG_ANNOTATION[type]; + if (name && annotation) { + element[name] = annotation.trim(); + } + return element; + } + + var rootDiv = window.document.createElement("div"), + current = rootDiv, + t, + tagStack = []; + + while ((t = nextToken()) !== null) { + if (t[0] === '<') { + if (t[1] === "/") { + // If the closing tag matches, move back up to the parent node. + if (tagStack.length && + tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { + tagStack.pop(); + current = current.parentNode; + } + // Otherwise just ignore the end tag. + continue; + } + var ts = parseTimeStamp(t.substr(1, t.length - 2)); + var node; + if (ts) { + // Timestamps are lead nodes as well. + node = window.document.createProcessingInstruction("timestamp", ts); + current.appendChild(node); + continue; + } + var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); + // If we can't parse the tag, skip to the next tag. + if (!m) { + continue; + } + // Try to construct an element, and ignore the tag if we couldn't. + node = createElement(m[1], m[3]); + if (!node) { + continue; + } + // Determine if the tag should be added based on the context of where it + // is placed in the cuetext. + if (!shouldAdd(current, node)) { + continue; + } + // Set the class list (as a list of classes, separated by space). + if (m[2]) { + node.className = m[2].substr(1).replace('.', ' '); + } + // Append the node to the current node, and enter the scope of the new + // node. + tagStack.push(m[1]); + current.appendChild(node); + current = node; + continue; + } + + // Text nodes are leaf nodes. + current.appendChild(window.document.createTextNode(unescape(t))); + } + + return rootDiv; + } + + // This is a list of all the Unicode characters that have a strong + // right-to-left category. What this means is that these characters are + // written right-to-left for sure. It was generated by pulling all the strong + // right-to-left characters out of the Unicode data table. That table can + // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt + var strongRTLChars = [0x05BE, 0x05C0, 0x05C3, 0x05C6, 0x05D0, 0x05D1, + 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, + 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, 0x05E0, 0x05E1, 0x05E2, 0x05E3, + 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x05F0, 0x05F1, + 0x05F2, 0x05F3, 0x05F4, 0x0608, 0x060B, 0x060D, 0x061B, 0x061E, 0x061F, + 0x0620, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627, 0x0628, + 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631, + 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638, 0x0639, 0x063A, + 0x063B, 0x063C, 0x063D, 0x063E, 0x063F, 0x0640, 0x0641, 0x0642, 0x0643, + 0x0644, 0x0645, 0x0646, 0x0647, 0x0648, 0x0649, 0x064A, 0x066D, 0x066E, + 0x066F, 0x0671, 0x0672, 0x0673, 0x0674, 0x0675, 0x0676, 0x0677, 0x0678, + 0x0679, 0x067A, 0x067B, 0x067C, 0x067D, 0x067E, 0x067F, 0x0680, 0x0681, + 0x0682, 0x0683, 0x0684, 0x0685, 0x0686, 0x0687, 0x0688, 0x0689, 0x068A, + 0x068B, 0x068C, 0x068D, 0x068E, 0x068F, 0x0690, 0x0691, 0x0692, 0x0693, + 0x0694, 0x0695, 0x0696, 0x0697, 0x0698, 0x0699, 0x069A, 0x069B, 0x069C, + 0x069D, 0x069E, 0x069F, 0x06A0, 0x06A1, 0x06A2, 0x06A3, 0x06A4, 0x06A5, + 0x06A6, 0x06A7, 0x06A8, 0x06A9, 0x06AA, 0x06AB, 0x06AC, 0x06AD, 0x06AE, + 0x06AF, 0x06B0, 0x06B1, 0x06B2, 0x06B3, 0x06B4, 0x06B5, 0x06B6, 0x06B7, + 0x06B8, 0x06B9, 0x06BA, 0x06BB, 0x06BC, 0x06BD, 0x06BE, 0x06BF, 0x06C0, + 0x06C1, 0x06C2, 0x06C3, 0x06C4, 0x06C5, 0x06C6, 0x06C7, 0x06C8, 0x06C9, + 0x06CA, 0x06CB, 0x06CC, 0x06CD, 0x06CE, 0x06CF, 0x06D0, 0x06D1, 0x06D2, + 0x06D3, 0x06D4, 0x06D5, 0x06E5, 0x06E6, 0x06EE, 0x06EF, 0x06FA, 0x06FB, + 0x06FC, 0x06FD, 0x06FE, 0x06FF, 0x0700, 0x0701, 0x0702, 0x0703, 0x0704, + 0x0705, 0x0706, 0x0707, 0x0708, 0x0709, 0x070A, 0x070B, 0x070C, 0x070D, + 0x070F, 0x0710, 0x0712, 0x0713, 0x0714, 0x0715, 0x0716, 0x0717, 0x0718, + 0x0719, 0x071A, 0x071B, 0x071C, 0x071D, 0x071E, 0x071F, 0x0720, 0x0721, + 0x0722, 0x0723, 0x0724, 0x0725, 0x0726, 0x0727, 0x0728, 0x0729, 0x072A, + 0x072B, 0x072C, 0x072D, 0x072E, 0x072F, 0x074D, 0x074E, 0x074F, 0x0750, + 0x0751, 0x0752, 0x0753, 0x0754, 0x0755, 0x0756, 0x0757, 0x0758, 0x0759, + 0x075A, 0x075B, 0x075C, 0x075D, 0x075E, 0x075F, 0x0760, 0x0761, 0x0762, + 0x0763, 0x0764, 0x0765, 0x0766, 0x0767, 0x0768, 0x0769, 0x076A, 0x076B, + 0x076C, 0x076D, 0x076E, 0x076F, 0x0770, 0x0771, 0x0772, 0x0773, 0x0774, + 0x0775, 0x0776, 0x0777, 0x0778, 0x0779, 0x077A, 0x077B, 0x077C, 0x077D, + 0x077E, 0x077F, 0x0780, 0x0781, 0x0782, 0x0783, 0x0784, 0x0785, 0x0786, + 0x0787, 0x0788, 0x0789, 0x078A, 0x078B, 0x078C, 0x078D, 0x078E, 0x078F, + 0x0790, 0x0791, 0x0792, 0x0793, 0x0794, 0x0795, 0x0796, 0x0797, 0x0798, + 0x0799, 0x079A, 0x079B, 0x079C, 0x079D, 0x079E, 0x079F, 0x07A0, 0x07A1, + 0x07A2, 0x07A3, 0x07A4, 0x07A5, 0x07B1, 0x07C0, 0x07C1, 0x07C2, 0x07C3, + 0x07C4, 0x07C5, 0x07C6, 0x07C7, 0x07C8, 0x07C9, 0x07CA, 0x07CB, 0x07CC, + 0x07CD, 0x07CE, 0x07CF, 0x07D0, 0x07D1, 0x07D2, 0x07D3, 0x07D4, 0x07D5, + 0x07D6, 0x07D7, 0x07D8, 0x07D9, 0x07DA, 0x07DB, 0x07DC, 0x07DD, 0x07DE, + 0x07DF, 0x07E0, 0x07E1, 0x07E2, 0x07E3, 0x07E4, 0x07E5, 0x07E6, 0x07E7, + 0x07E8, 0x07E9, 0x07EA, 0x07F4, 0x07F5, 0x07FA, 0x0800, 0x0801, 0x0802, + 0x0803, 0x0804, 0x0805, 0x0806, 0x0807, 0x0808, 0x0809, 0x080A, 0x080B, + 0x080C, 0x080D, 0x080E, 0x080F, 0x0810, 0x0811, 0x0812, 0x0813, 0x0814, + 0x0815, 0x081A, 0x0824, 0x0828, 0x0830, 0x0831, 0x0832, 0x0833, 0x0834, + 0x0835, 0x0836, 0x0837, 0x0838, 0x0839, 0x083A, 0x083B, 0x083C, 0x083D, + 0x083E, 0x0840, 0x0841, 0x0842, 0x0843, 0x0844, 0x0845, 0x0846, 0x0847, + 0x0848, 0x0849, 0x084A, 0x084B, 0x084C, 0x084D, 0x084E, 0x084F, 0x0850, + 0x0851, 0x0852, 0x0853, 0x0854, 0x0855, 0x0856, 0x0857, 0x0858, 0x085E, + 0x08A0, 0x08A2, 0x08A3, 0x08A4, 0x08A5, 0x08A6, 0x08A7, 0x08A8, 0x08A9, + 0x08AA, 0x08AB, 0x08AC, 0x200F, 0xFB1D, 0xFB1F, 0xFB20, 0xFB21, 0xFB22, + 0xFB23, 0xFB24, 0xFB25, 0xFB26, 0xFB27, 0xFB28, 0xFB2A, 0xFB2B, 0xFB2C, + 0xFB2D, 0xFB2E, 0xFB2F, 0xFB30, 0xFB31, 0xFB32, 0xFB33, 0xFB34, 0xFB35, + 0xFB36, 0xFB38, 0xFB39, 0xFB3A, 0xFB3B, 0xFB3C, 0xFB3E, 0xFB40, 0xFB41, + 0xFB43, 0xFB44, 0xFB46, 0xFB47, 0xFB48, 0xFB49, 0xFB4A, 0xFB4B, 0xFB4C, + 0xFB4D, 0xFB4E, 0xFB4F, 0xFB50, 0xFB51, 0xFB52, 0xFB53, 0xFB54, 0xFB55, + 0xFB56, 0xFB57, 0xFB58, 0xFB59, 0xFB5A, 0xFB5B, 0xFB5C, 0xFB5D, 0xFB5E, + 0xFB5F, 0xFB60, 0xFB61, 0xFB62, 0xFB63, 0xFB64, 0xFB65, 0xFB66, 0xFB67, + 0xFB68, 0xFB69, 0xFB6A, 0xFB6B, 0xFB6C, 0xFB6D, 0xFB6E, 0xFB6F, 0xFB70, + 0xFB71, 0xFB72, 0xFB73, 0xFB74, 0xFB75, 0xFB76, 0xFB77, 0xFB78, 0xFB79, + 0xFB7A, 0xFB7B, 0xFB7C, 0xFB7D, 0xFB7E, 0xFB7F, 0xFB80, 0xFB81, 0xFB82, + 0xFB83, 0xFB84, 0xFB85, 0xFB86, 0xFB87, 0xFB88, 0xFB89, 0xFB8A, 0xFB8B, + 0xFB8C, 0xFB8D, 0xFB8E, 0xFB8F, 0xFB90, 0xFB91, 0xFB92, 0xFB93, 0xFB94, + 0xFB95, 0xFB96, 0xFB97, 0xFB98, 0xFB99, 0xFB9A, 0xFB9B, 0xFB9C, 0xFB9D, + 0xFB9E, 0xFB9F, 0xFBA0, 0xFBA1, 0xFBA2, 0xFBA3, 0xFBA4, 0xFBA5, 0xFBA6, + 0xFBA7, 0xFBA8, 0xFBA9, 0xFBAA, 0xFBAB, 0xFBAC, 0xFBAD, 0xFBAE, 0xFBAF, + 0xFBB0, 0xFBB1, 0xFBB2, 0xFBB3, 0xFBB4, 0xFBB5, 0xFBB6, 0xFBB7, 0xFBB8, + 0xFBB9, 0xFBBA, 0xFBBB, 0xFBBC, 0xFBBD, 0xFBBE, 0xFBBF, 0xFBC0, 0xFBC1, + 0xFBD3, 0xFBD4, 0xFBD5, 0xFBD6, 0xFBD7, 0xFBD8, 0xFBD9, 0xFBDA, 0xFBDB, + 0xFBDC, 0xFBDD, 0xFBDE, 0xFBDF, 0xFBE0, 0xFBE1, 0xFBE2, 0xFBE3, 0xFBE4, + 0xFBE5, 0xFBE6, 0xFBE7, 0xFBE8, 0xFBE9, 0xFBEA, 0xFBEB, 0xFBEC, 0xFBED, + 0xFBEE, 0xFBEF, 0xFBF0, 0xFBF1, 0xFBF2, 0xFBF3, 0xFBF4, 0xFBF5, 0xFBF6, + 0xFBF7, 0xFBF8, 0xFBF9, 0xFBFA, 0xFBFB, 0xFBFC, 0xFBFD, 0xFBFE, 0xFBFF, + 0xFC00, 0xFC01, 0xFC02, 0xFC03, 0xFC04, 0xFC05, 0xFC06, 0xFC07, 0xFC08, + 0xFC09, 0xFC0A, 0xFC0B, 0xFC0C, 0xFC0D, 0xFC0E, 0xFC0F, 0xFC10, 0xFC11, + 0xFC12, 0xFC13, 0xFC14, 0xFC15, 0xFC16, 0xFC17, 0xFC18, 0xFC19, 0xFC1A, + 0xFC1B, 0xFC1C, 0xFC1D, 0xFC1E, 0xFC1F, 0xFC20, 0xFC21, 0xFC22, 0xFC23, + 0xFC24, 0xFC25, 0xFC26, 0xFC27, 0xFC28, 0xFC29, 0xFC2A, 0xFC2B, 0xFC2C, + 0xFC2D, 0xFC2E, 0xFC2F, 0xFC30, 0xFC31, 0xFC32, 0xFC33, 0xFC34, 0xFC35, + 0xFC36, 0xFC37, 0xFC38, 0xFC39, 0xFC3A, 0xFC3B, 0xFC3C, 0xFC3D, 0xFC3E, + 0xFC3F, 0xFC40, 0xFC41, 0xFC42, 0xFC43, 0xFC44, 0xFC45, 0xFC46, 0xFC47, + 0xFC48, 0xFC49, 0xFC4A, 0xFC4B, 0xFC4C, 0xFC4D, 0xFC4E, 0xFC4F, 0xFC50, + 0xFC51, 0xFC52, 0xFC53, 0xFC54, 0xFC55, 0xFC56, 0xFC57, 0xFC58, 0xFC59, + 0xFC5A, 0xFC5B, 0xFC5C, 0xFC5D, 0xFC5E, 0xFC5F, 0xFC60, 0xFC61, 0xFC62, + 0xFC63, 0xFC64, 0xFC65, 0xFC66, 0xFC67, 0xFC68, 0xFC69, 0xFC6A, 0xFC6B, + 0xFC6C, 0xFC6D, 0xFC6E, 0xFC6F, 0xFC70, 0xFC71, 0xFC72, 0xFC73, 0xFC74, + 0xFC75, 0xFC76, 0xFC77, 0xFC78, 0xFC79, 0xFC7A, 0xFC7B, 0xFC7C, 0xFC7D, + 0xFC7E, 0xFC7F, 0xFC80, 0xFC81, 0xFC82, 0xFC83, 0xFC84, 0xFC85, 0xFC86, + 0xFC87, 0xFC88, 0xFC89, 0xFC8A, 0xFC8B, 0xFC8C, 0xFC8D, 0xFC8E, 0xFC8F, + 0xFC90, 0xFC91, 0xFC92, 0xFC93, 0xFC94, 0xFC95, 0xFC96, 0xFC97, 0xFC98, + 0xFC99, 0xFC9A, 0xFC9B, 0xFC9C, 0xFC9D, 0xFC9E, 0xFC9F, 0xFCA0, 0xFCA1, + 0xFCA2, 0xFCA3, 0xFCA4, 0xFCA5, 0xFCA6, 0xFCA7, 0xFCA8, 0xFCA9, 0xFCAA, + 0xFCAB, 0xFCAC, 0xFCAD, 0xFCAE, 0xFCAF, 0xFCB0, 0xFCB1, 0xFCB2, 0xFCB3, + 0xFCB4, 0xFCB5, 0xFCB6, 0xFCB7, 0xFCB8, 0xFCB9, 0xFCBA, 0xFCBB, 0xFCBC, + 0xFCBD, 0xFCBE, 0xFCBF, 0xFCC0, 0xFCC1, 0xFCC2, 0xFCC3, 0xFCC4, 0xFCC5, + 0xFCC6, 0xFCC7, 0xFCC8, 0xFCC9, 0xFCCA, 0xFCCB, 0xFCCC, 0xFCCD, 0xFCCE, + 0xFCCF, 0xFCD0, 0xFCD1, 0xFCD2, 0xFCD3, 0xFCD4, 0xFCD5, 0xFCD6, 0xFCD7, + 0xFCD8, 0xFCD9, 0xFCDA, 0xFCDB, 0xFCDC, 0xFCDD, 0xFCDE, 0xFCDF, 0xFCE0, + 0xFCE1, 0xFCE2, 0xFCE3, 0xFCE4, 0xFCE5, 0xFCE6, 0xFCE7, 0xFCE8, 0xFCE9, + 0xFCEA, 0xFCEB, 0xFCEC, 0xFCED, 0xFCEE, 0xFCEF, 0xFCF0, 0xFCF1, 0xFCF2, + 0xFCF3, 0xFCF4, 0xFCF5, 0xFCF6, 0xFCF7, 0xFCF8, 0xFCF9, 0xFCFA, 0xFCFB, + 0xFCFC, 0xFCFD, 0xFCFE, 0xFCFF, 0xFD00, 0xFD01, 0xFD02, 0xFD03, 0xFD04, + 0xFD05, 0xFD06, 0xFD07, 0xFD08, 0xFD09, 0xFD0A, 0xFD0B, 0xFD0C, 0xFD0D, + 0xFD0E, 0xFD0F, 0xFD10, 0xFD11, 0xFD12, 0xFD13, 0xFD14, 0xFD15, 0xFD16, + 0xFD17, 0xFD18, 0xFD19, 0xFD1A, 0xFD1B, 0xFD1C, 0xFD1D, 0xFD1E, 0xFD1F, + 0xFD20, 0xFD21, 0xFD22, 0xFD23, 0xFD24, 0xFD25, 0xFD26, 0xFD27, 0xFD28, + 0xFD29, 0xFD2A, 0xFD2B, 0xFD2C, 0xFD2D, 0xFD2E, 0xFD2F, 0xFD30, 0xFD31, + 0xFD32, 0xFD33, 0xFD34, 0xFD35, 0xFD36, 0xFD37, 0xFD38, 0xFD39, 0xFD3A, + 0xFD3B, 0xFD3C, 0xFD3D, 0xFD50, 0xFD51, 0xFD52, 0xFD53, 0xFD54, 0xFD55, + 0xFD56, 0xFD57, 0xFD58, 0xFD59, 0xFD5A, 0xFD5B, 0xFD5C, 0xFD5D, 0xFD5E, + 0xFD5F, 0xFD60, 0xFD61, 0xFD62, 0xFD63, 0xFD64, 0xFD65, 0xFD66, 0xFD67, + 0xFD68, 0xFD69, 0xFD6A, 0xFD6B, 0xFD6C, 0xFD6D, 0xFD6E, 0xFD6F, 0xFD70, + 0xFD71, 0xFD72, 0xFD73, 0xFD74, 0xFD75, 0xFD76, 0xFD77, 0xFD78, 0xFD79, + 0xFD7A, 0xFD7B, 0xFD7C, 0xFD7D, 0xFD7E, 0xFD7F, 0xFD80, 0xFD81, 0xFD82, + 0xFD83, 0xFD84, 0xFD85, 0xFD86, 0xFD87, 0xFD88, 0xFD89, 0xFD8A, 0xFD8B, + 0xFD8C, 0xFD8D, 0xFD8E, 0xFD8F, 0xFD92, 0xFD93, 0xFD94, 0xFD95, 0xFD96, + 0xFD97, 0xFD98, 0xFD99, 0xFD9A, 0xFD9B, 0xFD9C, 0xFD9D, 0xFD9E, 0xFD9F, + 0xFDA0, 0xFDA1, 0xFDA2, 0xFDA3, 0xFDA4, 0xFDA5, 0xFDA6, 0xFDA7, 0xFDA8, + 0xFDA9, 0xFDAA, 0xFDAB, 0xFDAC, 0xFDAD, 0xFDAE, 0xFDAF, 0xFDB0, 0xFDB1, + 0xFDB2, 0xFDB3, 0xFDB4, 0xFDB5, 0xFDB6, 0xFDB7, 0xFDB8, 0xFDB9, 0xFDBA, + 0xFDBB, 0xFDBC, 0xFDBD, 0xFDBE, 0xFDBF, 0xFDC0, 0xFDC1, 0xFDC2, 0xFDC3, + 0xFDC4, 0xFDC5, 0xFDC6, 0xFDC7, 0xFDF0, 0xFDF1, 0xFDF2, 0xFDF3, 0xFDF4, + 0xFDF5, 0xFDF6, 0xFDF7, 0xFDF8, 0xFDF9, 0xFDFA, 0xFDFB, 0xFDFC, 0xFE70, + 0xFE71, 0xFE72, 0xFE73, 0xFE74, 0xFE76, 0xFE77, 0xFE78, 0xFE79, 0xFE7A, + 0xFE7B, 0xFE7C, 0xFE7D, 0xFE7E, 0xFE7F, 0xFE80, 0xFE81, 0xFE82, 0xFE83, + 0xFE84, 0xFE85, 0xFE86, 0xFE87, 0xFE88, 0xFE89, 0xFE8A, 0xFE8B, 0xFE8C, + 0xFE8D, 0xFE8E, 0xFE8F, 0xFE90, 0xFE91, 0xFE92, 0xFE93, 0xFE94, 0xFE95, + 0xFE96, 0xFE97, 0xFE98, 0xFE99, 0xFE9A, 0xFE9B, 0xFE9C, 0xFE9D, 0xFE9E, + 0xFE9F, 0xFEA0, 0xFEA1, 0xFEA2, 0xFEA3, 0xFEA4, 0xFEA5, 0xFEA6, 0xFEA7, + 0xFEA8, 0xFEA9, 0xFEAA, 0xFEAB, 0xFEAC, 0xFEAD, 0xFEAE, 0xFEAF, 0xFEB0, + 0xFEB1, 0xFEB2, 0xFEB3, 0xFEB4, 0xFEB5, 0xFEB6, 0xFEB7, 0xFEB8, 0xFEB9, + 0xFEBA, 0xFEBB, 0xFEBC, 0xFEBD, 0xFEBE, 0xFEBF, 0xFEC0, 0xFEC1, 0xFEC2, + 0xFEC3, 0xFEC4, 0xFEC5, 0xFEC6, 0xFEC7, 0xFEC8, 0xFEC9, 0xFECA, 0xFECB, + 0xFECC, 0xFECD, 0xFECE, 0xFECF, 0xFED0, 0xFED1, 0xFED2, 0xFED3, 0xFED4, + 0xFED5, 0xFED6, 0xFED7, 0xFED8, 0xFED9, 0xFEDA, 0xFEDB, 0xFEDC, 0xFEDD, + 0xFEDE, 0xFEDF, 0xFEE0, 0xFEE1, 0xFEE2, 0xFEE3, 0xFEE4, 0xFEE5, 0xFEE6, + 0xFEE7, 0xFEE8, 0xFEE9, 0xFEEA, 0xFEEB, 0xFEEC, 0xFEED, 0xFEEE, 0xFEEF, + 0xFEF0, 0xFEF1, 0xFEF2, 0xFEF3, 0xFEF4, 0xFEF5, 0xFEF6, 0xFEF7, 0xFEF8, + 0xFEF9, 0xFEFA, 0xFEFB, 0xFEFC, 0x10800, 0x10801, 0x10802, 0x10803, + 0x10804, 0x10805, 0x10808, 0x1080A, 0x1080B, 0x1080C, 0x1080D, 0x1080E, + 0x1080F, 0x10810, 0x10811, 0x10812, 0x10813, 0x10814, 0x10815, 0x10816, + 0x10817, 0x10818, 0x10819, 0x1081A, 0x1081B, 0x1081C, 0x1081D, 0x1081E, + 0x1081F, 0x10820, 0x10821, 0x10822, 0x10823, 0x10824, 0x10825, 0x10826, + 0x10827, 0x10828, 0x10829, 0x1082A, 0x1082B, 0x1082C, 0x1082D, 0x1082E, + 0x1082F, 0x10830, 0x10831, 0x10832, 0x10833, 0x10834, 0x10835, 0x10837, + 0x10838, 0x1083C, 0x1083F, 0x10840, 0x10841, 0x10842, 0x10843, 0x10844, + 0x10845, 0x10846, 0x10847, 0x10848, 0x10849, 0x1084A, 0x1084B, 0x1084C, + 0x1084D, 0x1084E, 0x1084F, 0x10850, 0x10851, 0x10852, 0x10853, 0x10854, + 0x10855, 0x10857, 0x10858, 0x10859, 0x1085A, 0x1085B, 0x1085C, 0x1085D, + 0x1085E, 0x1085F, 0x10900, 0x10901, 0x10902, 0x10903, 0x10904, 0x10905, + 0x10906, 0x10907, 0x10908, 0x10909, 0x1090A, 0x1090B, 0x1090C, 0x1090D, + 0x1090E, 0x1090F, 0x10910, 0x10911, 0x10912, 0x10913, 0x10914, 0x10915, + 0x10916, 0x10917, 0x10918, 0x10919, 0x1091A, 0x1091B, 0x10920, 0x10921, + 0x10922, 0x10923, 0x10924, 0x10925, 0x10926, 0x10927, 0x10928, 0x10929, + 0x1092A, 0x1092B, 0x1092C, 0x1092D, 0x1092E, 0x1092F, 0x10930, 0x10931, + 0x10932, 0x10933, 0x10934, 0x10935, 0x10936, 0x10937, 0x10938, 0x10939, + 0x1093F, 0x10980, 0x10981, 0x10982, 0x10983, 0x10984, 0x10985, 0x10986, + 0x10987, 0x10988, 0x10989, 0x1098A, 0x1098B, 0x1098C, 0x1098D, 0x1098E, + 0x1098F, 0x10990, 0x10991, 0x10992, 0x10993, 0x10994, 0x10995, 0x10996, + 0x10997, 0x10998, 0x10999, 0x1099A, 0x1099B, 0x1099C, 0x1099D, 0x1099E, + 0x1099F, 0x109A0, 0x109A1, 0x109A2, 0x109A3, 0x109A4, 0x109A5, 0x109A6, + 0x109A7, 0x109A8, 0x109A9, 0x109AA, 0x109AB, 0x109AC, 0x109AD, 0x109AE, + 0x109AF, 0x109B0, 0x109B1, 0x109B2, 0x109B3, 0x109B4, 0x109B5, 0x109B6, + 0x109B7, 0x109BE, 0x109BF, 0x10A00, 0x10A10, 0x10A11, 0x10A12, 0x10A13, + 0x10A15, 0x10A16, 0x10A17, 0x10A19, 0x10A1A, 0x10A1B, 0x10A1C, 0x10A1D, + 0x10A1E, 0x10A1F, 0x10A20, 0x10A21, 0x10A22, 0x10A23, 0x10A24, 0x10A25, + 0x10A26, 0x10A27, 0x10A28, 0x10A29, 0x10A2A, 0x10A2B, 0x10A2C, 0x10A2D, + 0x10A2E, 0x10A2F, 0x10A30, 0x10A31, 0x10A32, 0x10A33, 0x10A40, 0x10A41, + 0x10A42, 0x10A43, 0x10A44, 0x10A45, 0x10A46, 0x10A47, 0x10A50, 0x10A51, + 0x10A52, 0x10A53, 0x10A54, 0x10A55, 0x10A56, 0x10A57, 0x10A58, 0x10A60, + 0x10A61, 0x10A62, 0x10A63, 0x10A64, 0x10A65, 0x10A66, 0x10A67, 0x10A68, + 0x10A69, 0x10A6A, 0x10A6B, 0x10A6C, 0x10A6D, 0x10A6E, 0x10A6F, 0x10A70, + 0x10A71, 0x10A72, 0x10A73, 0x10A74, 0x10A75, 0x10A76, 0x10A77, 0x10A78, + 0x10A79, 0x10A7A, 0x10A7B, 0x10A7C, 0x10A7D, 0x10A7E, 0x10A7F, 0x10B00, + 0x10B01, 0x10B02, 0x10B03, 0x10B04, 0x10B05, 0x10B06, 0x10B07, 0x10B08, + 0x10B09, 0x10B0A, 0x10B0B, 0x10B0C, 0x10B0D, 0x10B0E, 0x10B0F, 0x10B10, + 0x10B11, 0x10B12, 0x10B13, 0x10B14, 0x10B15, 0x10B16, 0x10B17, 0x10B18, + 0x10B19, 0x10B1A, 0x10B1B, 0x10B1C, 0x10B1D, 0x10B1E, 0x10B1F, 0x10B20, + 0x10B21, 0x10B22, 0x10B23, 0x10B24, 0x10B25, 0x10B26, 0x10B27, 0x10B28, + 0x10B29, 0x10B2A, 0x10B2B, 0x10B2C, 0x10B2D, 0x10B2E, 0x10B2F, 0x10B30, + 0x10B31, 0x10B32, 0x10B33, 0x10B34, 0x10B35, 0x10B40, 0x10B41, 0x10B42, + 0x10B43, 0x10B44, 0x10B45, 0x10B46, 0x10B47, 0x10B48, 0x10B49, 0x10B4A, + 0x10B4B, 0x10B4C, 0x10B4D, 0x10B4E, 0x10B4F, 0x10B50, 0x10B51, 0x10B52, + 0x10B53, 0x10B54, 0x10B55, 0x10B58, 0x10B59, 0x10B5A, 0x10B5B, 0x10B5C, + 0x10B5D, 0x10B5E, 0x10B5F, 0x10B60, 0x10B61, 0x10B62, 0x10B63, 0x10B64, + 0x10B65, 0x10B66, 0x10B67, 0x10B68, 0x10B69, 0x10B6A, 0x10B6B, 0x10B6C, + 0x10B6D, 0x10B6E, 0x10B6F, 0x10B70, 0x10B71, 0x10B72, 0x10B78, 0x10B79, + 0x10B7A, 0x10B7B, 0x10B7C, 0x10B7D, 0x10B7E, 0x10B7F, 0x10C00, 0x10C01, + 0x10C02, 0x10C03, 0x10C04, 0x10C05, 0x10C06, 0x10C07, 0x10C08, 0x10C09, + 0x10C0A, 0x10C0B, 0x10C0C, 0x10C0D, 0x10C0E, 0x10C0F, 0x10C10, 0x10C11, + 0x10C12, 0x10C13, 0x10C14, 0x10C15, 0x10C16, 0x10C17, 0x10C18, 0x10C19, + 0x10C1A, 0x10C1B, 0x10C1C, 0x10C1D, 0x10C1E, 0x10C1F, 0x10C20, 0x10C21, + 0x10C22, 0x10C23, 0x10C24, 0x10C25, 0x10C26, 0x10C27, 0x10C28, 0x10C29, + 0x10C2A, 0x10C2B, 0x10C2C, 0x10C2D, 0x10C2E, 0x10C2F, 0x10C30, 0x10C31, + 0x10C32, 0x10C33, 0x10C34, 0x10C35, 0x10C36, 0x10C37, 0x10C38, 0x10C39, + 0x10C3A, 0x10C3B, 0x10C3C, 0x10C3D, 0x10C3E, 0x10C3F, 0x10C40, 0x10C41, + 0x10C42, 0x10C43, 0x10C44, 0x10C45, 0x10C46, 0x10C47, 0x10C48, 0x1EE00, + 0x1EE01, 0x1EE02, 0x1EE03, 0x1EE05, 0x1EE06, 0x1EE07, 0x1EE08, 0x1EE09, + 0x1EE0A, 0x1EE0B, 0x1EE0C, 0x1EE0D, 0x1EE0E, 0x1EE0F, 0x1EE10, 0x1EE11, + 0x1EE12, 0x1EE13, 0x1EE14, 0x1EE15, 0x1EE16, 0x1EE17, 0x1EE18, 0x1EE19, + 0x1EE1A, 0x1EE1B, 0x1EE1C, 0x1EE1D, 0x1EE1E, 0x1EE1F, 0x1EE21, 0x1EE22, + 0x1EE24, 0x1EE27, 0x1EE29, 0x1EE2A, 0x1EE2B, 0x1EE2C, 0x1EE2D, 0x1EE2E, + 0x1EE2F, 0x1EE30, 0x1EE31, 0x1EE32, 0x1EE34, 0x1EE35, 0x1EE36, 0x1EE37, + 0x1EE39, 0x1EE3B, 0x1EE42, 0x1EE47, 0x1EE49, 0x1EE4B, 0x1EE4D, 0x1EE4E, + 0x1EE4F, 0x1EE51, 0x1EE52, 0x1EE54, 0x1EE57, 0x1EE59, 0x1EE5B, 0x1EE5D, + 0x1EE5F, 0x1EE61, 0x1EE62, 0x1EE64, 0x1EE67, 0x1EE68, 0x1EE69, 0x1EE6A, + 0x1EE6C, 0x1EE6D, 0x1EE6E, 0x1EE6F, 0x1EE70, 0x1EE71, 0x1EE72, 0x1EE74, + 0x1EE75, 0x1EE76, 0x1EE77, 0x1EE79, 0x1EE7A, 0x1EE7B, 0x1EE7C, 0x1EE7E, + 0x1EE80, 0x1EE81, 0x1EE82, 0x1EE83, 0x1EE84, 0x1EE85, 0x1EE86, 0x1EE87, + 0x1EE88, 0x1EE89, 0x1EE8B, 0x1EE8C, 0x1EE8D, 0x1EE8E, 0x1EE8F, 0x1EE90, + 0x1EE91, 0x1EE92, 0x1EE93, 0x1EE94, 0x1EE95, 0x1EE96, 0x1EE97, 0x1EE98, + 0x1EE99, 0x1EE9A, 0x1EE9B, 0x1EEA1, 0x1EEA2, 0x1EEA3, 0x1EEA5, 0x1EEA6, + 0x1EEA7, 0x1EEA8, 0x1EEA9, 0x1EEAB, 0x1EEAC, 0x1EEAD, 0x1EEAE, 0x1EEAF, + 0x1EEB0, 0x1EEB1, 0x1EEB2, 0x1EEB3, 0x1EEB4, 0x1EEB5, 0x1EEB6, 0x1EEB7, + 0x1EEB8, 0x1EEB9, 0x1EEBA, 0x1EEBB, 0x10FFFD]; + + function determineBidi(cueDiv) { + var nodeStack = [], + text = "", + charCode; + + if (!cueDiv || !cueDiv.childNodes) { + return "ltr"; + } + + function pushNodes(nodeStack, node) { + for (var i = node.childNodes.length - 1; i >= 0; i--) { + nodeStack.push(node.childNodes[i]); + } + } + + function nextTextNode(nodeStack) { + if (!nodeStack || !nodeStack.length) { + return null; + } + + var node = nodeStack.pop(), + text = node.textContent || node.innerText; + if (text) { + // TODO: This should match all unicode type B characters (paragraph + // separator characters). See issue #115. + var m = text.match(/^.*(\n|\r)/); + if (m) { + nodeStack.length = 0; + return m[0]; + } + return text; + } + if (node.tagName === "ruby") { + return nextTextNode(nodeStack); + } + if (node.childNodes) { + pushNodes(nodeStack, node); + return nextTextNode(nodeStack); + } + } + + pushNodes(nodeStack, cueDiv); + while ((text = nextTextNode(nodeStack))) { + for (var i = 0; i < text.length; i++) { + charCode = text.charCodeAt(i); + for (var j = 0; j < strongRTLChars.length; j++) { + if (strongRTLChars[j] === charCode) { + return "rtl"; + } + } + } + } + return "ltr"; + } + + function computeLinePos(cue) { + if (typeof cue.line === "number" && + (cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) { + return cue.line; + } + if (!cue.track || !cue.track.textTrackList || + !cue.track.textTrackList.mediaElement) { + return -1; + } + var track = cue.track, + trackList = track.textTrackList, + count = 0; + for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { + if (trackList[i].mode === "showing") { + count++; + } + } + return ++count * -1; + } + + function StyleBox() { + } + + // Apply styles to a div. If there is no div passed then it defaults to the + // div on 'this'. + StyleBox.prototype.applyStyles = function(styles, div) { + div = div || this.div; + for (var prop in styles) { + if (styles.hasOwnProperty(prop)) { + div.style[prop] = styles[prop]; + } + } + }; + + StyleBox.prototype.formatStyle = function(val, unit) { + return val === 0 ? 0 : val + unit; + }; + + // Constructs the computed display state of the cue (a div). Places the div + // into the overlay which should be a block level element (usually a div). + function CueStyleBox(window, cue, styleOptions) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + var color = "rgba(255, 255, 255, 1)"; + var backgroundColor = "rgba(0, 0, 0, 0.8)"; + + if (isIE8) { + color = "rgb(255, 255, 255)"; + backgroundColor = "rgb(0, 0, 0)"; + } + + StyleBox.call(this); + this.cue = cue; + + // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will + // have inline positioning and will function as the cue background box. + this.cueDiv = parseContent(window, cue.text); + var styles = { + color: color, + backgroundColor: backgroundColor, + position: "relative", + left: 0, + right: 0, + top: 0, + bottom: 0, + display: "inline" + }; + + if (!isIE8) { + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl"; + styles.unicodeBidi = "plaintext"; + } + this.applyStyles(styles, this.cueDiv); + + // Create an absolutely positioned div that will be used to position the cue + // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS + // mirrors of them except "middle" which is "center" in CSS. + this.div = window.document.createElement("div"); + styles = { + textAlign: cue.align === "middle" ? "center" : cue.align, + font: styleOptions.font, + whiteSpace: "pre-line", + position: "absolute" + }; + + if (!isIE8) { + styles.direction = determineBidi(this.cueDiv); + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl". + stylesunicodeBidi = "plaintext"; + } + + this.applyStyles(styles); + + this.div.appendChild(this.cueDiv); + + // Calculate the distance from the reference edge of the viewport to the text + // position of the cue box. The reference edge will be resolved later when + // the box orientation styles are applied. + var textPos = 0; + switch (cue.positionAlign) { + case "start": + textPos = cue.position; + break; + case "middle": + textPos = cue.position - (cue.size / 2); + break; + case "end": + textPos = cue.position - cue.size; + break; + } + + // Horizontal box orientation; textPos is the distance from the left edge of the + // area to the left edge of the box and cue.size is the distance extending to + // the right from there. + if (cue.vertical === "") { + this.applyStyles({ + left: this.formatStyle(textPos, "%"), + width: this.formatStyle(cue.size, "%"), + }); + // Vertical box orientation; textPos is the distance from the top edge of the + // area to the top edge of the box and cue.size is the height extending + // downwards from there. + } else { + this.applyStyles({ + top: this.formatStyle(textPos, "%"), + height: this.formatStyle(cue.size, "%") + }); + } + + this.move = function(box) { + this.applyStyles({ + top: this.formatStyle(box.top, "px"), + bottom: this.formatStyle(box.bottom, "px"), + left: this.formatStyle(box.left, "px"), + right: this.formatStyle(box.right, "px"), + height: this.formatStyle(box.height, "px"), + width: this.formatStyle(box.width, "px"), + }); + }; + } + CueStyleBox.prototype = _objCreate(StyleBox.prototype); + CueStyleBox.prototype.constructor = CueStyleBox; + + // Represents the co-ordinates of an Element in a way that we can easily + // compute things with such as if it overlaps or intersects with another Element. + // Can initialize it with either a StyleBox or another BoxPosition. + function BoxPosition(obj) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + + // Either a BoxPosition was passed in and we need to copy it, or a StyleBox + // was passed in and we need to copy the results of 'getBoundingClientRect' + // as the object returned is readonly. All co-ordinate values are in reference + // to the viewport origin (top left). + var lh, height, width, top; + if (obj.div) { + height = obj.div.offsetHeight; + width = obj.div.offsetWidth; + top = obj.div.offsetTop; + + var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && + rects.getClientRects && rects.getClientRects(); + obj = obj.div.getBoundingClientRect(); + // In certain cases the outter div will be slightly larger then the sum of + // the inner div's lines. This could be due to bold text, etc, on some platforms. + // In this case we should get the average line height and use that. This will + // result in the desired behaviour. + lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length) + : 0; + + } + this.left = obj.left; + this.right = obj.right; + this.top = obj.top || top; + this.height = obj.height || height; + this.bottom = obj.bottom || (top + (obj.height || height)); + this.width = obj.width || width; + this.lineHeight = lh !== undefined ? lh : obj.lineHeight; + + if (isIE8 && !this.lineHeight) { + this.lineHeight = 13; + } + } + + // Move the box along a particular axis. Optionally pass in an amount to move + // the box. If no amount is passed then the default is the line height of the + // box. + BoxPosition.prototype.move = function(axis, toMove) { + toMove = toMove !== undefined ? toMove : this.lineHeight; + switch (axis) { + case "+x": + this.left += toMove; + this.right += toMove; + break; + case "-x": + this.left -= toMove; + this.right -= toMove; + break; + case "+y": + this.top += toMove; + this.bottom += toMove; + break; + case "-y": + this.top -= toMove; + this.bottom -= toMove; + break; + } + }; + + // Check if this box overlaps another box, b2. + BoxPosition.prototype.overlaps = function(b2) { + return this.left < b2.right && + this.right > b2.left && + this.top < b2.bottom && + this.bottom > b2.top; + }; + + // Check if this box overlaps any other boxes in boxes. + BoxPosition.prototype.overlapsAny = function(boxes) { + for (var i = 0; i < boxes.length; i++) { + if (this.overlaps(boxes[i])) { + return true; + } + } + return false; + }; + + // Check if this box is within another box. + BoxPosition.prototype.within = function(container) { + return this.top >= container.top && + this.bottom <= container.bottom && + this.left >= container.left && + this.right <= container.right; + }; + + // Check if this box is entirely within the container or it is overlapping + // on the edge opposite of the axis direction passed. For example, if "+x" is + // passed and the box is overlapping on the left edge of the container, then + // return true. + BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) { + switch (axis) { + case "+x": + return this.left < container.left; + case "-x": + return this.right > container.right; + case "+y": + return this.top < container.top; + case "-y": + return this.bottom > container.bottom; + } + }; + + // Find the percentage of the area that this box is overlapping with another + // box. + BoxPosition.prototype.intersectPercentage = function(b2) { + var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), + y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), + intersectArea = x * y; + return intersectArea / (this.height * this.width); + }; + + // Convert the positions from this box to CSS compatible positions using + // the reference container's positions. This has to be done because this + // box's positions are in reference to the viewport origin, whereas, CSS + // values are in referecne to their respective edges. + BoxPosition.prototype.toCSSCompatValues = function(reference) { + return { + top: this.top - reference.top, + bottom: reference.bottom - this.bottom, + left: this.left - reference.left, + right: reference.right - this.right, + height: this.height, + width: this.width + }; + }; + + // Get an object that represents the box's position without anything extra. + // Can pass a StyleBox, HTMLElement, or another BoxPositon. + BoxPosition.getSimpleBoxPosition = function(obj) { + var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; + var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; + var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; + + obj = obj.div ? obj.div.getBoundingClientRect() : + obj.tagName ? obj.getBoundingClientRect() : obj; + var ret = { + left: obj.left, + right: obj.right, + top: obj.top || top, + height: obj.height || height, + bottom: obj.bottom || (top + (obj.height || height)), + width: obj.width || width + }; + return ret; + }; + + // Move a StyleBox to its specified, or next best, position. The containerBox + // is the box that contains the StyleBox, such as a div. boxPositions are + // a list of other boxes that the styleBox can't overlap with. + function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { + + // Find the best position for a cue box, b, on the video. The axis parameter + // is a list of axis, the order of which, it will move the box along. For example: + // Passing ["+x", "-x"] will move the box first along the x axis in the positive + // direction. If it doesn't find a good position for it there it will then move + // it along the x axis in the negative direction. + function findBestPosition(b, axis) { + var bestPosition, + specifiedPosition = new BoxPosition(b), + percentage = 1; // Highest possible so the first thing we get is better. + + for (var i = 0; i < axis.length; i++) { + while (b.overlapsOppositeAxis(containerBox, axis[i]) || + (b.within(containerBox) && b.overlapsAny(boxPositions))) { + b.move(axis[i]); + } + // We found a spot where we aren't overlapping anything. This is our + // best position. + if (b.within(containerBox)) { + return b; + } + var p = b.intersectPercentage(containerBox); + // If we're outside the container box less then we were on our last try + // then remember this position as the best position. + if (percentage > p) { + bestPosition = new BoxPosition(b); + percentage = p; + } + // Reset the box position to the specified position. + b = new BoxPosition(specifiedPosition); + } + return bestPosition || specifiedPosition; + } + + var boxPosition = new BoxPosition(styleBox), + cue = styleBox.cue, + linePos = computeLinePos(cue), + axis = []; + + // If we have a line number to align the cue to. + if (cue.snapToLines) { + var size; + switch (cue.vertical) { + case "": + axis = [ "+y", "-y" ]; + size = "height"; + break; + case "rl": + axis = [ "+x", "-x" ]; + size = "width"; + break; + case "lr": + axis = [ "-x", "+x" ]; + size = "width"; + break; + } + + var step = boxPosition.lineHeight, + position = step * Math.round(linePos), + maxPosition = containerBox[size] + step, + initialAxis = axis[0]; + + // If the specified intial position is greater then the max position then + // clamp the box to the amount of steps it would take for the box to + // reach the max position. + if (Math.abs(position) > maxPosition) { + position = position < 0 ? -1 : 1; + position *= Math.ceil(maxPosition / step) * step; + } + + // If computed line position returns negative then line numbers are + // relative to the bottom of the video instead of the top. Therefore, we + // need to increase our initial position by the length or width of the + // video, depending on the writing direction, and reverse our axis directions. + if (linePos < 0) { + position += cue.vertical === "" ? containerBox.height : containerBox.width; + axis = axis.reverse(); + } + + // Move the box to the specified position. This may not be its best + // position. + boxPosition.move(initialAxis, position); + + } else { + // If we have a percentage line value for the cue. + var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100; + + switch (cue.lineAlign) { + case "middle": + linePos -= (calculatedPercentage / 2); + break; + case "end": + linePos -= calculatedPercentage; + break; + } + + // Apply initial line position to the cue box. + switch (cue.vertical) { + case "": + styleBox.applyStyles({ + top: styleBox.formatStyle(linePos, "%") + }); + break; + case "rl": + styleBox.applyStyles({ + left: styleBox.formatStyle(linePos, "%") + }); + break; + case "lr": + styleBox.applyStyles({ + right: styleBox.formatStyle(linePos, "%") + }); + break; + } + + axis = [ "+y", "-x", "+x", "-y" ]; + + // Get the box position again after we've applied the specified positioning + // to it. + boxPosition = new BoxPosition(styleBox); + } + + var bestPosition = findBestPosition(boxPosition, axis); + styleBox.move(bestPosition.toCSSCompatValues(containerBox)); + } + + function WebVTT() { + // Nothing + } + + // Helper to allow strings to be decoded instead of the default binary utf8 data. + WebVTT.StringDecoder = function() { + return { + decode: function(data) { + if (!data) { + return ""; + } + if (typeof data !== "string") { + throw new Error("Error - expected string data."); + } + return decodeURIComponent(encodeURIComponent(data)); + } + }; + }; + + WebVTT.convertCueToDOMTree = function(window, cuetext) { + if (!window || !cuetext) { + return null; + } + return parseContent(window, cuetext); + }; + + var FONT_SIZE_PERCENT = 0.05; + var FONT_STYLE = "sans-serif"; + var CUE_BACKGROUND_PADDING = "1.5%"; + + // Runs the processing model over the cues and regions passed to it. + // @param overlay A block level element (usually a div) that the computed cues + // and regions will be placed into. + WebVTT.processCues = function(window, cues, overlay) { + if (!window || !cues || !overlay) { + return null; + } + + // Remove all previous children. + while (overlay.firstChild) { + overlay.removeChild(overlay.firstChild); + } + + var paddedOverlay = window.document.createElement("div"); + paddedOverlay.style.position = "absolute"; + paddedOverlay.style.left = "0"; + paddedOverlay.style.right = "0"; + paddedOverlay.style.top = "0"; + paddedOverlay.style.bottom = "0"; + paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; + overlay.appendChild(paddedOverlay); + + // Determine if we need to compute the display states of the cues. This could + // be the case if a cue's state has been changed since the last computation or + // if it has not been computed yet. + function shouldCompute(cues) { + for (var i = 0; i < cues.length; i++) { + if (cues[i].hasBeenReset || !cues[i].displayState) { + return true; + } + } + return false; + } + + // We don't need to recompute the cues' display states. Just reuse them. + if (!shouldCompute(cues)) { + for (var i = 0; i < cues.length; i++) { + paddedOverlay.appendChild(cues[i].displayState); + } + return; + } + + var boxPositions = [], + containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), + fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; + var styleOptions = { + font: fontSize + "px " + FONT_STYLE + }; + + (function() { + var styleBox, cue; + + for (var i = 0; i < cues.length; i++) { + cue = cues[i]; + + // Compute the intial position and styles of the cue div. + styleBox = new CueStyleBox(window, cue, styleOptions); + paddedOverlay.appendChild(styleBox.div); + + // Move the cue div to it's correct line position. + moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); + + // Remember the computed div so that we don't have to recompute it later + // if we don't have too. + cue.displayState = styleBox.div; + + boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); + } + })(); + }; + + WebVTT.Parser = function(window, vttjs, decoder) { + if (!decoder) { + decoder = vttjs; + vttjs = {}; + } + if (!vttjs) { + vttjs = {}; + } + + this.window = window; + this.vttjs = vttjs; + this.state = "INITIAL"; + this.buffer = ""; + this.decoder = decoder || new TextDecoder("utf8"); + this.regionList = []; + }; + + WebVTT.Parser.prototype = { + // If the error is a ParsingError then report it to the consumer if + // possible. If it's not a ParsingError then throw it like normal. + reportOrThrowError: function(e) { + if (e instanceof ParsingError) { + this.onparsingerror && this.onparsingerror(e); + } else { + throw e; + } + }, + parse: function (data) { + var self = this; + + // If there is no data then we won't decode it, but will just try to parse + // whatever is in buffer already. This may occur in circumstances, for + // example when flush() is called. + if (data) { + // Try to decode the data that we received. + self.buffer += self.decoder.decode(data, {stream: true}); + } + + function collectNextLine() { + var buffer = self.buffer; + var pos = 0; + while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { + ++pos; + } + var line = buffer.substr(0, pos); + // Advance the buffer early in case we fail below. + if (buffer[pos] === '\r') { + ++pos; + } + if (buffer[pos] === '\n') { + ++pos; + } + self.buffer = buffer.substr(pos); + return line; + } + + // 3.4 WebVTT region and WebVTT region settings syntax + function parseRegion(input) { + var settings = new Settings(); + + parseOptions(input, function (k, v) { + switch (k) { + case "id": + settings.set(k, v); + break; + case "width": + settings.percent(k, v); + break; + case "lines": + settings.integer(k, v); + break; + case "regionanchor": + case "viewportanchor": + var xy = v.split(','); + if (xy.length !== 2) { + break; + } + // We have to make sure both x and y parse, so use a temporary + // settings object here. + var anchor = new Settings(); + anchor.percent("x", xy[0]); + anchor.percent("y", xy[1]); + if (!anchor.has("x") || !anchor.has("y")) { + break; + } + settings.set(k + "X", anchor.get("x")); + settings.set(k + "Y", anchor.get("y")); + break; + case "scroll": + settings.alt(k, v, ["up"]); + break; + } + }, /=/, /\s/); + + // Create the region, using default values for any values that were not + // specified. + if (settings.has("id")) { + var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); + region.width = settings.get("width", 100); + region.lines = settings.get("lines", 3); + region.regionAnchorX = settings.get("regionanchorX", 0); + region.regionAnchorY = settings.get("regionanchorY", 100); + region.viewportAnchorX = settings.get("viewportanchorX", 0); + region.viewportAnchorY = settings.get("viewportanchorY", 100); + region.scroll = settings.get("scroll", ""); + // Register the region. + self.onregion && self.onregion(region); + // Remember the VTTRegion for later in case we parse any VTTCues that + // reference it. + self.regionList.push({ + id: settings.get("id"), + region: region + }); + } + } + + // 3.2 WebVTT metadata header syntax + function parseHeader(input) { + parseOptions(input, function (k, v) { + switch (k) { + case "Region": + // 3.3 WebVTT region metadata header syntax + parseRegion(v); + break; + } + }, /:/); + } + + // 5.1 WebVTT file parsing. + try { + var line; + if (self.state === "INITIAL") { + // We can't start parsing until we have the first line. + if (!/\r\n|\n/.test(self.buffer)) { + return this; + } + + line = collectNextLine(); + + var m = line.match(/^WEBVTT([ \t].*)?$/); + if (!m || !m[0]) { + throw new ParsingError(ParsingError.Errors.BadSignature); + } + + self.state = "HEADER"; + } + + var alreadyCollectedLine = false; + while (self.buffer) { + // We can't parse a line until we have the full line. + if (!/\r\n|\n/.test(self.buffer)) { + return this; + } + + if (!alreadyCollectedLine) { + line = collectNextLine(); + } else { + alreadyCollectedLine = false; + } + + switch (self.state) { + case "HEADER": + // 13-18 - Allow a header (metadata) under the WEBVTT line. + if (/:/.test(line)) { + parseHeader(line); + } else if (!line) { + // An empty line terminates the header and starts the body (cues). + self.state = "ID"; + } + continue; + case "NOTE": + // Ignore NOTE blocks. + if (!line) { + self.state = "ID"; + } + continue; + case "ID": + // Check for the start of NOTE blocks. + if (/^NOTE($|[ \t])/.test(line)) { + self.state = "NOTE"; + break; + } + // 19-29 - Allow any number of line terminators, then initialize new cue values. + if (!line) { + continue; + } + self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); + self.state = "CUE"; + // 30-39 - Check if self line contains an optional identifier or timing data. + if (line.indexOf("-->") === -1) { + self.cue.id = line; + continue; + } + // Process line as start of a cue. + /*falls through*/ + case "CUE": + // 40 - Collect cue timings and settings. + try { + parseCue(line, self.cue, self.regionList); + } catch (e) { + self.reportOrThrowError(e); + // In case of an error ignore rest of the cue. + self.cue = null; + self.state = "BADCUE"; + continue; + } + self.state = "CUETEXT"; + continue; + case "CUETEXT": + var hasSubstring = line.indexOf("-->") !== -1; + // 34 - If we have an empty line then report the cue. + // 35 - If we have the special substring '-->' then report the cue, + // but do not collect the line as we need to process the current + // one as a new cue. + if (!line || hasSubstring && (alreadyCollectedLine = true)) { + // We are done parsing self cue. + self.oncue && self.oncue(self.cue); + self.cue = null; + self.state = "ID"; + continue; + } + if (self.cue.text) { + self.cue.text += "\n"; + } + self.cue.text += line; + continue; + case "BADCUE": // BADCUE + // 54-62 - Collect and discard the remaining cue. + if (!line) { + self.state = "ID"; + } + continue; + } + } + } catch (e) { + self.reportOrThrowError(e); + + // If we are currently parsing a cue, report what we have. + if (self.state === "CUETEXT" && self.cue && self.oncue) { + self.oncue(self.cue); + } + self.cue = null; + // Enter BADWEBVTT state if header was not parsed correctly otherwise + // another exception occurred so enter BADCUE state. + self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; + } + return this; + }, + flush: function () { + var self = this; + try { + // Finish decoding the stream. + self.buffer += self.decoder.decode(); + // Synthesize the end of the current cue or region. + if (self.cue || self.state === "HEADER") { + self.buffer += "\n\n"; + self.parse(); + } + // If we've flushed, parsed, and we're still on the INITIAL state then + // that means we don't have enough of the stream to parse the first + // line. + if (self.state === "INITIAL") { + throw new ParsingError(ParsingError.Errors.BadSignature); + } + } catch(e) { + self.reportOrThrowError(e); + } + self.onflush && self.onflush(); + return this; + } + }; + + global.WebVTT = WebVTT; + +}(this, (this.vttjs || {}))); diff --git a/videojs/video.js b/videojs/video.js old mode 100644 new mode 100755 index 2797363..826aa6a --- a/videojs/video.js +++ b/videojs/video.js @@ -1,138 +1,200 @@ -/*! Video.js v4.5.1 Copyright 2014 Brightcove, Inc. https://github.com/videojs/video.js/blob/master/LICENSE */ -(function() {var b=void 0,f=!0,h=null,l=!1;function m(){return function(){}}function p(a){return function(){return this[a]}}function q(a){return function(){return a}}var t;document.createElement("video");document.createElement("audio");document.createElement("track");function u(a,c,d){if("string"===typeof a){0===a.indexOf("#")&&(a=a.slice(1));if(u.va[a])return u.va[a];a=u.u(a)}if(!a||!a.nodeName)throw new TypeError("The element or ID supplied is not valid. (videojs)");return a.player||new u.Player(a,c,d)} -var videojs=u;window.Wd=window.Xd=u;u.Rb="4.5";u.Fc="https:"==document.location.protocol?"https://":"http://";u.options={techOrder:["html5","flash"],html5:{},flash:{},width:300,height:150,defaultVolume:0,children:{mediaLoader:{},posterImage:{},textTrackDisplay:{},loadingSpinner:{},bigPlayButton:{},controlBar:{}},notSupportedMessage:'Sorry, no compatible source and playback technology were found for this video. Try using another browser like Chrome or download the latest Adobe Flash Player.'}; -"GENERATED_CDN_VSN"!==u.Rb&&(videojs.options.flash.swf=u.Fc+"vjs.zencdn.net/"+u.Rb+"/video-js.swf");u.va={};"function"===typeof define&&define.amd?define([],function(){return videojs}):"object"===typeof exports&&"object"===typeof module&&(module.exports=videojs);u.ka=u.CoreObject=m(); -u.ka.extend=function(a){var c,d;a=a||{};c=a.init||a.j||this.prototype.init||this.prototype.j||m();d=function(){c.apply(this,arguments)};d.prototype=u.l.create(this.prototype);d.prototype.constructor=d;d.extend=u.ka.extend;d.create=u.ka.create;for(var e in a)a.hasOwnProperty(e)&&(d.prototype[e]=a[e]);return d};u.ka.create=function(){var a=u.l.create(this.prototype);this.apply(a,arguments);return a}; -u.d=function(a,c,d){var e=u.getData(a);e.z||(e.z={});e.z[c]||(e.z[c]=[]);d.s||(d.s=u.s++);e.z[c].push(d);e.V||(e.disabled=l,e.V=function(c){if(!e.disabled){c=u.ic(c);var d=e.z[c.type];if(d)for(var d=d.slice(0),k=0,r=d.length;ku.Qb;u.Ub=/Firefox/i.test(u.J);u.Ld=/Chrome/i.test(u.J);u.$b=!!("ontouchstart"in window||window.Gc&&document instanceof window.Gc); -u.wb=function(a){var c,d,e,g;c={};if(a&&a.attributes&&0e?"0"+e:e)+":")+(10>d?"0"+d:d)};u.Sc=function(){document.body.focus();document.onselectstart=q(l)};u.Ed=function(){document.onselectstart=q(f)};u.trim=function(a){return(a+"").replace(/^\s+|\s+$/g,"")};u.round=function(a,c){c||(c=0);return Math.round(a*Math.pow(10,c))/Math.pow(10,c)}; -u.sb=function(a,c){return{length:1,start:function(){return a},end:function(){return c}}}; -u.get=function(a,c,d){var e,g;"undefined"===typeof XMLHttpRequest&&(window.XMLHttpRequest=function(){try{return new window.ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new window.ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new window.ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw Error("This browser does not support XMLHttpRequest.");});g=new XMLHttpRequest;try{g.open("GET",a)}catch(j){d(j)}e=0===a.indexOf("file:")||0===window.location.href.indexOf("file:")&&-1===a.indexOf("http"); -g.onreadystatechange=function(){4===g.readyState&&(200===g.status||e&&0===g.status?c(g.responseText):d&&d())};try{g.send()}catch(k){d&&d(k)}};u.wd=function(a){try{var c=window.localStorage||l;c&&(c.volume=a)}catch(d){22==d.code||1014==d.code?u.log("LocalStorage Full (VideoJS)",d):18==d.code?u.log("LocalStorage not allowed (VideoJS)",d):u.log("LocalStorage Error (VideoJS)",d)}};u.kc=function(a){a.match(/^https?:\/\//)||(a=u.e("div",{innerHTML:'x'}).firstChild.href);return a}; -u.log=function(){u.log.history=u.log.history||[];u.log.history.push(arguments);window.console&&window.console.log(Array.prototype.slice.call(arguments))};u.cd=function(a){var c,d;a.getBoundingClientRect&&a.parentNode&&(c=a.getBoundingClientRect());if(!c)return{left:0,top:0};a=document.documentElement;d=document.body;return{left:u.round(c.left+(window.pageXOffset||d.scrollLeft)-(a.clientLeft||d.clientLeft||0)),top:u.round(c.top+(window.pageYOffset||d.scrollTop)-(a.clientTop||d.clientTop||0))}}; -u.ja={};u.ja.Eb=function(a,c){var d,e,g;a=u.l.copy(a);for(d in c)c.hasOwnProperty(d)&&(e=a[d],g=c[d],a[d]=u.l.Na(e)&&u.l.Na(g)?u.ja.Eb(e,g):c[d]);return a}; -u.b=u.ka.extend({j:function(a,c,d){this.c=a;this.h=u.l.copy(this.h);c=this.options(c);this.R=c.id||(c.el&&c.el.id?c.el.id:a.id()+"_component_"+u.s++);this.ld=c.name||h;this.a=c.el||this.e();this.K=[];this.Ja={};this.Ka={};this.nc();this.I(d);if(c.vc!==l){var e,g;e=u.bind(this.C(),this.C().reportUserActivity);this.d("touchstart",function(){e();clearInterval(g);g=setInterval(e,250)});a=function(){e();clearInterval(g)};this.d("touchmove",e);this.d("touchend",a);this.d("touchcancel",a)}}});t=u.b.prototype; -t.dispose=function(){this.k({type:"dispose",bubbles:l});if(this.K)for(var a=this.K.length-1;0<=a;a--)this.K[a].dispose&&this.K[a].dispose();this.Ka=this.Ja=this.K=h;this.o();this.a.parentNode&&this.a.parentNode.removeChild(this.a);u.uc(this.a);this.a=h};t.c=f;t.C=p("c");t.options=function(a){return a===b?this.h:this.h=u.ja.Eb(this.h,a)};t.e=function(a,c){return u.e(a,c)};t.u=p("a");t.La=function(){return this.G||this.a};t.id=p("R");t.name=p("ld");t.children=p("K");t.ed=function(a){return this.Ja[a]}; -t.fa=function(a){return this.Ka[a]};t.Y=function(a,c){var d,e;"string"===typeof a?(e=a,c=c||{},d=c.componentClass||u.Z(e),c.name=e,d=new window.videojs[d](this.c||this,c)):d=a;this.K.push(d);"function"===typeof d.id&&(this.Ja[d.id()]=d);(e=e||d.name&&d.name())&&(this.Ka[e]=d);"function"===typeof d.el&&d.el()&&this.La().appendChild(d.el());return d}; -t.removeChild=function(a){"string"===typeof a&&(a=this.fa(a));if(a&&this.K){for(var c=l,d=this.K.length-1;0<=d;d--)if(this.K[d]===a){c=f;this.K.splice(d,1);break}c&&(this.Ja[a.id]=h,this.Ka[a.name]=h,(c=a.u())&&c.parentNode===this.La()&&this.La().removeChild(a.u()))}};t.nc=function(){var a=this.h;if(a&&a.children){var c=this;u.l.ra(a.children,function(a,e){e!==l&&!e.loadEvent&&(c[a]=c.Y(a,e))})}};t.Q=q("");t.d=function(a,c){u.d(this.a,a,u.bind(this,c));return this}; -t.o=function(a,c){u.o(this.a,a,c);return this};t.U=function(a,c){u.U(this.a,a,u.bind(this,c));return this};t.k=function(a,c){u.k(this.a,a,c);return this};t.I=function(a){a&&(this.aa?a.call(this):(this.Ua===b&&(this.Ua=[]),this.Ua.push(a)));return this};t.za=function(){this.aa=f;var a=this.Ua;if(a&&0'+(this.oa||"Need Text")+"",role:"button","aria-live":"polite",tabIndex:0},c);return u.b.prototype.e.call(this,a,c)};t.Q=function(){return"vjs-control "+u.b.prototype.Q.call(this)};t.p=m();t.Qa=function(){u.d(document,"keyup",u.bind(this,this.ba))};t.ba=function(a){if(32==a.which||13==a.which)a.preventDefault(),this.p()}; -t.Pa=function(){u.o(document,"keyup",u.bind(this,this.ba))};u.O=u.b.extend({j:function(a,c){u.b.call(this,a,c);this.Rc=this.fa(this.h.barName);this.handle=this.fa(this.h.handleName);a.d(this.sc,u.bind(this,this.update));this.d("mousedown",this.Ra);this.d("touchstart",this.Ra);this.d("focus",this.Qa);this.d("blur",this.Pa);this.d("click",this.p);this.c.d("controlsvisible",u.bind(this,this.update));a.I(u.bind(this,this.update));this.P={}}});t=u.O.prototype; -t.e=function(a,c){c=c||{};c.className+=" vjs-slider";c=u.l.B({role:"slider","aria-valuenow":0,"aria-valuemin":0,"aria-valuemax":100,tabIndex:0},c);return u.b.prototype.e.call(this,a,c)};t.Ra=function(a){a.preventDefault();u.Sc();this.P.move=u.bind(this,this.Gb);this.P.end=u.bind(this,this.Hb);u.d(document,"mousemove",this.P.move);u.d(document,"mouseup",this.P.end);u.d(document,"touchmove",this.P.move);u.d(document,"touchend",this.P.end);this.Gb(a)}; -t.Hb=function(){u.Ed();u.o(document,"mousemove",this.P.move,l);u.o(document,"mouseup",this.P.end,l);u.o(document,"touchmove",this.P.move,l);u.o(document,"touchend",this.P.end,l);this.update()};t.update=function(){if(this.a){var a,c=this.xb(),d=this.handle,e=this.Rc;isNaN(c)&&(c=0);a=c;if(d){a=this.a.offsetWidth;var g=d.u().offsetWidth;a=g?g/a:0;c*=1-a;a=c+a/2;d.u().style.left=u.round(100*c,2)+"%"}e.u().style.width=u.round(100*a,2)+"%"}}; -function F(a,c){var d,e,g,j;d=a.a;e=u.cd(d);j=g=d.offsetWidth;d=a.handle;if(a.h.Gd)return j=e.top,e=c.changedTouches?c.changedTouches[0].pageY:c.pageY,d&&(d=d.u().offsetHeight,j+=d/2,g-=d),Math.max(0,Math.min(1,(j-e+g)/g));g=e.left;e=c.changedTouches?c.changedTouches[0].pageX:c.pageX;d&&(d=d.u().offsetWidth,g+=d/2,j-=d);return Math.max(0,Math.min(1,(e-g)/j))}t.Qa=function(){u.d(document,"keyup",u.bind(this,this.ba))}; -t.ba=function(a){37==a.which?(a.preventDefault(),this.yc()):39==a.which&&(a.preventDefault(),this.zc())};t.Pa=function(){u.o(document,"keyup",u.bind(this,this.ba))};t.p=function(a){a.stopImmediatePropagation();a.preventDefault()};u.W=u.b.extend();u.W.prototype.defaultValue=0;u.W.prototype.e=function(a,c){c=c||{};c.className+=" vjs-slider-handle";c=u.l.B({innerHTML:''+this.defaultValue+""},c);return u.b.prototype.e.call(this,"div",c)};u.la=u.b.extend(); -function ca(a,c){a.Y(c);c.d("click",u.bind(a,function(){C(this)}))}u.la.prototype.e=function(){var a=this.options().Uc||"ul";this.G=u.e(a,{className:"vjs-menu-content"});a=u.b.prototype.e.call(this,"div",{append:this.G,className:"vjs-menu"});a.appendChild(this.G);u.d(a,"click",function(a){a.preventDefault();a.stopImmediatePropagation()});return a};u.N=u.q.extend({j:function(a,c){u.q.call(this,a,c);this.selected(c.selected)}}); -u.N.prototype.e=function(a,c){return u.q.prototype.e.call(this,"li",u.l.B({className:"vjs-menu-item",innerHTML:this.h.label},c))};u.N.prototype.p=function(){this.selected(f)};u.N.prototype.selected=function(a){a?(this.n("vjs-selected"),this.a.setAttribute("aria-selected",f)):(this.t("vjs-selected"),this.a.setAttribute("aria-selected",l))}; -u.S=u.q.extend({j:function(a,c){u.q.call(this,a,c);this.ua=this.Ma();this.Y(this.ua);this.L&&0===this.L.length&&this.D();this.d("keyup",this.ba);this.a.setAttribute("aria-haspopup",f);this.a.setAttribute("role","button")}});t=u.S.prototype;t.na=l;t.Ma=function(){var a=new u.la(this.c);this.options().title&&a.u().appendChild(u.e("li",{className:"vjs-menu-title",innerHTML:u.Z(this.A),Cd:-1}));if(this.L=this.createItems())for(var c=0;cCurrent Time 0:00',"aria-live":"off"});a.appendChild(this.G);return a};u.$a.prototype.da=function(){var a=this.c.Va?this.c.v.currentTime:this.c.currentTime();this.G.innerHTML='Current Time '+u.ta(a,this.c.duration())}; -u.ab=u.b.extend({j:function(a,c){u.b.call(this,a,c);a.d("timeupdate",u.bind(this,this.da))}});u.ab.prototype.e=function(){var a=u.b.prototype.e.call(this,"div",{className:"vjs-duration vjs-time-controls vjs-control"});this.G=u.e("div",{className:"vjs-duration-display",innerHTML:'Duration Time 0:00',"aria-live":"off"});a.appendChild(this.G);return a}; -u.ab.prototype.da=function(){var a=this.c.duration();a&&(this.G.innerHTML='Duration Time '+u.ta(a))};u.bc=u.b.extend({j:function(a,c){u.b.call(this,a,c)}});u.bc.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-time-divider",innerHTML:"
/
"})};u.gb=u.b.extend({j:function(a,c){u.b.call(this,a,c);a.d("timeupdate",u.bind(this,this.da))}}); -u.gb.prototype.e=function(){var a=u.b.prototype.e.call(this,"div",{className:"vjs-remaining-time vjs-time-controls vjs-control"});this.G=u.e("div",{className:"vjs-remaining-time-display",innerHTML:'Remaining Time -0:00',"aria-live":"off"});a.appendChild(this.G);return a};u.gb.prototype.da=function(){this.c.duration()&&(this.G.innerHTML='Remaining Time -'+u.ta(this.c.duration()-this.c.currentTime()))}; -u.Ea=u.q.extend({j:function(a,c){u.q.call(this,a,c)}});u.Ea.prototype.oa="Fullscreen";u.Ea.prototype.Q=function(){return"vjs-fullscreen-control "+u.q.prototype.Q.call(this)};u.Ea.prototype.p=function(){this.c.isFullScreen()?(this.c.cancelFullScreen(),this.a.children[0].children[0].innerHTML="Fullscreen"):(this.c.requestFullScreen(),this.a.children[0].children[0].innerHTML="Non-Fullscreen")};u.fb=u.b.extend({j:function(a,c){u.b.call(this,a,c)}});u.fb.prototype.h={children:{seekBar:{}}}; -u.fb.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-progress-control vjs-control"})};u.Yb=u.O.extend({j:function(a,c){u.O.call(this,a,c);a.d("timeupdate",u.bind(this,this.Aa));a.I(u.bind(this,this.Aa))}});t=u.Yb.prototype;t.h={children:{loadProgressBar:{},playProgressBar:{},seekHandle:{}},barName:"playProgressBar",handleName:"seekHandle"};t.sc="timeupdate";t.e=function(){return u.O.prototype.e.call(this,"div",{className:"vjs-progress-holder","aria-label":"video progress bar"})}; -t.Aa=function(){var a=this.c.Va?this.c.v.currentTime:this.c.currentTime();this.a.setAttribute("aria-valuenow",u.round(100*this.xb(),2));this.a.setAttribute("aria-valuetext",u.ta(a,this.c.duration()))};t.xb=function(){return this.c.currentTime()/this.c.duration()};t.Ra=function(a){u.O.prototype.Ra.call(this,a);this.c.Va=f;this.Hd=!this.c.paused();this.c.pause()};t.Gb=function(a){a=F(this,a)*this.c.duration();a==this.c.duration()&&(a-=0.1);this.c.currentTime(a)}; -t.Hb=function(a){u.O.prototype.Hb.call(this,a);this.c.Va=l;this.Hd&&this.c.play()};t.zc=function(){this.c.currentTime(this.c.currentTime()+5)};t.yc=function(){this.c.currentTime(this.c.currentTime()-5)};u.cb=u.b.extend({j:function(a,c){u.b.call(this,a,c);a.d("progress",u.bind(this,this.update))}});u.cb.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-load-progress",innerHTML:'Loaded: 0%'})}; -u.cb.prototype.update=function(){this.a.style&&(this.a.style.width=u.round(100*this.c.bufferedPercent(),2)+"%")};u.Wb=u.b.extend({j:function(a,c){u.b.call(this,a,c)}});u.Wb.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-play-progress",innerHTML:'Progress: 0%'})};u.Ga=u.W.extend({j:function(a,c){u.W.call(this,a,c);a.d("timeupdate",u.bind(this,this.da))}});u.Ga.prototype.defaultValue="00:00"; -u.Ga.prototype.e=function(){return u.W.prototype.e.call(this,"div",{className:"vjs-seek-handle","aria-live":"off"})};u.Ga.prototype.da=function(){var a=this.c.Va?this.c.v.currentTime:this.c.currentTime();this.a.innerHTML=''+u.ta(a,this.c.duration())+""};u.ib=u.b.extend({j:function(a,c){u.b.call(this,a,c);a.i&&(a.i.m&&a.i.m.volumeControl===l)&&this.n("vjs-hidden");a.d("loadstart",u.bind(this,function(){a.i.m&&a.i.m.volumeControl===l?this.n("vjs-hidden"):this.t("vjs-hidden")}))}}); -u.ib.prototype.h={children:{volumeBar:{}}};u.ib.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-volume-control vjs-control"})};u.hb=u.O.extend({j:function(a,c){u.O.call(this,a,c);a.d("volumechange",u.bind(this,this.Aa));a.I(u.bind(this,this.Aa));setTimeout(u.bind(this,this.update),0)}});t=u.hb.prototype;t.Aa=function(){this.a.setAttribute("aria-valuenow",u.round(100*this.c.volume(),2));this.a.setAttribute("aria-valuetext",u.round(100*this.c.volume(),2)+"%")}; -t.h={children:{volumeLevel:{},volumeHandle:{}},barName:"volumeLevel",handleName:"volumeHandle"};t.sc="volumechange";t.e=function(){return u.O.prototype.e.call(this,"div",{className:"vjs-volume-bar","aria-label":"volume level"})};t.Gb=function(a){this.c.muted()&&this.c.muted(l);this.c.volume(F(this,a))};t.xb=function(){return this.c.muted()?0:this.c.volume()};t.zc=function(){this.c.volume(this.c.volume()+0.1)};t.yc=function(){this.c.volume(this.c.volume()-0.1)}; -u.cc=u.b.extend({j:function(a,c){u.b.call(this,a,c)}});u.cc.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-volume-level",innerHTML:''})};u.jb=u.W.extend();u.jb.prototype.defaultValue="00:00";u.jb.prototype.e=function(){return u.W.prototype.e.call(this,"div",{className:"vjs-volume-handle"})}; -u.ea=u.q.extend({j:function(a,c){u.q.call(this,a,c);a.d("volumechange",u.bind(this,this.update));a.i&&(a.i.m&&a.i.m.volumeControl===l)&&this.n("vjs-hidden");a.d("loadstart",u.bind(this,function(){a.i.m&&a.i.m.volumeControl===l?this.n("vjs-hidden"):this.t("vjs-hidden")}))}});u.ea.prototype.e=function(){return u.q.prototype.e.call(this,"div",{className:"vjs-mute-control vjs-control",innerHTML:'
Mute
'})}; -u.ea.prototype.p=function(){this.c.muted(this.c.muted()?l:f)};u.ea.prototype.update=function(){var a=this.c.volume(),c=3;0===a||this.c.muted()?c=0:0.33>a?c=1:0.67>a&&(c=2);this.c.muted()?"Unmute"!=this.a.children[0].children[0].innerHTML&&(this.a.children[0].children[0].innerHTML="Unmute"):"Mute"!=this.a.children[0].children[0].innerHTML&&(this.a.children[0].children[0].innerHTML="Mute");for(a=0;4>a;a++)u.t(this.a,"vjs-vol-"+a);u.n(this.a,"vjs-vol-"+c)}; -u.ma=u.S.extend({j:function(a,c){u.S.call(this,a,c);a.d("volumechange",u.bind(this,this.update));a.i&&(a.i.m&&a.i.m.Dc===l)&&this.n("vjs-hidden");a.d("loadstart",u.bind(this,function(){a.i.m&&a.i.m.Dc===l?this.n("vjs-hidden"):this.t("vjs-hidden")}));this.n("vjs-menu-button")}});u.ma.prototype.Ma=function(){var a=new u.la(this.c,{Uc:"div"}),c=new u.hb(this.c,u.l.B({Gd:f},this.h.Yd));a.Y(c);return a};u.ma.prototype.p=function(){u.ea.prototype.p.call(this);u.S.prototype.p.call(this)}; -u.ma.prototype.e=function(){return u.q.prototype.e.call(this,"div",{className:"vjs-volume-menu-button vjs-menu-button vjs-control",innerHTML:'
Mute
'})};u.ma.prototype.update=u.ea.prototype.update;u.Fa=u.q.extend({j:function(a,c){u.q.call(this,a,c);a.poster()&&this.src(a.poster());(!a.poster()||!a.controls())&&this.D();a.d("posterchange",u.bind(this,function(){this.src(a.poster())}));a.d("play",u.bind(this,this.D))}});var Q="backgroundSize"in u.F.style; -u.Fa.prototype.e=function(){var a=u.e("div",{className:"vjs-poster",tabIndex:-1});Q||a.appendChild(u.e("img"));return a};u.Fa.prototype.src=function(a){var c=this.u();a!==b&&(Q?c.style.backgroundImage='url("'+a+'")':c.firstChild.src=a)};u.Fa.prototype.p=function(){this.C().controls()&&this.c.play()}; -u.Vb=u.b.extend({j:function(a,c){u.b.call(this,a,c);a.d("canplay",u.bind(this,this.D));a.d("canplaythrough",u.bind(this,this.D));a.d("playing",u.bind(this,this.D));a.d("seeking",u.bind(this,this.show));a.d("seeked",u.bind(this,this.D));a.d("error",u.bind(this,this.show));a.d("ended",u.bind(this,this.D));a.d("waiting",u.bind(this,this.show))}});u.Vb.prototype.e=function(){return u.b.prototype.e.call(this,"div",{className:"vjs-loading-spinner"})};u.Ya=u.q.extend(); -u.Ya.prototype.e=function(){return u.q.prototype.e.call(this,"div",{className:"vjs-big-play-button",innerHTML:'',"aria-label":"play video"})};u.Ya.prototype.p=function(){this.c.play()}; -u.r=u.b.extend({j:function(a,c,d){c=c||{};c.vc=l;u.b.call(this,a,c,d);var e,g;g=this;e=this.C();a=function(){if(e.controls()&&!e.usingNativeControls()){var a;g.d("mousedown",g.p);g.d("touchstart",function(c){c.preventDefault();a=this.c.userActive()});g.d("touchmove",function(){a&&this.C().reportUserActivity()});var c,d,n,s;c=0;g.d("touchstart",function(){c=(new Date).getTime();n=f});s=function(){n=l};g.d("touchmove",s);g.d("touchleave",s);g.d("touchcancel",s);g.d("touchend",function(){n===f&&(d=(new Date).getTime()- -c,250>d&&this.k("tap"))});g.d("tap",g.qd)}};c=u.bind(g,g.td);this.I(a);e.d("controlsenabled",a);e.d("controlsdisabled",c)}});t=u.r.prototype;t.td=function(){this.o("tap");this.o("touchstart");this.o("touchmove");this.o("touchleave");this.o("touchcancel");this.o("touchend");this.o("click");this.o("mousedown")};t.p=function(a){0===a.button&&this.C().controls()&&(this.C().paused()?this.C().play():this.C().pause())};t.qd=function(){this.C().userActive(!this.C().userActive())};t.Lb=m(); -t.m={volumeControl:f,fullscreenResize:l,progressEvents:l,timeupdateEvents:l};u.media={};u.media.Xa="play pause paused currentTime setCurrentTime duration buffered volume setVolume muted setMuted width height supportsFullScreen enterFullScreen src load currentSrc preload setPreload autoplay setAutoplay loop setLoop error networkState readyState seeking initialTime startOffsetTime played seekable ended videoTracks audioTracks videoWidth videoHeight textTracks defaultPlaybackRate playbackRate mediaGroup controller controls defaultMuted".split(" "); -function ea(){var a=u.media.Xa[i];return function(){throw Error('The "'+a+"\" method is not available on the playback technology's API");}}for(var i=u.media.Xa.length-1;0<=i;i--)u.r.prototype[u.media.Xa[i]]=ea(); -u.g=u.r.extend({j:function(a,c,d){this.m.volumeControl=u.g.Tc();this.m.movingMediaElementInDOM=!u.Ic;this.m.fullscreenResize=f;u.r.call(this,a,c,d);for(d=u.g.bb.length-1;0<=d;d--)u.d(this.a,u.g.bb[d],u.bind(this.c,this.ad));(c=c.source)&&this.a.currentSrc===c.src&&0'});e=u.l.B({data:a,width:"100%",height:"100%"},e);u.l.ra(e,function(a,c){k+=a+'="'+c+'" '});return'"+j+""};u.f.Bd=function(a,c){return a+"&"+c}; -u.f.Ac=function(a){var c={qb:"",Mb:""};if(!a)return c;var d=a.indexOf("&"),e;-1!==d?e=d+1:(d=e=a.lastIndexOf("/")+1,0===d&&(d=e=a.length));c.qb=a.substring(0,d);c.Mb=a.substring(e,a.length);return c};u.f.jd=function(a){return a in u.f.Bc};u.f.Oc=/^rtmp[set]?:\/\//i;u.f.hd=function(a){return u.f.Oc.test(a)}; -u.Nc=u.b.extend({j:function(a,c,d){u.b.call(this,a,c,d);if(!a.h.sources||0===a.h.sources.length){c=0;for(d=a.h.techOrder;c");this.$.push(c)}this.ha=2;this.k("loaded")}; -function ia(a){var c=a.split(":");a=0;var d,e,g;3==c.length?(d=c[0],e=c[1],c=c[2]):(d=0,e=c[0],c=c[1]);c=c.split(/\s+/);c=c.splice(0,1)[0];c=c.split(/\.|,/);g=parseFloat(c[1]);c=c[0];a+=3600*parseFloat(d);a+=60*parseFloat(e);a+=parseFloat(c);g&&(a+=g/1E3);return a} -t.update=function(){if(0=this.Oa||this.Oa===b?s=this.vb!==b?this.vb:0:(g=f,s=this.Bb!==b?this.Bb:c.length-1);for(;;){n=c[s];if(n.sa<=a)e=Math.max(e,n.sa),n.Ia&&(n.Ia=l);else if(a'+a[d].text+"";this.a.innerHTML=c;this.k("cuechange")}}};t.reset=function(){this.Oa=0;this.Kb=this.c.duration();this.Bb=this.vb=0};u.Sb=u.w.extend();u.Sb.prototype.A="captions";u.Zb=u.w.extend();u.Zb.prototype.A="subtitles";u.Tb=u.w.extend();u.Tb.prototype.A="chapters"; -u.ac=u.b.extend({j:function(a,c,d){u.b.call(this,a,c,d);if(a.h.tracks&&0=this.L.length&&this.D()}}); -u.T.prototype.qa=function(){var a=[],c;a.push(new u.eb(this.c,{kind:this.A}));for(var d=0;de.readyState()){this.Nd=e;e.d("loaded",u.bind(this,this.Ma));return}g=e;break}a=this.ua=new u.la(this.c);a.a.appendChild(u.e("li",{className:"vjs-menu-title",innerHTML:u.Z(this.A),Cd:-1}));if(g){e=g.$;for(var k,c=0,d=e.length;ct.hc;t.jc=/Firefox/i.test(t.P);t.lf=/Chrome/i.test(t.P);t.oa=/MSIE\s8\.0/.test(t.P);t.Eb=!!("ontouchstart"in window||window.xd&&document instanceof window.xd);t.wd="backgroundSize"in t.A.style; +t.ed=function(a,c){t.i.da(c,function(c,e){e===j||"undefined"===typeof e||e===l?a.removeAttribute(c):a.setAttribute(c,e===f?"":e)})};t.Na=function(a){var c,d,e,g;c={};if(a&&a.attributes&&0e?"0"+e:e)+":")+(10>d?"0"+d:d)};t.Ld=function(){document.body.focus();document.onselectstart=q(l)};t.af=function(){document.onselectstart=q(f)};t.trim=function(a){return(a+"").replace(/^\s+|\s+$/g,"")};t.round=function(a,c){c||(c=0);return Math.round(a*Math.pow(10,c))/Math.pow(10,c)}; +t.Lb=function(a,c){return{length:1,start:function(){return a},end:function(){return c}}};t.Me=function(a){try{var c=window.localStorage||l;c&&(c.volume=a)}catch(d){22==d.code||1014==d.code?t.log("LocalStorage Full (VideoJS)",d):18==d.code?t.log("LocalStorage not allowed (VideoJS)",d):t.log("LocalStorage Error (VideoJS)",d)}};t.$d=function(a){a.match(/^https?:\/\//)||(a=t.e("div",{innerHTML:'x'}).firstChild.href);return a}; +t.Ee=function(a){var c,d,e,g;g="protocol hostname port pathname search hash host".split(" ");d=t.e("a",{href:a});if(e=""===d.host&&"file:"!==d.protocol)c=t.e("div"),c.innerHTML='',d=c.firstChild,c.setAttribute("style","display:none; position:absolute;"),document.body.appendChild(c);a={};for(var h=0;he&&(a.preventDefault(),this.o("tap")))})} +s.setTimeout=function(a,c){function d(){this.clearTimeout(e)}a=t.bind(this,a);var e=setTimeout(a,c);d.s="vjs-timeout-"+e;this.b("dispose",d);return e};s.clearTimeout=function(a){function c(){}clearTimeout(a);c.s="vjs-timeout-"+a;this.n("dispose",c);return a};s.setInterval=function(a,c){function d(){this.clearInterval(e)}a=t.bind(this,a);var e=setInterval(a,c);d.s="vjs-interval-"+e;this.b("dispose",d);return e}; +s.clearInterval=function(a){function c(){}clearInterval(a);c.s="vjs-interval-"+a;this.n("dispose",c);return a};t.w=t.a.extend({l:function(a,c){t.a.call(this,a,c);da(this);this.b("tap",this.u);this.b("click",this.u);this.b("focus",this.lb);this.b("blur",this.kb)}});s=t.w.prototype; +s.e=function(a,c){var d;c=t.i.D({className:this.V(),role:"button","aria-live":"polite",tabIndex:0},c);d=t.a.prototype.e.call(this,a,c);c.innerHTML||(this.B=t.e("div",{className:"vjs-control-content"}),this.Jb=t.e("span",{className:"vjs-control-text",innerHTML:this.v(this.sa)||"Need Text"}),this.B.appendChild(this.Jb),d.appendChild(this.B));return d};s.V=function(){return"vjs-control "+t.a.prototype.V.call(this)};s.u=m();s.lb=function(){t.b(document,"keydown",t.bind(this,this.ja))}; +s.ja=function(a){if(32==a.which||13==a.which)a.preventDefault(),this.u()};s.kb=function(){t.n(document,"keydown",t.bind(this,this.ja))};t.U=t.a.extend({l:function(a,c){t.a.call(this,a,c);this.Kd=this.ea(this.q.barName);this.handle=this.ea(this.q.handleName);this.b("mousedown",this.mb);this.b("touchstart",this.mb);this.b("focus",this.lb);this.b("blur",this.kb);this.b("click",this.u);this.b(a,"controlsvisible",this.update);this.b(a,this.Yc,this.update)}});s=t.U.prototype; +s.e=function(a,c){c=c||{};c.className+=" vjs-slider";c=t.i.D({role:"slider","aria-valuenow":0,"aria-valuemin":0,"aria-valuemax":100,tabIndex:0},c);return t.a.prototype.e.call(this,a,c)};s.mb=function(a){a.preventDefault();t.Ld();this.p("vjs-sliding");this.b(document,"mousemove",this.ka);this.b(document,"mouseup",this.za);this.b(document,"touchmove",this.ka);this.b(document,"touchend",this.za);this.ka(a)};s.ka=m(); +s.za=function(){t.af();this.r("vjs-sliding");this.n(document,"mousemove",this.ka);this.n(document,"mouseup",this.za);this.n(document,"touchmove",this.ka);this.n(document,"touchend",this.za);this.update()};s.update=function(){if(this.c){var a,c=this.Sb(),d=this.handle,e=this.Kd;if("number"!==typeof c||c!==c||0>c||Infinity===c)c=0;a=c;if(d){a=this.c.offsetWidth;var g=d.m().offsetWidth;a=g?g/a:0;c*=1-a;a=c+a/2;d.m().style.left=t.round(100*c,2)+"%"}e&&(e.m().style.width=t.round(100*a,2)+"%")}}; +function ea(a,c){var d,e,g,h;d=a.c;e=t.Yd(d);h=g=d.offsetWidth;d=a.handle;if(a.options().vertical)return h=e.top,e=c.changedTouches?c.changedTouches[0].pageY:c.pageY,d&&(d=d.m().offsetHeight,h+=d/2,g-=d),Math.max(0,Math.min(1,(h-e+g)/g));g=e.left;e=c.changedTouches?c.changedTouches[0].pageX:c.pageX;d&&(d=d.m().offsetWidth,g+=d/2,h-=d);return Math.max(0,Math.min(1,(e-g)/h))}s.lb=function(){this.b(document,"keydown",this.ja)}; +s.ja=function(a){if(37==a.which||40==a.which)a.preventDefault(),this.jd();else if(38==a.which||39==a.which)a.preventDefault(),this.kd()};s.kb=function(){this.n(document,"keydown",this.ja)};s.u=function(a){a.stopImmediatePropagation();a.preventDefault()};t.ga=t.a.extend();t.ga.prototype.defaultValue=0;t.ga.prototype.e=function(a,c){c=c||{};c.className+=" vjs-slider-handle";c=t.i.D({innerHTML:''+this.defaultValue+""},c);return t.a.prototype.e.call(this,"div",c)}; +t.pa=t.a.extend();function fa(a,c){a.ba(c);c.b("click",t.bind(a,function(){G(this)}))}t.pa.prototype.e=function(){var a=this.options().Cc||"ul";this.B=t.e(a,{className:"vjs-menu-content"});a=t.a.prototype.e.call(this,"div",{append:this.B,className:"vjs-menu"});a.appendChild(this.B);t.b(a,"click",function(a){a.preventDefault();a.stopImmediatePropagation()});return a};t.M=t.w.extend({l:function(a,c){t.w.call(this,a,c);this.selected(c.selected)}}); +t.M.prototype.e=function(a,c){return t.w.prototype.e.call(this,"li",t.i.D({className:"vjs-menu-item",innerHTML:this.v(this.q.label)},c))};t.M.prototype.u=function(){this.selected(f)};t.M.prototype.selected=function(a){a?(this.p("vjs-selected"),this.c.setAttribute("aria-selected",f)):(this.r("vjs-selected"),this.c.setAttribute("aria-selected",l))}; +t.O=t.w.extend({l:function(a,c){t.w.call(this,a,c);this.update();this.b("keydown",this.ja);this.c.setAttribute("aria-haspopup",f);this.c.setAttribute("role","button")}});s=t.O.prototype;s.update=function(){var a=this.Ja();this.xa&&this.removeChild(this.xa);this.xa=a;this.ba(a);this.H&&0===this.H.length?this.Y():this.H&&1a&&(a=Infinity),this.duration(a),Infinity===a?this.p("vjs-live"):this.r("vjs-live"))};s.we=function(){this.isFullscreen()?this.p("vjs-fullscreen"):this.r("vjs-fullscreen")}; +function N(a,c,d){if(a.h&&!a.h.wa)a.h.I(function(){this[c](d)});else try{a.h[c](d)}catch(e){throw t.log(e),e;}}function M(a,c){if(a.h&&a.h.wa)try{return a.h[c]()}catch(d){throw a.h[c]===b?t.log("Video.js: "+c+" method not defined for "+a.Ua+" playback technology.",d):"TypeError"==d.name?(t.log("Video.js: "+c+" unavailable on "+a.Ua+" playback technology element.",d),a.h.wa=l):t.log(d),d;}}s.play=function(){N(this,"play");return this};s.pause=function(){N(this,"pause");return this}; +s.paused=function(){return M(this,"paused")===l?l:f};s.currentTime=function(a){return a!==b?(N(this,"setCurrentTime",a),this):this.K.currentTime=M(this,"currentTime")||0};s.duration=function(a){if(a!==b)return this.K.duration=parseFloat(a),this;this.K.duration===b&&this.Wc();return this.K.duration||0};s.remainingTime=function(){return this.duration()-this.currentTime()};s.buffered=function(){var a=M(this,"buffered");if(!a||!a.length)a=t.Lb(0,0);return a}; +s.bufferedPercent=function(){var a=this.duration(),c=this.buffered(),d=0,e,g;if(!a)return 0;for(var h=0;ha&&(g=a),d+=g-e;return d/a};s.volume=function(a){if(a!==b)return a=Math.max(0,Math.min(1,parseFloat(a))),this.K.volume=a,N(this,"setVolume",a),t.Me(a),this;a=parseFloat(M(this,"volume"));return isNaN(a)?1:a};s.muted=function(a){return a!==b?(N(this,"setMuted",a),this):M(this,"muted")||l};s.Ta=function(){return M(this,"supportsFullScreen")||l};s.Qc=l; +s.isFullscreen=function(a){return a!==b?(this.Qc=!!a,this):this.Qc};s.isFullScreen=function(a){t.log.warn('player.isFullScreen() has been deprecated, use player.isFullscreen() with a lowercase "s")');return this.isFullscreen(a)}; +s.requestFullscreen=function(){var a=t.cb.Rb;this.isFullscreen(f);a?(t.b(document,a.fullscreenchange,t.bind(this,function(c){this.isFullscreen(document[a.fullscreenElement]);this.isFullscreen()===l&&t.n(document,a.fullscreenchange,arguments.callee);this.o("fullscreenchange")})),this.c[a.requestFullscreen]()):this.h.Ta()?N(this,"enterFullScreen"):(this.Jc(),this.o("fullscreenchange"));return this}; +s.requestFullScreen=function(){t.log.warn('player.requestFullScreen() has been deprecated, use player.requestFullscreen() with a lowercase "s")');return this.requestFullscreen()};s.exitFullscreen=function(){var a=t.cb.Rb;this.isFullscreen(l);if(a)document[a.exitFullscreen]();else this.h.Ta()?N(this,"exitFullScreen"):(this.Nb(),this.o("fullscreenchange"));return this};s.cancelFullScreen=function(){t.log.warn("player.cancelFullScreen() has been deprecated, use player.exitFullscreen()");return this.exitFullscreen()}; +s.Jc=function(){this.je=f;this.Ud=document.documentElement.style.overflow;t.b(document,"keydown",t.bind(this,this.Kc));document.documentElement.style.overflow="hidden";t.p(document.body,"vjs-full-window");this.o("enterFullWindow")};s.Kc=function(a){27===a.keyCode&&(this.isFullscreen()===f?this.exitFullscreen():this.Nb())};s.Nb=function(){this.je=l;t.n(document,"keydown",this.Kc);document.documentElement.style.overflow=this.Ud;t.r(document.body,"vjs-full-window");this.o("exitFullWindow")}; +s.selectSource=function(a){for(var c=0,d=this.q.techOrder;c'+this.v("Stream Type")+""+this.v("LIVE"),"aria-live":"off"});a.appendChild(this.B);return a};t.nc=t.w.extend({l:function(a,c){t.w.call(this,a,c);this.b(a,"play",this.$b);this.b(a,"pause",this.Zb)}});s=t.nc.prototype;s.sa="Play"; +s.V=function(){return"vjs-play-control "+t.w.prototype.V.call(this)};s.u=function(){this.d.paused()?this.d.play():this.d.pause()};s.$b=function(){this.r("vjs-paused");this.p("vjs-playing");this.c.children[0].children[0].innerHTML=this.v("Pause")};s.Zb=function(){this.r("vjs-playing");this.p("vjs-paused");this.c.children[0].children[0].innerHTML=this.v("Play")};t.vb=t.a.extend({l:function(a,c){t.a.call(this,a,c);this.b(a,"timeupdate",this.ma)}}); +t.vb.prototype.e=function(){var a=t.a.prototype.e.call(this,"div",{className:"vjs-current-time vjs-time-controls vjs-control"});this.B=t.e("div",{className:"vjs-current-time-display",innerHTML:'Current Time 0:00',"aria-live":"off"});a.appendChild(this.B);return a};t.vb.prototype.ma=function(){var a=this.d.ob?this.d.K.currentTime:this.d.currentTime();this.B.innerHTML=''+this.v("Current Time")+" "+t.Ma(a,this.d.duration())}; +t.wb=t.a.extend({l:function(a,c){t.a.call(this,a,c);this.b(a,"timeupdate",this.ma)}});t.wb.prototype.e=function(){var a=t.a.prototype.e.call(this,"div",{className:"vjs-duration vjs-time-controls vjs-control"});this.B=t.e("div",{className:"vjs-duration-display",innerHTML:''+this.v("Duration Time")+" 0:00","aria-live":"off"});a.appendChild(this.B);return a}; +t.wb.prototype.ma=function(){var a=this.d.duration();a&&(this.B.innerHTML=''+this.v("Duration Time")+" "+t.Ma(a))};t.tc=t.a.extend({l:function(a,c){t.a.call(this,a,c)}});t.tc.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-time-divider",innerHTML:"
/
"})};t.Db=t.a.extend({l:function(a,c){t.a.call(this,a,c);this.b(a,"timeupdate",this.ma)}}); +t.Db.prototype.e=function(){var a=t.a.prototype.e.call(this,"div",{className:"vjs-remaining-time vjs-time-controls vjs-control"});this.B=t.e("div",{className:"vjs-remaining-time-display",innerHTML:''+this.v("Remaining Time")+" -0:00","aria-live":"off"});a.appendChild(this.B);return a};t.Db.prototype.ma=function(){this.d.duration()&&(this.B.innerHTML=''+this.v("Remaining Time")+" -"+t.Ma(this.d.remainingTime()))}; +t.Za=t.w.extend({l:function(a,c){t.w.call(this,a,c)}});t.Za.prototype.sa="Fullscreen";t.Za.prototype.V=function(){return"vjs-fullscreen-control "+t.w.prototype.V.call(this)};t.Za.prototype.u=function(){this.d.isFullscreen()?(this.d.exitFullscreen(),this.Jb.innerHTML=this.v("Fullscreen")):(this.d.requestFullscreen(),this.Jb.innerHTML=this.v("Non-Fullscreen"))};t.Cb=t.a.extend({l:function(a,c){t.a.call(this,a,c)}});t.Cb.prototype.q={children:{seekBar:{}}}; +t.Cb.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-progress-control vjs-control"})};t.qc=t.U.extend({l:function(a,c){t.U.call(this,a,c);this.b(a,"timeupdate",this.Ca);a.I(t.bind(this,this.Ca))}});s=t.qc.prototype;s.q={children:{loadProgressBar:{},playProgressBar:{},seekHandle:{}},barName:"playProgressBar",handleName:"seekHandle"};s.Yc="timeupdate";s.e=function(){return t.U.prototype.e.call(this,"div",{className:"vjs-progress-holder","aria-label":"video progress bar"})}; +s.Ca=function(){var a=this.d.ob?this.d.K.currentTime:this.d.currentTime();this.c.setAttribute("aria-valuenow",t.round(100*this.Sb(),2));this.c.setAttribute("aria-valuetext",t.Ma(a,this.d.duration()))};s.Sb=function(){return this.d.currentTime()/this.d.duration()};s.mb=function(a){t.U.prototype.mb.call(this,a);this.d.ob=f;this.d.p("vjs-scrubbing");this.df=!this.d.paused();this.d.pause()};s.ka=function(a){a=ea(this,a)*this.d.duration();a==this.d.duration()&&(a-=0.1);this.d.currentTime(a)}; +s.za=function(a){t.U.prototype.za.call(this,a);this.d.ob=l;this.d.r("vjs-scrubbing");this.df&&this.d.play()};s.kd=function(){this.d.currentTime(this.d.currentTime()+5)};s.jd=function(){this.d.currentTime(this.d.currentTime()-5)};t.zb=t.a.extend({l:function(a,c){t.a.call(this,a,c);this.b(a,"progress",this.update)}});t.zb.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-load-progress",innerHTML:''+this.v("Loaded")+": 0%"})}; +t.zb.prototype.update=function(){var a,c,d,e,g=this.d.buffered();a=this.d.duration();var h,k=this.d;h=k.buffered();k=k.duration();h=h.end(h.length-1);h>k&&(h=k);k=this.c.children;this.c.style.width=100*(h/a||0)+"%";for(a=0;ag.length;a--)this.c.removeChild(k[a-1])};t.mc=t.a.extend({l:function(a,c){t.a.call(this,a,c)}}); +t.mc.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-play-progress",innerHTML:''+this.v("Progress")+": 0%"})};t.$a=t.ga.extend({l:function(a,c){t.ga.call(this,a,c);this.b(a,"timeupdate",this.ma)}});t.$a.prototype.defaultValue="00:00";t.$a.prototype.e=function(){return t.ga.prototype.e.call(this,"div",{className:"vjs-seek-handle","aria-live":"off"})}; +t.$a.prototype.ma=function(){var a=this.d.ob?this.d.K.currentTime:this.d.currentTime();this.c.innerHTML=''+t.Ma(a,this.d.duration())+""};t.Gb=t.a.extend({l:function(a,c){t.a.call(this,a,c);a.h&&a.h.featuresVolumeControl===l&&this.p("vjs-hidden");this.b(a,"loadstart",function(){a.h.featuresVolumeControl===l?this.p("vjs-hidden"):this.r("vjs-hidden")})}});t.Gb.prototype.q={children:{volumeBar:{}}}; +t.Gb.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-volume-control vjs-control"})};t.Fb=t.U.extend({l:function(a,c){t.U.call(this,a,c);this.b(a,"volumechange",this.Ca);a.I(t.bind(this,this.Ca))}});s=t.Fb.prototype;s.Ca=function(){this.c.setAttribute("aria-valuenow",t.round(100*this.d.volume(),2));this.c.setAttribute("aria-valuetext",t.round(100*this.d.volume(),2)+"%")};s.q={children:{volumeLevel:{},volumeHandle:{}},barName:"volumeLevel",handleName:"volumeHandle"}; +s.Yc="volumechange";s.e=function(){return t.U.prototype.e.call(this,"div",{className:"vjs-volume-bar","aria-label":"volume level"})};s.ka=function(a){this.d.muted()&&this.d.muted(l);this.d.volume(ea(this,a))};s.Sb=function(){return this.d.muted()?0:this.d.volume()};s.kd=function(){this.d.volume(this.d.volume()+0.1)};s.jd=function(){this.d.volume(this.d.volume()-0.1)};t.uc=t.a.extend({l:function(a,c){t.a.call(this,a,c)}}); +t.uc.prototype.e=function(){return t.a.prototype.e.call(this,"div",{className:"vjs-volume-level",innerHTML:''})};t.Hb=t.ga.extend();t.Hb.prototype.defaultValue="00:00";t.Hb.prototype.e=function(){return t.ga.prototype.e.call(this,"div",{className:"vjs-volume-handle"})}; +t.qa=t.w.extend({l:function(a,c){t.w.call(this,a,c);this.b(a,"volumechange",this.update);a.h&&a.h.featuresVolumeControl===l&&this.p("vjs-hidden");this.b(a,"loadstart",function(){a.h.featuresVolumeControl===l?this.p("vjs-hidden"):this.r("vjs-hidden")})}});t.qa.prototype.e=function(){return t.w.prototype.e.call(this,"div",{className:"vjs-mute-control vjs-control",innerHTML:'
'+this.v("Mute")+"
"})}; +t.qa.prototype.u=function(){this.d.muted(this.d.muted()?l:f)};t.qa.prototype.update=function(){var a=this.d.volume(),c=3;0===a||this.d.muted()?c=0:0.33>a?c=1:0.67>a&&(c=2);this.d.muted()?this.c.children[0].children[0].innerHTML!=this.v("Unmute")&&(this.c.children[0].children[0].innerHTML=this.v("Unmute")):this.c.children[0].children[0].innerHTML!=this.v("Mute")&&(this.c.children[0].children[0].innerHTML=this.v("Mute"));for(a=0;4>a;a++)t.r(this.c,"vjs-vol-"+a);t.p(this.c,"vjs-vol-"+c)}; +t.Fa=t.O.extend({l:function(a,c){t.O.call(this,a,c);this.b(a,"volumechange",this.ef);a.h&&a.h.featuresVolumeControl===l&&this.p("vjs-hidden");this.b(a,"loadstart",function(){a.h.featuresVolumeControl===l?this.p("vjs-hidden"):this.r("vjs-hidden")});this.p("vjs-menu-button")}});t.Fa.prototype.Ja=function(){var a=new t.pa(this.d,{Cc:"div"}),c=new t.Fb(this.d,this.q.volumeBar);c.b("focus",function(){a.p("vjs-lock-showing")});c.b("blur",function(){G(a)});a.ba(c);return a}; +t.Fa.prototype.u=function(){t.qa.prototype.u.call(this);t.O.prototype.u.call(this)};t.Fa.prototype.e=function(){return t.w.prototype.e.call(this,"div",{className:"vjs-volume-menu-button vjs-menu-button vjs-control",innerHTML:'
'+this.v("Mute")+"
"})};t.Fa.prototype.ef=t.qa.prototype.update;t.oc=t.O.extend({l:function(a,c){t.O.call(this,a,c);this.sd();this.rd();this.b(a,"loadstart",this.sd);this.b(a,"ratechange",this.rd)}});s=t.oc.prototype;s.sa="Playback Rate"; +s.className="vjs-playback-rate";s.e=function(){var a=t.O.prototype.e.call(this);this.Sc=t.e("div",{className:"vjs-playback-rate-value",innerHTML:1});a.appendChild(this.Sc);return a};s.Ja=function(){var a=new t.pa(this.k()),c=this.k().options().playbackRates;if(c)for(var d=c.length-1;0<=d;d--)a.ba(new t.Bb(this.k(),{rate:c[d]+"x"}));return a};s.Ca=function(){this.m().setAttribute("aria-valuenow",this.k().playbackRate())}; +s.u=function(){for(var a=this.k().playbackRate(),c=this.k().options().playbackRates,d=c[0],e=0;ea){d=c[e];break}this.k().playbackRate(d)};function na(a){return a.k().h&&a.k().h.featuresPlaybackRate&&a.k().options().playbackRates&&0