summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js')
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js1088
1 files changed, 1088 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js
new file mode 100644
index 00000000..8c514f70
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js
@@ -0,0 +1,1088 @@
+/**
+* Native embed library:
+*
+* Enables embedPlayer support for native html5 browser playback system
+*/
+( function( mw, $ ) { "use strict";
+
+mw.EmbedPlayerNative = {
+
+ //Instance Name
+ instanceOf: 'Native',
+
+ // Flag to only load the video ( not play it )
+ onlyLoadFlag:false,
+
+ //Callback fired once video is "loaded"
+ onLoadedCallback: null,
+
+ // The previous "currentTime" to sniff seek actions
+ // NOTE the bug where onSeeked does not seem fire consistently may no longer be applicable
+ prevCurrentTime: -1,
+
+ // Store the progress event ( updated during monitor )
+ progressEventData: null,
+
+ // If the media loaded event has been fired
+ mediaLoadedFlag: null,
+
+ // A flag to keep the video tag offscreen.
+ keepPlayerOffScreenFlag: null,
+
+ // A flag to designate the first play event, as to not propagate the native event in this case
+ isFirstEmbedPlay: null,
+
+ // A local var to store the current seek target time:
+ currentSeekTargetTime: null,
+
+ // All the native events per:
+ // http://www.w3.org/TR/html5/video.html#mediaevents
+ nativeEvents : [
+ 'loadstart',
+ 'progress',
+ 'suspend',
+ 'abort',
+ 'error',
+ 'emptied',
+ 'stalled',
+ 'play',
+ 'pause',
+ 'loadedmetadata',
+ 'loadeddata',
+ 'waiting',
+ 'playing',
+ 'canplay',
+ 'canplaythrough',
+ 'seeking',
+ 'seeked',
+ 'timeupdate',
+ 'ended',
+ 'ratechange',
+ 'durationchange',
+ 'volumechange'
+ ],
+
+ // Native player supported feature set
+ supports: {
+ 'playHead' : true,
+ 'pause' : true,
+ 'fullscreen' : true,
+ 'sourceSwitch': true,
+ 'timeDisplay' : true,
+ 'volumeControl' : true,
+ 'overlays' : true
+ },
+ /**
+ * Updates the supported features given the "type of player"
+ */
+ updateFeatureSupport: function(){
+ // The native controls function checks for overly support
+ // especially the special case of iPad in-dom or not support
+ if( this.useNativePlayerControls() ) {
+ this.supports.overlays = false;
+ this.supports.volumeControl = false;
+ }
+ // iOS does not support volume control
+ if( mw.isIpad() ){
+ this.supports.volumeControl = false;
+ }
+ // Check if we already have a selected source and a player in the page,
+ if( this.getPlayerElement() && this.getSrc() ){
+ $( this.getPlayerElement() ).attr( 'src', this.getSrc() );
+ }
+ // Check if we already have a video element an apply bindings ( for native interfaces )
+ if( this.getPlayerElement() ){
+ this.applyMediaElementBindings();
+ }
+
+ this.parent_updateFeatureSupport();
+ },
+ /**
+ * Adds an HTML screen and moves the video tag off screen, works around some iPhone bugs
+ */
+ addPlayScreenWithNativeOffScreen: function(){
+ var _this = this;
+ // Hide the player offscreen:
+ this.hidePlayerOffScreen();
+ this.keepPlayerOffScreenFlag = true;
+
+ // Add a play button on the native player:
+ this.addLargePlayBtn();
+
+ // Add a binding to show loader once clicked to show the loader
+ // bad ui to leave the play button displayed
+ this.$interface.find( '.play-btn-large' ).click( function(){
+ _this.$interface.find( '.play-btn-large' ).hide();
+ _this.addPlayerSpinner();
+ _this.hideSpinnerOncePlaying();
+ });
+
+ // Add an image poster:
+ var posterSrc = ( this.poster ) ? this.poster :
+ mw.config.get( 'EmbedPlayer.BlackPixel' );
+
+ // Check if the poster is already present:
+ if( $( this ).find( '.playerPoster' ).length ){
+ $( this ).find( '.playerPoster' ).attr('src', posterSrc );
+ } else {
+ $( this ).append(
+ $('<img />').css({
+ 'margin' : '0',
+ 'width': '100%',
+ 'height': '100%'
+ })
+ .attr( 'src', posterSrc)
+ .addClass('playerPoster')
+ )
+ }
+ $( this ).show();
+ },
+ /**
+ * Return the embed code
+ */
+ embedPlayerHTML : function () {
+ var _this = this;
+ var vid = _this.getPlayerElement();
+ this.isFirstEmbedPlay = true;
+
+ // Check if we should have a play button on the native player:
+ if( this.useLargePlayBtn() ){
+ this.addLargePlayBtn();
+ }
+
+ if( vid && $( vid ).attr('src') == this.getSrc( this.currentTime ) ){
+ _this.postEmbedActions();
+ return ;
+ }
+ mw.log( "EmbedPlayerNative::embedPlayerHTML > play url:" + this.getSrc( this.currentTime ) + ' startOffset: ' + this.start_ntp + ' end: ' + this.end_ntp );
+
+ // Check if using native controls and already the "pid" is already in the DOM
+ if( this.isPersistentNativePlayer() && vid ) {
+ _this.postEmbedActions();
+ return ;
+ }
+ // Reset some play state flags:
+ _this.bufferStartFlag = false;
+ _this.bufferEndFlag = false;
+
+ $( this ).html(
+ _this.getNativePlayerHtml()
+ );
+
+ // Directly run postEmbedActions ( if playerElement is not available it will retry )
+ _this.postEmbedActions();
+ },
+
+ /**
+ * Get the native player embed code.
+ *
+ * @param {object} playerAttributes Attributes to be override in function call
+ * @return {object} cssSet css to apply to the player
+ */
+ getNativePlayerHtml: function( playerAttributes, cssSet ){
+ if( !playerAttributes) {
+ playerAttributes = {};
+ }
+ // Update required attributes
+ if( !playerAttributes['id'] ){
+ playerAttributes['id'] = this.pid;
+ }
+ if( !playerAttributes['src'] ){
+ playerAttributes['src'] = this.getSrc( this.currentTime);
+ }
+
+ // If autoplay pass along to attribute ( needed for iPad / iPod no js autoplay support
+ if( this.autoplay ) {
+ playerAttributes['autoplay'] = 'true';
+ }
+
+ if( !cssSet ){
+ cssSet = {};
+ }
+
+ // Set default width height to 100% of parent container
+ if( !cssSet['width'] ) cssSet['width'] = '100%';
+ if( !cssSet['height'] ) cssSet['height'] = '100%';
+
+ // Also need to set the loop param directly for iPad / iPod
+ if( this.loop ) {
+ playerAttributes['loop'] = 'true';
+ }
+
+ var tagName = this.isAudio() ? 'audio' : 'video';
+
+ return $( '<' + tagName + ' />' )
+ // Add the special nativeEmbedPlayer to avoid any rewrites of of this video tag.
+ .addClass( 'nativeEmbedPlayerPid' )
+ .attr( playerAttributes )
+ .css( cssSet )
+ },
+
+ /**
+ * Post element javascript, binds event listeners and starts monitor
+ */
+ postEmbedActions: function() {
+ var _this = this;
+ // Setup local pointer:
+ var vid = this.getPlayerElement();
+ if( !vid ){
+ return ;
+ }
+ // Update the player source ( if needed )
+ if( $( vid).attr( 'src' ) != this.getSrc( this.currentTime ) ){
+ $( vid ).attr( 'src', this.getSrc( this.currentTime ) );
+ }
+ // Update the WebKitPlaysInline value
+ if( mw.config.get( 'EmbedPlayer.WebKitPlaysInline') ){
+ $( vid ).attr( 'webkit-playsinline', 1 );
+ }
+ // Update the EmbedPlayer.WebKitAllowAirplay option:
+ if( mw.config.get( 'EmbedPlayer.WebKitAllowAirplay' ) ){
+ $( vid ).attr( 'x-webkit-airplay', "allow" );
+ }
+ // make sure to display native controls if enabled:
+ if( this.useNativePlayerControls() ){
+ $( vid ).attr( 'controls', "true" );
+ }
+
+ // Apply media element bindings:
+ _this.applyMediaElementBindings();
+
+ // Make sure we start playing in the correct place:
+ if( this.currentTime != vid.currentTime ){
+ var waitReadyStateCount = 0;
+ var checkReadyState = function(){
+ if( vid.readyState > 0 ){
+ vid.currentTime = this.currentTime;
+ return ;
+ }
+ if( waitReadyStateCount > 1000 ){
+ mw.log("Error: EmbedPlayerNative: could not run native seek");
+ return ;
+ }
+ waitReadyStateCount++;
+ setTimeout( function() {
+ checkReadyState();
+ }, 10 );
+ };
+ }
+ // Some mobile devices ( iOS need a load call before play will work )
+ if ( !_this.loop ) {
+ vid.load();
+ }
+ },
+ /**
+ * Apply media element bindings
+ */
+ applyMediaElementBindings: function(){
+ var _this = this;
+ mw.log("EmbedPlayerNative::MediaElementBindings");
+ var vid = this.getPlayerElement();
+ if( ! vid ){
+ mw.log( " Error: applyMediaElementBindings without player elemnet");
+ return ;
+ }
+ $.each( _this.nativeEvents, function( inx, eventName ){
+ $( vid ).unbind( eventName + '.embedPlayerNative').bind( eventName + '.embedPlayerNative', function(){
+ if( _this._propagateEvents ){
+ var argArray = $.makeArray( arguments );
+ // always pass the current ref id as the last argument
+ // helps check against some event trigger ref issues in jQuery
+ argArray.push( _this.id );
+ // Check if there is local handler:
+ if( _this[ '_on' + eventName ] ){
+ _this[ '_on' + eventName ].apply( _this, argArray);
+ } else {
+ // No local handler directly propagate the event to the abstract object:
+ $( _this ).trigger( eventName, argArray );
+ }
+ }
+ });
+ });
+ },
+
+ // basic monitor function to update buffer
+ monitor: function(){
+ var _this = this;
+ var vid = _this.getPlayerElement();
+
+ // Update the bufferedPercent
+ if( vid && vid.buffered && vid.buffered.end && vid.duration ) {
+ try{
+ this.bufferedPercent = ( vid.buffered.end(0) / vid.duration );
+ } catch ( e ){
+ // opera does not have buffered.end zero index support ?
+ }
+ }
+ _this.parent_monitor();
+ },
+
+
+ /**
+ * Issue a seeking request.
+ *
+ * @param {Float} percent
+ * @param {bollean} stopAfterSeek if the player should stop after the seek
+ */
+ seek: function( percent, stopAfterSeek ) {
+ // bounds check
+ if( percent < 0 ){
+ percent = 0;
+ }
+
+ if( percent > 1 ){
+ percent = 1;
+ }
+ mw.log( 'EmbedPlayerNative::seek p: ' + percent + ' : ' + this.supportsURLTimeEncoding() + ' dur: ' + this.getDuration() + ' sts:' + this.seekTimeSec );
+
+ // Trigger preSeek event for plugins that want to store pre seek conditions.
+ this.triggerHelper( 'preSeek', percent );
+
+ this.seeking = true;
+ // Update the current time ( local property )
+ this.currentTime = ( percent * this.duration ).toFixed( 2 ) ;
+
+ // trigger the seeking event:
+ mw.log( 'EmbedPlayerNative::seek:trigger' );
+ this.triggerHelper( 'seeking' );
+
+ // Run the onSeeking interface update
+ this.controlBuilder.onSeek();
+
+ // @@todo check if the clip is loaded here (if so we can do a local seek)
+ if ( this.supportsURLTimeEncoding() ) {
+ // Make sure we could not do a local seek instead:
+ if ( percent < this.bufferedPercent && this.playerElement.duration && !this.didSeekJump ) {
+ mw.log( "EmbedPlayerNative::seek local seek " + percent + ' is already buffered < ' + this.bufferedPercent );
+ this.doNativeSeek( percent );
+ } else {
+ // We support URLTimeEncoding call parent seek:
+ this.parent_seek( percent );
+ }
+ } else {
+ // Try to do a play then seek:
+ this.doNativeSeek( percent );
+ }
+ },
+
+ /**
+ * Do a native seek by updating the currentTime
+ * @param {float} percent
+ * Percent to seek to of full time
+ */
+ doNativeSeek: function( percent, callback ) {
+ // If player already seeking, exit
+ var _this = this;
+ // chrome crashes with multiple seeks:
+ if( (navigator.userAgent.indexOf('Chrome') === -1) && _this.playerElement.seeking ) {
+ return ;
+ }
+
+ mw.log( 'EmbedPlayerNative::doNativeSeek::' + percent );
+ this.seeking = true;
+
+ this.seekTimeSec = 0;
+
+ // Hide iPad video off screen ( iOS shows quicktime logo during seek )
+ if( mw.isIOS() ){
+ this.hidePlayerOffScreen();
+ }
+
+ this.setCurrentTime( ( percent * this.duration ) , function(){
+ // Update the current time ( so that there is not a monitor delay in reflecting "seeked time" )
+ _this.currentTime = _this.getPlayerElement().currentTime;
+ // Done seeking ( should be a fallback trigger event ) :
+ if( _this.seeking ){
+ _this.seeking = false;
+ $( _this ).trigger( 'seeked' );
+ }
+ // restore iPad video position:
+ _this.restorePlayerOnScreen();
+
+ _this.monitor();
+ // issue the callback:
+ if( callback ){
+ callback();
+ }
+ });
+ },
+
+ /**
+ * Seek in a existing stream, we first play then seek to work around issues with iPad seeking.
+ *
+ * @param {Float} percent
+ * percent of the stream to seek to between 0 and 1
+ */
+ doPlayThenSeek: function( percent ) {
+ mw.log( 'EmbedPlayerNative::doPlayThenSeek::' + percent + ' isPaused ' + this.paused);
+ var _this = this;
+ var oldPauseState = this.paused;
+ this.play();
+ var retryCount = 0;
+ var readyForSeek = function() {
+ _this.getPlayerElement();
+ // If we have duration then we are ready to do the seek
+ if ( _this.playerElement && _this.playerElement.duration ) {
+ _this.doNativeSeek( percent, function(){
+ // restore pause if paused:
+ if( oldPauseState ){
+ _this.pause();
+ }
+ } );
+ } else {
+ // Try to get player for 30 seconds:
+ // (it would be nice if the onmetadata type callbacks where fired consistently)
+ if ( retryCount < 800 ) {
+ setTimeout( readyForSeek, 10 );
+ retryCount++;
+ } else {
+ mw.log( 'EmbedPlayerNative:: Error: doPlayThenSeek failed :' + _this.playerElement.duration);
+ }
+ }
+ };
+ readyForSeek();
+ },
+
+ /**
+ * Set the current time with a callback
+ *
+ * @param {Float} position
+ * Seconds to set the time to
+ * @param {Function} callback
+ * Function called once time has been set.
+ */
+ setCurrentTime: function( seekTime , callback, callbackCount ) {
+ var _this = this;
+ if( !callbackCount ){
+ callbackCount = 0;
+ }
+ mw.log( "EmbedPlayerNative:: setCurrentTime seekTime:" + seekTime + ' count:' + callbackCount );
+
+ // Make sure all the timeouts don't seek to an expired target:
+ $( this ).data('currentSeekTarget', seekTime );
+
+ var vid = this.getPlayerElement();
+ // add a callback handler to null out callback:
+ var callbackHandler = function(){
+ if( $.isFunction( callback ) ){
+ callback();
+ callback = null;
+ }
+ }
+ if( !vid ) {
+ callbackHandler();
+ _this.currentSeekTargetTime = seekTime.toFixed( 2 );
+ return;
+ }
+ // Check if player is ready for seek:
+ if( vid.readyState < 1 ){
+ // Try to seek for 4 seconds:
+ if( callbackCount >= 40 ){
+ mw.log("Error:: EmbedPlayerNative: with seek request, media never in ready state");
+ callbackHandler();
+ return ;
+ }
+ setTimeout( function(){
+ // Check that this seek did not expire:
+ if( $( _this ).data('currentSeekTarget') != seekTime ){
+ mw.log("EmbedPlayerNative:: expired seek target");
+ return ;
+ }
+ _this.setCurrentTime( seekTime, callback , callbackCount+1);
+ }, 100 );
+ return ;
+ }
+ // Check if currentTime is already set to the seek target:
+ if( vid.currentTime.toFixed(2) == seekTime.toFixed(2) ){
+ mw.log("EmbedPlayerNative:: setCurrentTime: current time matches seek target: " +
+ vid.currentTime.toFixed(2) + ' == ' + seekTime.toFixed(2) );
+ callbackHandler();
+ return;
+ }
+ // setup a namespaced seek bind:
+ var seekBind = 'seeked.nativeSeekBind';
+
+ // Remove any old listeners
+ $( vid ).unbind( seekBind );
+ // Bind a seeked listener for the callback
+ $( vid ).bind( seekBind, function( event ) {
+ // Remove the listener:
+ $( vid ).unbind( seekBind );
+
+ // Check if seeking to zero:
+ if( seekTime == 0 && vid.currentTime == 0 ){
+ callbackHandler();
+ return ;
+ }
+
+ // Check if we got a valid seek:
+ if( vid.currentTime > 0 ){
+ callbackHandler();
+ } else {
+ mw.log( "Error:: EmbedPlayerNative: seek callback without time updatet " + vid.currentTime );
+ }
+ });
+ setTimeout(function(){
+ // Check that this seek did not expire:
+ if( $( _this ).data('currentSeekTarget') != seekTime ){
+ mw.log("EmbedPlayerNative:: Expired seek target");
+ return ;
+ }
+
+ if( $.isFunction( callback ) ){
+ // if seek is within 5 seconds of the target assume success. ( key frame intervals can mess with seek accuracy )
+ // this only runs where the seek callback failed ( i.e broken html5 seek ? )
+ if( Math.abs( vid.currentTime - seekTime ) < 5 ){
+ mw.log( "EmbedPlayerNative:: Seek time is within 5 seconds of target, sucessfull seek");
+ callback();
+ } else {
+ mw.log( "Error:: EmbedPlayerNative: Seek still has not made a callback after 5 seconds, retry");
+ _this.setCurrentTime( seekTime, callback , callbackCount++ );
+ }
+ }
+ }, 5000);
+
+ // Try to update the playerElement time:
+ try {
+ _this.currentSeekTargetTime = seekTime.toFixed( 2 );
+ // use toFixed ( iOS issue with float seek times )
+ vid.currentTime = _this.currentSeekTargetTime;
+ } catch ( e ) {
+ mw.log("Error:: EmbedPlayerNative: Could not set video tag seekTime");
+ callbackHandler();
+ return ;
+ }
+
+ // Check for seeking state ( some player iOS / iPad can only seek while playing )
+ if(! vid.seeking ){
+ mw.log( "Error:: not entering seek state, play and wait for positive time" );
+ vid.play();
+ setTimeout(function(){
+ _this.waitForPositiveCurrentTime( function(){
+ mw.log("EmbedPlayerNative:: Got possitive time:" + vid.currentTime.toFixed(3) + ", trying to seek again");
+ _this.setCurrentTime( seekTime , callback, callbackCount+1 );
+ });
+ }, mw.config.get( 'EmbedPlayer.MonitorRate' ) );
+ }
+ },
+ waitForPositiveCurrentTime: function( callback ){
+ var _this = this;
+ var vid = this.getPlayerElement();
+ this.waitForPositiveCurrentTimeCount++;
+ // Wait for playback for 10 seconds
+ if( vid.currentTime > 0 ){
+ mw.log( 'EmbedPlayerNative:: waitForPositiveCurrentTime success' );
+ callback();
+ } else if( this.waitForPositiveCurrentTimeCount > 200 ){
+ mw.log( "Error:: waitForPositiveCurrentTime failed to reach possitve time");
+ callback();
+ } else {
+ setTimeout(function(){ _this.waitForPositiveCurrentTime( callback ) }, 50 )
+ }
+ },
+ /**
+ * Get the embed player time
+ */
+ getPlayerElementTime: function() {
+ var _this = this;
+ // Make sure we have .vid obj
+ this.getPlayerElement();
+ if ( !this.playerElement ) {
+ mw.log( 'EmbedPlayerNative::getPlayerElementTime: ' + this.id + ' not in dom ( stop monitor)' );
+ this.stop();
+ return false;
+ }
+ var ct = this.playerElement.currentTime;
+ // Return 0 or a positive number:
+ if( ! ct || isNaN( ct ) || ct < 0 || ! isFinite( ct ) ){
+ return 0;
+ }
+ // Return the playerElement currentTime
+ return this.playerElement.currentTime;
+ },
+
+ // Update the poster src ( updates the native object if in dom )
+ updatePosterSrc: function( src ){
+ if( this.getPlayerElement() ){
+ $( this.getPlayerElement() ).attr('poster', src );
+ }
+ // Also update the embedPlayer poster
+ this.parent_updatePosterSrc( src );
+ },
+ /**
+ * Empty player sources from the active video tag element
+ */
+ emptySources: function(){
+ // empty player source:
+ $( this.getPlayerElement() ).attr( 'src', null );
+ // empty out generic sources:
+ this.parent_emptySources();
+ },
+ /**
+ * playerSwitchSource switches the player source working around a few bugs in browsers
+ *
+ * @param {Object}
+ * Source object to switch to.
+ * @param {function}
+ * switchCallback Function to call once the source has been switched
+ * @param {function}
+ * doneCallback Function to call once the clip has completed playback
+ */
+ playerSwitchSource: function( source, switchCallback, doneCallback ){
+ var _this = this;
+ var src = source.getSrc();
+ var vid = this.getPlayerElement();
+ var switchBindPostfix = '.playerSwitchSource';
+ this.isPauseLoading = false;
+ // Make sure the switch source is different:
+ if( !src || src == vid.src ){
+ if( $.isFunction( switchCallback ) ){
+ switchCallback( vid );
+ }
+ // Delay done callback to allow any non-blocking switch callback code to fully execute
+ if( $.isFunction( doneCallback ) ){
+ doneCallback();
+ }
+ return ;
+ }
+
+ // only display switch msg if actually switching:
+ mw.log( 'EmbedPlayerNative:: playerSwitchSource: ' + src + ' native time: ' + vid.currentTime );
+
+ // Update some parent embedPlayer vars:
+ this.currentTime = 0;
+ this.previousTime = 0;
+ if ( vid ) {
+ try {
+ // Remove all switch player bindings
+ $( vid ).unbind( switchBindPostfix );
+
+ // pause before switching source
+ vid.pause();
+
+ var orginalControlsState = vid.controls;
+ // Hide controls ( to not display native play button while switching sources )
+ vid.removeAttribute('controls');
+
+ // dissable seeking ( if we were in a seeking state before the switch )
+ _this.seeking = false;
+
+ // add a loading indicator:
+ _this.addPlayerSpinner();
+
+ // Do the actual source switch:
+ vid.src = src;
+ // load the updated src
+ vid.load();
+
+ // hide the player offscreen while we switch
+ _this.hidePlayerOffScreen();
+ // restore position once we have metadata
+ $( vid ).bind( 'loadedmetadata' + switchBindPostfix, function(){
+ $( vid ).unbind( 'loadedmetadata' + switchBindPostfix);
+ mw.log("EmbedPlayerNative:: playerSwitchSource> loadedmetadata callback for:" + src + ' switchCallback: ' + switchCallback );
+ // Only update duration if we didn't get it server side
+ // Some browsers report bad duration (e.g. Android native browser)
+ // So avoid using the browser detected value if possible.
+ if ( !_this.duration && isFinite( vid.duration ) ) {
+ _this.duration = vid.duration;
+ }
+ // keep going towards playback! if switchCallback has not been called yet
+ // we need the "playing" event to trigger the switch callback
+ if ( $.isFunction( switchCallback ) ){
+ vid.play();
+ }
+ });
+
+ var handleSwitchCallback = function(){
+ // restore video position ( now that we are playing with metadata size )
+ _this.restorePlayerOnScreen();
+ // play hide loading spinner:
+ _this.hideSpinnerAndPlayBtn();
+ // Restore
+ vid.controls = orginalControlsState;
+ // check if we have a switch callback and issue it now:
+ if ( $.isFunction( switchCallback ) ){
+ switchCallback( vid );
+ switchCallback = null;
+ }
+ }
+
+ // once playing issue callbacks:
+ $( vid ).bind( 'playing' + switchBindPostfix, function(){
+ $( vid ).unbind( 'playing' + switchBindPostfix );
+ mw.log("EmbedPlayerNative:: playerSwitchSource> playing callback");
+ handleSwitchCallback();
+ });
+
+ // Add the end binding if we have a post event:
+ if( $.isFunction( doneCallback ) ){
+ $( vid ).bind( 'ended' + switchBindPostfix , function( event ) {
+ // remove end binding:
+ $( vid ).unbind( switchBindPostfix );
+ // issue the doneCallback
+ doneCallback();
+
+ // Support loop for older iOS
+ // Temporarly disabled pending more testing or refactor into a better place.
+ //if ( _this.loop ) {
+ // vid.play();
+ //}
+ return false;
+ });
+ }
+
+ // issue the play request:
+ vid.play();
+
+ // check if ready state is loading or doing anything ( iOS play restriction )
+ // give iOS 5 seconds to ~start~ loading media
+ setTimeout(function(){
+ // Check that the player got out of readyState 0
+ if( vid.readyState === 0 && $.isFunction( switchCallback ) ){
+ mw.log("EmbedPlayerNative:: possible iOS play without gesture failed, issue callback");
+ // hand off to the swtich callback method.
+ handleSwitchCallback();
+ // make sure we are in a pause state ( failed to change and play media );
+ _this.pause();
+ // show the big play button so the user can give us a user gesture:
+ if( ! _this.useLargePlayBtn() ){
+ _this.addLargePlayBtn();
+ }
+ }
+ }, 5000 );
+
+
+ } catch (e) {
+ mw.log("Error: EmbedPlayerNative Error in switching source playback");
+ }
+ }
+ },
+ hidePlayerOffScreen:function( vid ){
+ var vid = this.getPlayerElement();
+ // Move the video offscreen while it switches ( hides quicktime logo only applies to iPad )
+ $( vid ).css( {
+ 'position' : 'absolute',
+ 'left': '-4048px'
+ });
+ },
+ restorePlayerOnScreen: function( vid ){
+ var vid = this.getPlayerElement();
+ if( this.keepPlayerOffScreenFlag ){
+ return ;
+ }
+
+ // Remove any poster div ( that would overlay the player )
+ $( this ).find( '.playerPoster' ).remove();
+ // Restore video pos before calling sync syze
+ $( vid ).css( {
+ 'left': '0px'
+ });
+ },
+ /**
+ * Pause the video playback
+ * calls parent_pause to update the interface
+ */
+ pause: function( ) {
+ this.getPlayerElement();
+ this.parent_pause(); // update interface
+ if ( this.playerElement ) { // update player
+ this.playerElement.pause();
+ }
+ },
+
+ /**
+ * Play back the video stream
+ * calls parent_play to update the interface
+ */
+ play: function() {
+ var _this = this;
+ // if starting playback from stoped state and not in an ad or otherise blocked controls state:
+ // restore player:
+ if( this.isStopped() && this._playContorls ){
+ this.restorePlayerOnScreen();
+ }
+ // Run parent play:
+ if( _this.parent_play() ){
+ if ( this.getPlayerElement() && this.getPlayerElement().play ) {
+ mw.log( "EmbedPlayerNative:: issue native play call" );
+ // If in pauseloading state make sure the loading spinner is present:
+ if( this.isPauseLoading ){
+ this.hideSpinnerOncePlaying();
+ }
+ // issue a play request
+ this.getPlayerElement().play();
+ // re-start the monitor:
+ this.monitor();
+ }
+ } else {
+ mw.log( "EmbedPlayerNative:: parent play returned false, don't issue play on native element");
+ }
+ },
+
+ /**
+ * Stop the player ( end all listeners )
+ */
+ stop: function(){
+ var _this = this;
+ if( this.playerElement && this.playerElement.currentTime){
+ this.playerElement.currentTime = 0;
+ this.playerElement.pause();
+ }
+ this.parent_stop();
+ },
+
+ /**
+ * Toggle the Mute
+ * calls parent_toggleMute to update the interface
+ */
+ toggleMute: function() {
+ this.parent_toggleMute();
+ this.getPlayerElement();
+ if ( this.playerElement )
+ this.playerElement.muted = this.muted;
+ },
+
+ /**
+ * Update Volume
+ *
+ * @param {Float} percent Value between 0 and 1 to set audio volume
+ */
+ setPlayerElementVolume : function( percent ) {
+ if ( this.getPlayerElement() ) {
+ // Disable mute if positive volume
+ if( percent != 0 ) {
+ this.playerElement.muted = false;
+ }
+ this.playerElement.volume = percent;
+ }
+ },
+
+ /**
+ * get Volume
+ *
+ * @return {Float}
+ * Audio volume between 0 and 1.
+ */
+ getPlayerElementVolume: function() {
+ if ( this.getPlayerElement() ) {
+ return this.playerElement.volume;
+ }
+ },
+ /**
+ * get the native muted state
+ */
+ getPlayerElementMuted: function(){
+ if ( this.getPlayerElement() ) {
+ return this.playerElement.muted;
+ }
+ },
+
+ /**
+ * Get the native media duration
+ */
+ getNativeDuration: function() {
+ if ( this.playerElement ) {
+ return this.playerElement.duration;
+ }
+ },
+
+ /**
+ * Load the video stream with a callback fired once the video is "loaded"
+ *
+ * @parma {Function} callbcak Function called once video is loaded
+ */
+ load: function( callback ) {
+ this.getPlayerElement();
+ if ( !this.playerElement ) {
+ // No vid loaded
+ mw.log( 'EmbedPlayerNative::load() ... doEmbed' );
+ this.onlyLoadFlag = true;
+ this.embedPlayerHTML();
+ this.onLoadedCallback = callback;
+ } else {
+ // Should not happen offten
+ this.playerElement.load();
+ if( callback ){
+ callback();
+ }
+ }
+ },
+
+ /**
+ * Get /update the playerElement value
+ */
+ getPlayerElement: function () {
+ this.playerElement = $( '#' + this.pid ).get( 0 );
+ return this.playerElement;
+ },
+
+ /**
+ * Bindings for the Video Element Events
+ */
+
+ /**
+ * Local method for seeking event
+ * fired when "seeking"
+ */
+ _onseeking: function() {
+ mw.log( "EmbedPlayerNative::onSeeking " + this.seeking + ' new time: ' + this.getPlayerElement().currentTime );
+ if( this.seeking && Math.round( this.getPlayerElement().currentTime - this.currentSeekTargetTime ) > 2 ){
+ mw.log( "Error:: EmbedPlayerNative Seek time missmatch: target:" + this.getPlayerElement().currentTime +
+ ' actual ' + this.currentSeekTargetTime + ', note apple HLS can only seek to 10 second targets');
+ }
+ // Trigger the html5 seeking event
+ //( if not already set from interface )
+ if( !this.seeking ) {
+ this.currentSeekTargetTime = this.getPlayerElement().currentTime;
+ this.seeking = true;
+ // Run the onSeeking interface update
+ this.controlBuilder.onSeek();
+
+ // Trigger the html5 "seeking" trigger
+ mw.log("EmbedPlayerNative::seeking:trigger:: " + this.seeking);
+ if( this._propagateEvents ){
+ this.triggerHelper( 'seeking' );
+ }
+ }
+ },
+
+ /**
+ * Local method for seeked event
+ * fired when done seeking
+ */
+ _onseeked: function() {
+ mw.log("EmbedPlayerNative::onSeeked " + this.seeking + ' ct:' + this.playerElement.currentTime );
+ // sync the seek checks so that we don't re-issue the seek request
+ this.previousTime = this.currentTime = this.playerElement.currentTime;
+
+ // Trigger the html5 action on the parent
+ if( this.seeking ){
+
+ // HLS safari triggers onseek when its not even close to the target time,
+ // we don't want to trigger the seek event for these "fake" onseeked triggers
+ if( Math.abs( this.currentSeekTargetTime - this.getPlayerElement().currentTime ) > 2 ){
+ mw.log( "Error:: EmbedPlayerNative:seeked triggred with time mismatch: target:" +
+ this.currentSeekTargetTime +
+ ' actual:' + this.getPlayerElement().currentTime );
+ return ;
+ }
+ this.seeking = false;
+ if( this._propagateEvents ){
+ mw.log( "EmbedPlayerNative:: trigger: seeked" );
+ this.triggerHelper( 'seeked' );
+ }
+ }
+ this.hideSpinner();
+ // update the playhead status
+ if( this.isStopped() ){
+ this.addLargePlayBtn();
+ }
+ this.monitor();
+ },
+
+ /**
+ * Handle the native paused event
+ */
+ _onpause: function(){
+ var _this = this;
+ var timeSincePlay = Math.abs( this.absoluteStartPlayTime - new Date().getTime() );
+ mw.log( "EmbedPlayerNative:: OnPaused:: propagate:" + this._propagateEvents + ' time since play: ' + timeSincePlay + ' isNative=true' );
+ // Only trigger parent pause if more than MonitorRate time has gone by.
+ // Some browsers trigger native pause events when they "play" or after a src switch
+ if( timeSincePlay > mw.config.get( 'EmbedPlayer.MonitorRate' ) ){
+ _this.parent_pause();
+ } else {
+ // continue playback:
+ this.getPlayerElement().play();
+ }
+ },
+
+ /**
+ * Handle the native play event
+ */
+ _onplay: function(){
+ mw.log("EmbedPlayerNative:: OnPlay:: propogate:" + this._propagateEvents + ' paused: ' + this.paused);
+ // if using native controls make sure the inteface does not block the native controls interface:
+ if( this.useNativePlayerControls() ){
+ this.$interface.css('pointer-events', 'none');
+ }
+
+ // Update the interface ( if paused )
+ if( ! this.isFirstEmbedPlay && this._propagateEvents && this.paused ){
+ this.parent_play();
+ } else {
+ // make sure the interface reflects the current play state if not calling parent_play()
+ this.playInterfaceUpdate();
+ }
+ // Set firstEmbedPlay state to false to avoid initial play invocation :
+ this.isFirstEmbedPlay = false;
+ },
+
+ /**
+ * Local method for metadata ready
+ * fired when metadata becomes available
+ *
+ * Used to update the media duration to
+ * accurately reflect the src duration
+ */
+ _onloadedmetadata: function() {
+ this.getPlayerElement();
+
+ if ( this.playerElement && !isNaN( this.playerElement.duration ) && isFinite( this.playerElement.duration) ) {
+ mw.log( 'EmbedPlayerNative :onloadedmetadata metadata ready Update duration:' + this.playerElement.duration + ' old dur: ' + this.getDuration() );
+ // Only update duration if we didn't get it server side
+ // Some browsers report bad duration (e.g. Android native browser)
+ // So avoid using the browser detected value if possible.
+ if( !this.duration && this.playerElement && isFinite( this.playerElement.duration ) ) {
+ this.duration = this.playerElement.duration;
+ }
+ }
+
+ // Check if in "playing" state and we are _propagateEvents events and continue to playback:
+ if( !this.paused && this._propagateEvents ){
+ this.getPlayerElement().play();
+ }
+
+ //Fire "onLoaded" flags if set
+ if( typeof this.onLoadedCallback == 'function' ) {
+ this.onLoadedCallback();
+ }
+
+ // Trigger "media loaded"
+ if( ! this.mediaLoadedFlag ){
+ $( this ).trigger( 'mediaLoaded' );
+ this.mediaLoadedFlag = true;
+ }
+ },
+
+ /**
+ * Local method for end of media event
+ */
+ _onended: function( event ) {
+ var _this = this;
+ if( this.getPlayerElement() ){
+ mw.log( 'EmbedPlayer:native: onended:' + this.playerElement.currentTime + ' real dur:' + this.getDuration() + ' ended ' + this._propagateEvents );
+ if( this._propagateEvents ){
+ this.onClipDone();
+ }
+ }
+ },
+ /**
+ * Local onClip done function for native player.
+ */
+ onClipDone: function(){
+ var _this = this;
+ // add clip done binding ( will only run on sequence complete )
+ $(this).unbind('onEndedDone.onClipDone').bind( 'onEndedDone.onClipDone', function(){
+ _this.addPlayScreenWithNativeOffScreen();
+ // if not a legitmate play screen don't keep the player offscreen when playback starts:
+ if( !_this.isImagePlayScreen() ){
+ _this.keepPlayerOffScreenFlag =false;
+ }
+ });
+ this.parent_onClipDone();
+ }
+};
+
+} )( mediaWiki, jQuery );