diff options
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js')
-rw-r--r-- | extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js | 2760 |
1 files changed, 2760 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js new file mode 100644 index 00000000..131302a2 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js @@ -0,0 +1,2760 @@ +/** +* embedPlayer is the base class for html5 video tag javascript abstraction library +* embedPlayer include a few subclasses: +* +* mediaPlayer Media player embed system ie: java, vlc or native. +* mediaElement Represents source media elements +* mw.PlayerControlBuilder Handles skinning of the player controls +*/ +( function( mw, $ ) {"use strict"; + /** + * Merge in the default video attributes supported by embedPlayer: + */ + mw.mergeConfig('EmbedPlayer.Attributes', { + /* + * Base html element attributes: + */ + + // id: Auto-populated if unset + "id" : null, + + // Width: alternate to "style" to set player width + "width" : null, + + // Height: alternative to "style" to set player height + "height" : null, + + /* + * Base html5 video element attributes / states also see: + * http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html + */ + + // Media src URI, can be relative or absolute URI + "src" : null, + + // Poster attribute for displaying a place holder image before loading + // or playing the video + "poster" : null, + + // Autoplay if the media should start playing + "autoplay" : false, + + // Loop attribute if the media should repeat on complete + "loop" : false, + + // If the player controls should be displayed + "controls" : true, + + // Video starts "paused" + "paused" : true, + + // ReadyState an attribute informs clients of video loading state: + // see: http://www.whatwg.org/specs/web-apps/current-work/#readystate + "readyState" : 0, + + // Loading state of the video element + "networkState" : 0, + + // Current playback position + "currentTime" : 0, + + // Previous player set time + // Lets javascript use $('#videoId')[0].currentTime = newTime; + "previousTime" : 0, + + // Previous player set volume + // Lets javascript use $('#videoId')[0].volume = newVolume; + "previousVolume" : 1, + + // Initial player volume: + "volume" : 0.75, + + // Caches the volume before a mute toggle + "preMuteVolume" : 0.75, + + // Media duration: Value is populated via + // custom data-durationhint attribute or via the media file once its played + "duration" : null, + + // A hint to the duration of the media file so that duration + // can be displayed in the player without loading the media file + 'data-durationhint': null, + + // to disable menu or timedText for a given embed + 'data-disablecontrols': null, + + // Also support direct durationHint attribute ( backwards compatibly ) + // @deprecated please use data-durationhint instead. + 'durationHint' : null, + + // Mute state + "muted" : false, + + /** + * Custom attributes for embedPlayer player: (not part of the html5 + * video spec) + */ + + // Default video aspect ratio + 'videoAspect' : '4:3', + + // Start time of the clip + "start" : 0, + + // End time of the clip + "end" : null, + + // If the player controls should be overlaid + // ( Global default via config EmbedPlayer.OverlayControls in module + // loader.js) + "overlaycontrols" : true, + + // Attribute to use 'native' controls + "usenativecontrols" : false, + + // If the player should include an attribution button: + 'attributionbutton' : true, + + // A player error object (Includes title and message) + // * Used to display an error instead of a play button + // * The full player api available + 'playerError' : {}, + + // A flag to hide the player gui and disable autoplay + // * Used for empty players or a player where you want to dynamically set sources, then play. + // * The player API remains active. + 'data-blockPlayerDisplay': null, + + // If serving an ogg_chop segment use this to offset the presentation time + // ( for some plugins that use ogg page time rather than presentation time ) + "startOffset" : 0, + + // If the download link should be shown + "downloadLink" : true, + + // Content type of the media + "type" : null + + } ); + + + /** + * The base source attribute checks also see: + * http://dev.w3.org/html5/spec/Overview.html#the-source-element + */ + mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [ + // source id + 'id', + + // media url + 'src', + + // Title string for the source asset + 'title', + + // The html5 spec uses label instead of 'title' for naming sources + 'label', + + // boolean if we support temporal url requests on the source media + 'URLTimeEncoding', + + // Media has a startOffset ( used for plugins that + // display ogg page time rather than presentation time + 'startOffset', + + // Media start time + 'start', + + // Media end time + 'end', + + // If the source is the default source + 'default', + + // Title of the source + 'title', + + // titleKey ( used for api lookups TODO move into mediaWiki specific support + 'titleKey' + ] ); + + + /** + * Base embedPlayer object + * + * @param {Element} + * element, the element used for initialization. + * @constructor + */ + mw.EmbedPlayer = function( element ) { + return this.init( element ); + }; + + mw.EmbedPlayer.prototype = { + + // The mediaElement object containing all mediaSource objects + 'mediaElement' : null, + + // Object that describes the supported feature set of the underling plugin / + // Support list is described in PlayerControlBuilder components + 'supports': { }, + + // If the player is done loading ( does not guarantee playability ) + // for example if there is an error playerReadyFlag is still set to true once + // no more loading is to be done + 'playerReadyFlag' : false, + + // Stores the loading errors + 'loadError' : false, + + // Thumbnail updating flag ( to avoid rewriting an thumbnail thats already + // being updated) + 'thumbnailUpdatingFlag' : false, + + // Stopped state flag + 'stopped' : true, + + // Local variable to hold CMML meeta data about the current clip + // for more on CMML see: http://wiki.xiph.org/CMML + 'cmmlData': null, + + // Stores the seek time request, Updated by the seek function + 'serverSeekTime' : 0, + + // If the embedPlayer is current 'seeking' + 'seeking' : false, + + // Percent of the clip buffered: + 'bufferedPercent' : 0, + + // Holds the timer interval function + 'monitorTimerId' : null, + + // Buffer flags + 'bufferStartFlag' : false, + 'bufferEndFlag' : false, + + // For supporting media fragments stores the play end time + 'pauseTime' : null, + + // On done playing + 'donePlayingCount' : 0 + , + // if player events should be Propagated + '_propagateEvents': true, + + // If the onDone interface should be displayed + 'onDoneInterfaceFlag': true, + + // if we should check for a loading spinner in the monitor function: + '_checkHideSpinner' : false, + + // If pause play controls click controls should be active: + '_playContorls' : true, + + // If player should be displayed (in some caused like audio, we don't need the player to be visible + 'displayPlayer': true, + + // Widget loaded should only fire once + 'widgetLoaded': false, + + /** + * embedPlayer + * + * @constructor + * + * @param {Element} + * element DOM element that we are building the player interface for. + */ + init: function( element ) { + var _this = this; + mw.log('EmbedPlayer: initEmbedPlayer: ' + $(element).width() ); + + var playerAttributes = mw.config.get( 'EmbedPlayer.Attributes' ); + + // Store the rewrite element tag type + this.rewriteElementTagName = element.tagName.toLowerCase(); + + this.noPlayerFallbackHTML = $( element ).html(); + + // Setup the player Interface from supported attributes: + for ( var attr in playerAttributes ) { + // We can't use $(element).attr( attr ) because we have to check for boolean attributes: + if ( element.getAttribute( attr ) != null ) { + // boolean attributes + if( element.getAttribute( attr ) == '' ){ + this[ attr ] = true; + } else { + this[ attr ] = element.getAttribute( attr ); + } + } else { + this[attr] = playerAttributes[attr]; + } + // string -> boolean + if( this[ attr ] == "false" ) this[attr] = false; + if( this[ attr ] == "true" ) this[attr] = true; + } + + // Hide "controls" if using native player controls: + if( this.useNativePlayerControls() ){ + _this.controls = true; + } + // Set the skin name from the class + var sn = $(element).attr( 'class' ); + + if ( sn && sn != '' ) { + var skinList = mw.config.get('EmbedPlayer.SkinList'); + for ( var n = 0; n < skinList.length; n++ ) { + if ( sn.indexOf( skinList[n].toLowerCase() ) !== -1 ) { + this.skinName = skinList[ n ]; + } + } + } + // Set the default skin if unset: + if ( !this.skinName ) { + this.skinName = mw.config.get( 'EmbedPlayer.DefaultSkin' ); + } + + // Support custom monitorRate Attribute ( if not use default ) + if( !this.monitorRate ){ + this.monitorRate = mw.config.get( 'EmbedPlayer.MonitorRate' ); + } + + // Make sure startOffset is cast as an float: + if ( this.startOffset && this.startOffset.split( ':' ).length >= 2 ) { + this.startOffset = parseFloat( mw.npt2seconds( this.startOffset ) ); + } + + // Make sure offset is in float: + this.startOffset = parseFloat( this.startOffset ); + + // Set the source duration + if ( $( element ).attr( 'duration' ) ) { + _this.duration = $( element ).attr( 'duration' ); + } + // Add durationHint property form data-durationhint: + if( _this['data-durationhint']){ + _this.durationHint = _this['data-durationhint']; + } + // Update duration from provided durationHint + if ( _this.durationHint && ! _this.duration){ + _this.duration = mw.npt2seconds( _this.durationHint ); + } + + // Make sure duration is a float: + this.duration = parseFloat( this.duration ); + mw.log( 'EmbedPlayer::init:' + this.id + " duration is: " + this.duration ); + + // Add disablecontrols property form data-disablecontrols: + if( _this['data-disablecontrols'] ){ + _this.disablecontrols = _this['data-disablecontrols']; + } + + // Set the playerElementId id + this.pid = 'pid_' + this.id; + + // Add the mediaElement object with the elements sources: + this.mediaElement = new mw.MediaElement( element ); + + this.bindHelper( 'updateLayout', function() { + _this.updateLayout(); + }); + }, + /** + * Bind helpers to help iOS retain bind context + * + * Yes, iOS will fail when you run $( embedPlayer ).bind() + * but "work" when you run embedPlayer.bind() if the script urls are from diffrent "resources" + */ + bindHelper: function( name, callback ){ + $( this ).bind( name, callback ); + return this; + }, + unbindHelper: function( bindName ){ + if( bindName ) { + $( this ).unbind( bindName ); + } + return this; + }, + triggerQueueCallback: function( name, callback ){ + $( this ).triggerQueueCallback( name, callback ); + }, + triggerHelper: function( name, obj ){ + try{ + $( this ).trigger( name, obj ); + } catch( e ){ + // ignore try catch calls + // mw.log( "EmbedPlayer:: possible error in trgger: " + name + " " + e.toString() ); + } + }, + /** + * Stop events from Propagation and blocks interface updates and trigger events. + * @return + */ + stopEventPropagation: function(){ + mw.log("EmbedPlayer:: stopEventPropagation"); + this.stopMonitor(); + this._propagateEvents = false; + }, + + /** + * Restores event propagation + * @return + */ + restoreEventPropagation: function(){ + mw.log("EmbedPlayer:: restoreEventPropagation"); + this._propagateEvents = true; + this.startMonitor(); + }, + + /** + * Enables the play controls ( for example when an ad is done ) + */ + enablePlayControls: function(){ + mw.log("EmbedPlayer:: enablePlayControls" ); + if( this.useNativePlayerControls() ){ + return ; + } + this._playContorls = true; + // re-enable hover: + this.getInterface().find( '.play-btn' ) + .buttonHover() + .css('cursor', 'pointer' ); + + this.controlBuilder.enableSeekBar(); + /* + * We should pass an array with enabled components, and the controlBuilder will listen + * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer. + * [ 'playButton', 'seekBar' ] + */ + $( this ).trigger( 'onEnableInterfaceComponents'); + }, + + /** + * Disables play controls, for example when an ad is playing back + */ + disablePlayControls: function(){ + if( this.useNativePlayerControls() ){ + return ; + } + this._playContorls = false; + // turn off hover: + this.getInterface().find( '.play-btn' ) + .unbind('mouseenter mouseleave') + .css('cursor', 'default' ); + + this.controlBuilder.disableSeekBar(); + /** + * We should pass an array with disabled components, and the controlBuilder will listen + * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer. + * [ 'playButton', 'seekBar' ] + */ + $( this ).trigger( 'onDisableInterfaceComponents'); + }, + + /** + * For plugin-players to update supported features + */ + updateFeatureSupport: function(){ + $( this ).trigger('updateFeatureSupportEvent', this.supports ); + return ; + }, + /** + * Apply Intrinsic Aspect ratio of a given image to a poster image layout + */ + applyIntrinsicAspect: function(){ + var $this = $( this ); + // Check if a image thumbnail is present: + if( this.getInterface().find('.playerPoster').length ){ + var img = this.getInterface().find('.playerPoster')[0]; + var pHeight = $this.height(); + // Check for intrinsic width and maintain aspect ratio + if( img.naturalWidth && img.naturalHeight ){ + var pWidth = parseInt( img.naturalWidth / img.naturalHeight * pHeight); + if( pWidth > $this.width() ){ + pWidth = $this.width(); + pHeight = parseInt( img.naturalHeight / img.naturalWidth * pWidth ); + } + $( img ).css({ + 'height' : pHeight + 'px', + 'width': pWidth + 'px', + 'left': ( ( $this.width() - pWidth ) * .5 ) + 'px', + 'top': ( ( $this.height() - pHeight ) * .5 ) + 'px', + 'position' : 'absolute' + }); + } + } + }, + /** + * Set the width & height from css style attribute, element attribute, or by + * default value if no css or attribute is provided set a callback to + * resize. + * + * Updates this.width & this.height + * + * @param {Element} + * element Source element to grab size from + */ + loadPlayerSize: function( element ) { + // check for direct element attribute: + this.height = element.height > 0 ? element.height + '' : $(element).css( 'height' ); + this.width = element.width > 0 ? element.width + '' : $(element).css( 'width' ); + + // Special check for chrome 100% with re-mapping to 32px + // Video embed at 32x32 will have to wait for intrinsic video size later on + if( this.height == '32px' || this.height =='32px' ){ + this.width = '100%'; + this.height = '100%'; + } + mw.log('EmbedPlayer::loadPlayerSize: css size:' + this.width + ' h: ' + this.height); + + // Set to parent size ( resize events will cause player size updates) + if( this.height.indexOf('100%') != -1 || this.width.indexOf('100%') != -1 ){ + var $relativeParent = $(element).parents().filter(function() { + // reduce to only relative position or "body" elements + return $( this ).is('body') || $( this ).css('position') == 'relative'; + }).slice(0,1); // grab only the "first" + this.width = $relativeParent.width(); + this.height = $relativeParent.height(); + } + // Make sure height and width are a number + this.height = parseInt( this.height ); + this.width = parseInt( this.width ); + + // Set via attribute if CSS is zero or NaN and we have an attribute value: + this.height = ( this.height==0 || isNaN( this.height ) + && $(element).attr( 'height' ) ) ? + parseInt( $(element).attr( 'height' ) ): this.height; + this.width = ( this.width == 0 || isNaN( this.width ) + && $(element).attr( 'width' ) )? + parseInt( $(element).attr( 'width' ) ): this.width; + + + // Special case for audio + + // Firefox sets audio height to "0px" while webkit uses 32px .. force zero: + if( this.isAudio() && this.height == '32' ) { + this.height = 20; + } + + // Use default aspect ration to get height or width ( if rewriting a non-audio player ) + if( this.isAudio() && this.videoAspect ) { + var aspect = this.videoAspect.split( ':' ); + if( this.height && !this.width ) { + this.width = parseInt( this.height * ( aspect[0] / aspect[1] ) ); + } + if( this.width && !this.height ) { + var apectRatio = ( aspect[1] / aspect[0] ); + this.height = parseInt( this.width * ( aspect[1] / aspect[0] ) ); + } + } + + // On load sometimes attr is temporally -1 as we don't have video metadata yet. + // or in IE we get NaN for width height + // + // NOTE: browsers that do support height width should set "waitForMeta" flag in addElement + if( ( isNaN( this.height )|| isNaN( this.width ) ) || + ( this.height == -1 || this.width == -1 ) || + // Check for firefox defaults + // Note: ideally firefox would not do random guesses at css + // values + ( (this.height == 150 || this.height == 64 ) && this.width == 300 ) + ) { + var defaultSize = mw.config.get( 'EmbedPlayer.DefaultSize' ).split( 'x' ); + if( isNaN( this.width ) ){ + this.width = defaultSize[0]; + } + + // Special height default for audio tag ( if not set ) + if( this.isAudio() ) { + this.height = 20; + }else{ + this.height = defaultSize[1]; + } + } + }, + + /** + * Get the player pixel width not including controls + * + * @return {Number} pixel height of the video + */ + getPlayerWidth: function() { + var profile = $.client.profile(); + + if ( profile.name === 'firefox' && profile.versionNumber < 2 ) { + return ( $( this ).parent().parent().width() ); + } + return $( this ).width(); + }, + + /** + * Get the player pixel height not including controls + * + * @return {Number} pixel height of the video + */ + getPlayerHeight: function() { + return $( this ).height(); + }, + + /** + * Check player for sources. If we need to get media sources form an + * external file that request is issued here + */ + checkPlayerSources: function() { + mw.log( 'EmbedPlayer::checkPlayerSources: ' + this.id ); + var _this = this; + // Allow plugins to listen to a preCheckPlayerSources ( for registering the source loading point ) + $( _this ).trigger( 'preCheckPlayerSources' ); + + // Allow plugins to block on sources lookup ( cases where we just have an api key for example ) + $( _this ).triggerQueueCallback( 'checkPlayerSourcesEvent', function(){ + _this.setupSourcePlayer(); + }); + }, + + /** + * Get text tracks from the mediaElement + */ + getTextTracks: function(){ + if( !this.mediaElement ){ + return []; + } + return this.mediaElement.getTextTracks(); + }, + /** + * Empty the player sources + */ + emptySources: function(){ + if( this.mediaElement ){ + this.mediaElement.sources = []; + this.mediaElement.selectedSource = null; + } + // setup pointer to old source: + this.prevPlayer = this.selectedPlayer; + // don't null out the selected player on empty sources + //this.selectedPlayer =null; + }, + + /** + * Switch and play a video source + * + * Checks if the target source is the same playback mode and does player switch if needed. + * and calls playerSwitchSource + */ + switchPlaySource: function( source, switchCallback, doneCallback ){ + var _this = this; + var targetPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( source.mimeType ) ; + if( targetPlayer.library != this.selectedPlayer.library ){ + this.selectedPlayer = targetPlayer; + this.updatePlaybackInterface( function(){ + _this.playerSwitchSource( source, switchCallback, doneCallback ); + }); + } else { + // Call the player switch directly: + _this.playerSwitchSource( source, switchCallback, doneCallback ); + } + }, + /** + * abstract function player interface must support actual source switch + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + mw.log( "Error player interface must support actual source switch"); + }, + + /** + * Set up the select source player + * + * issues autoSelectSource call + * + * Sets load error if no source is playable + */ + setupSourcePlayer: function() { + var _this = this; + mw.log("EmbedPlayer::setupSourcePlayer: " + this.id + ' sources: ' + this.mediaElement.sources.length ); + + // Check for source replace configuration: + if( mw.config.get('EmbedPlayer.ReplaceSources' ) ){ + this.emptySources(); + $.each( mw.config.get('EmbedPlayer.ReplaceSources' ), function( inx, source ){ + _this.mediaElement.tryAddSource( source ); + }); + } + + // Autoseletct the media source + this.mediaElement.autoSelectSource(); + + // Auto select player based on default order + if( this.mediaElement.selectedSource ){ + this.selectedPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( this.mediaElement.selectedSource.mimeType ); + // Check if we need to switch player rendering libraries: + if ( this.selectedPlayer && ( !this.prevPlayer || this.prevPlayer.library != this.selectedPlayer.library ) ) { + // Inherit the playback system of the selected player: + this.updatePlaybackInterface(); + return ; + } + } + + // Check if no player is selected + if( !this.selectedPlayer || !this.mediaElement.selectedSource ){ + this.showPlayerError(); + mw.log( "EmbedPlayer:: setupSourcePlayer > player ready ( but with errors ) "); + } else { + // Trigger layout ready event + $( this ).trigger( 'layoutReady' ); + // Show the interface: + this.getInterface().find( '.control-bar').show(); + this.addLargePlayBtn(); + } + // We still do the playerReady sequence on errors to provide an api + // and player error events + this.playerReadyFlag = true; + // trigger the player ready event; + $( this ).trigger( 'playerReady' ); + this.triggerWidgetLoaded(); + }, + + /** + * Updates the player interface + * + * Loads and inherit methods from the selected player interface. + * + * @param {Function} + * callback Function to be called once playback-system has been + * inherited + */ + updatePlaybackInterface: function( callback ) { + var _this = this; + mw.log( "EmbedPlayer::updatePlaybackInterface: duration is: " + this.getDuration() + ' playerId: ' + this.id ); + // Clear out any non-base embedObj methods: + if ( this.instanceOf ) { + // Update the prev instance var used for swiching interfaces to know the previous instance. + $( this ).data( 'previousInstanceOf', this.instanceOf ); + var tmpObj = window['mw.EmbedPlayer' + this.instanceOf ]; + for ( var i in tmpObj ) { + // Restore parent into local location + if ( typeof this[ 'parent_' + i ] != 'undefined' ) { + this[i] = this[ 'parent_' + i]; + } else { + this[i] = null; + } + } + } + // Set up the new embedObj + mw.log( 'EmbedPlayer::updatePlaybackInterface: embedding with ' + this.selectedPlayer.library ); + this.selectedPlayer.load( function() { + _this.updateLoadedPlayerInterface( callback ); + }); + }, + /** + * Update a loaded player interface by setting local methods to the + * updated player prototype methods + * + * @parma {function} + * callback function called once player has been loaded + */ + updateLoadedPlayerInterface: function( callback ){ + var _this = this; + mw.log( 'EmbedPlayer::updateLoadedPlayerInterface ' + _this.selectedPlayer.library + " player loaded for " + _this.id ); + + // Get embed library player Interface + var playerInterface = mw[ 'EmbedPlayer' + _this.selectedPlayer.library ]; + + // Build the player interface ( if the interface includes an init ) + if( playerInterface.init ){ + playerInterface.init(); + } + + for ( var method in playerInterface ) { + if ( typeof _this[method] != 'undefined' && !_this['parent_' + method] ) { + _this['parent_' + method] = _this[method]; + } + _this[ method ] = playerInterface[ method ]; + } + // Update feature support + _this.updateFeatureSupport(); + // Update duration + _this.getDuration(); + // show player inline + _this.showPlayer(); + // Run the callback if provided + if ( callback && $.isFunction( callback ) ){ + callback(); + } + }, + + /** + * Select a player playback system + * + * @param {Object} + * player Player playback system to be selected player playback + * system include vlc, native, java etc. + */ + selectPlayer: function( player ) { + mw.log("EmbedPlayer:: selectPlayer " + player.id ); + var _this = this; + if ( this.selectedPlayer.id != player.id ) { + this.selectedPlayer = player; + this.updatePlaybackInterface( function(){ + // Hide / remove track container + _this.getInterface().find( '.track' ).remove(); + // We have to re-bind hoverIntent ( has to happen in this scope ) + if( !_this.useNativePlayerControls() && _this.controls && _this.controlBuilder.isOverlayControls() ){ + _this.controlBuilder.showControlBar(); + _this.getInterface().hoverIntent({ + 'sensitivity': 4, + 'timeout' : 2000, + 'over' : function(){ + _this.controlBuilder.showControlBar(); + }, + 'out' : function(){ + _this.controlBuilder.hideControlBar(); + } + }); + } + }); + } + }, + + /** + * Get a time range from the media start and end time + * + * @return startNpt and endNpt time if present + */ + getTimeRange: function() { + var end_time = ( this.controlBuilder.longTimeDisp )? '/' + mw.seconds2npt( this.getDuration() ) : ''; + var defaultTimeRange = '0:00' + end_time; + if ( !this.mediaElement ){ + return defaultTimeRange; + } + if ( !this.mediaElement.selectedSource ){ + return defaultTimeRange; + } + if ( !this.mediaElement.selectedSource.endNpt ){ + return defaultTimeRange; + } + return this.mediaElement.selectedSource.startNpt + this.mediaElement.selectedSource.endNpt; + }, + + /** + * Get the duration of the embed player + */ + getDuration: function() { + if ( isNaN(this.duration) && this.mediaElement && this.mediaElement.selectedSource && + typeof this.mediaElement.selectedSource.durationHint != 'undefined' ){ + this.duration = this.mediaElement.selectedSource.durationHint; + } + return this.duration; + }, + + /** + * Get the player height + */ + getHeight: function() { + return this.getInterface().height(); + }, + + /** + * Get the player width + */ + getWidth: function(){ + return this.getInterface().width(); + }, + + /** + * Check if the selected source is an audio element: + */ + isAudio: function(){ + return ( this.rewriteElementTagName == 'audio' + || + ( this.mediaElement && this.mediaElement.selectedSource && this.mediaElement.selectedSource.mimeType.indexOf('audio/') !== -1 ) + ); + }, + + /** + * Get the plugin embed html ( should be implemented by embed player interface ) + */ + embedPlayerHTML: function() { + return 'Error: function embedPlayerHTML should be implemented by embed player interface '; + }, + + /** + * Seek function ( should be implemented by embedPlayer interface + * playerNative, playerKplayer etc. ) embedPlayer seek only handles URL + * time seeks + * @param {Float} + * percent of the video total length to seek to + */ + seek: function( percent ) { + var _this = this; + this.seeking = true; + // Trigger preSeek event for plugins that want to store pre seek conditions. + $( this ).trigger( 'preSeek', percent ); + + // Do argument checking: + if( percent < 0 ){ + percent = 0; + } + + if( percent > 1 ){ + percent = 1; + } + // set the playhead to the target position + this.updatePlayHead( percent ); + + // See if we should do a server side seek ( player independent ) + if ( this.supportsURLTimeEncoding() ) { + mw.log( 'EmbedPlayer::seek:: updated serverSeekTime: ' + mw.seconds2npt ( this.serverSeekTime ) + + ' currentTime: ' + _this.currentTime ); + // make sure we need to seek: + if( _this.currentTime == _this.serverSeekTime ){ + return ; + } + + this.stop(); + this.didSeekJump = true; + // Make sure this.serverSeekTime is up-to-date: + this.serverSeekTime = mw.npt2seconds( this.startNpt ) + parseFloat( percent * this.getDuration() ); + } + // Run the onSeeking interface update + // NOTE controlBuilder should really bind to html5 events rather + // than explicitly calling it or inheriting stuff. + this.controlBuilder.onSeek(); + }, + + /** + * Seeks to the requested time and issues a callback when ready (should be + * overwritten by client that supports frame serving) + */ + setCurrentTime: function( time, callback ) { + mw.log( 'Error: EmbedPlayer, setCurrentTime not overriden' ); + if( $.isFunction( callback ) ){ + callback(); + } + }, + + /** + * On clip done action. Called once a clip is done playing + * TODO clean up end sequence flow + */ + triggeredEndDone: false, + postSequence: false, + onClipDone: function() { + var _this = this; + // Don't run onclipdone if _propagateEvents is off + if( !_this._propagateEvents ){ + return ; + } + mw.log( 'EmbedPlayer::onClipDone: propagate:' + _this._propagateEvents + ' id:' + this.id + ' doneCount:' + this.donePlayingCount + ' stop state:' +this.isStopped() ); + // Only run stopped once: + if( !this.isStopped() ){ + // set the "stopped" flag: + this.stopped = true; + + // Show the control bar: + this.controlBuilder.showControlBar(); + + // TOOD we should improve the end event flow + // First end event for ads or current clip ended bindings + if( ! this.onDoneInterfaceFlag ){ + this.stopEventPropagation(); + } + + mw.log("EmbedPlayer:: trigger: ended ( inteface continue pre-check: " + this.onDoneInterfaceFlag + ' )' ); + $( this ).trigger( 'ended' ); + mw.log("EmbedPlayer::onClipDone:Trigged ended, continue? " + this.onDoneInterfaceFlag); + + + if( ! this.onDoneInterfaceFlag ){ + // Restore events if we are not running the interface done actions + this.restoreEventPropagation(); + return ; + } + + // A secondary end event for playlist and clip sequence endings + if( this.onDoneInterfaceFlag ){ + // We trigger two end events to match KDP and ensure playbackComplete always comes before playerPlayEnd + // in content ends. + mw.log("EmbedPlayer:: trigger: playbackComplete"); + $( this ).trigger( 'playbackComplete' ); + // now trigger postEnd for( playerPlayEnd ) + mw.log("EmbedPlayer:: trigger: postEnded"); + $( this ).trigger( 'postEnded' ); + } + // if the ended event did not trigger more timeline actions run the actual stop: + if( this.onDoneInterfaceFlag ){ + mw.log("EmbedPlayer::onDoneInterfaceFlag=true do interface done"); + // Prevent the native "onPlay" event from propagating that happens when we rewind: + this.stopEventPropagation(); + + // Update the clip done playing count ( for keeping track of replays ) + _this.donePlayingCount ++; + + // Rewind the player to the start: + // NOTE: Setting to 0 causes lags on iPad when replaying, thus setting to 0.01 + this.setCurrentTime(0.01, function(){ + + // Set to stopped state: + _this.stop(); + + // Restore events after we rewind the player + _this.restoreEventPropagation(); + + // Check if we have the "loop" property set + if( _this.loop ) { + _this.stopped = false; + _this.play(); + return; + } else { + // make sure we are in a paused state. + _this.pause(); + } + // Check if have a force display of the large play button + if( mw.config.get('EmbedPlayer.ForceLargeReplayButton') === true ){ + _this.addLargePlayBtn(); + } else{ + // Check if we should hide the large play button on end: + if( $( _this ).data( 'hideEndPlayButton' ) || !_this.useLargePlayBtn() ){ + _this.hideLargePlayBtn(); + } else { + _this.addLargePlayBtn(); + } + } + // An event for once the all ended events are done. + mw.log("EmbedPlayer:: trigger: onEndedDone"); + if ( !_this.triggeredEndDone ){ + _this.triggeredEndDone = true; + $( _this ).trigger( 'onEndedDone', [_this.id] ); + } + }) + } + } + }, + + + /** + * Shows the video Thumbnail, updates pause state + */ + showThumbnail: function() { + var _this = this; + mw.log( 'EmbedPlayer::showThumbnail::' + this.stopped ); + + // Close Menu Overlay: + this.controlBuilder.closeMenuOverlay(); + + // update the thumbnail html: + this.updatePosterHTML(); + + this.paused = true; + this.stopped = true; + + // Once the thumbnail is shown run the mediaReady trigger (if not using native controls) + if( !this.useNativePlayerControls() ){ + mw.log("mediaLoaded"); + $( this ).trigger( 'mediaLoaded' ); + } + }, + + /** + * Show the player + */ + showPlayer: function () { + mw.log( 'EmbedPlayer:: showPlayer: ' + this.id + ' interface: w:' + this.width + ' h:' + this.height ); + var _this = this; + + // Remove the player loader spinner if it exists + this.hideSpinnerAndPlayBtn(); + // If a isPersistentNativePlayer ( overlay the controls ) + if( !this.useNativePlayerControls() && this.isPersistentNativePlayer() ){ + $( this ).show(); + } + // Add controls if enabled: + if ( this.controls ) { + if( this.useNativePlayerControls() ){ + if( this.getPlayerElement() ){ + $( this.getPlayerElement() ).attr('controls', "true"); + } + } else { + this.controlBuilder.addControls(); + } + } + + // Update Thumbnail for the "player" + this.updatePosterHTML(); + + // Update temporal url if present + this.updateTemporalUrl(); + + // Do we need to show the player? + if( this.displayPlayer === false ) { + _this.getVideoHolder().hide(); + _this.getInterface().height( _this.getComponentsHeight() ); + _this.triggerHelper('updateLayout'); + } + + // Update layout + this.updateLayout(); + + // Make sure we have a play btn: + this.addLargePlayBtn(); + + // Update the playerReady flag + this.playerReadyFlag = true; + mw.log("EmbedPlayer:: Trigger: playerReady"); + // trigger the player ready event; + $( this ).trigger( 'playerReady' ); + this.triggerWidgetLoaded(); + + // Check if we want to block the player display + if( this['data-blockPlayerDisplay'] ){ + this.blockPlayerDisplay(); + return ; + } + + // Check if there are any errors to be displayed: + if( this.getError() ){ + this.showErrorMsg( this.getError() ); + return ; + } + // Auto play stopped ( no playerReady has already started playback ) and if not on an iPad with iOS > 3 + if ( this.isStopped() && this.autoplay && (!mw.isIOS() || mw.isIpad3() ) ) { + mw.log( 'EmbedPlayer::showPlayer::Do autoPlay' ); + _this.play(); + } + }, + + getComponentsHeight: function() { + var height = 0; + + // Go over all playerContainer direct children with .block class + this.getInterface().find('.block').each(function() { + height += $( this ).outerHeight( true ); + }); + + // FIXME embedPlayer should know nothing about playlist layout + /* If we're in vertical playlist mode, and not in fullscreen add playlist height + if( $('#container').hasClass('vertical') && ! this.controlBuilder.isInFullScreen() && this.displayPlayer ) { + height += $('#playlistContainer').outerHeight( true ); + } + */ + + // + var offset = (mw.isIOS()) ? 5 : 0; + + return height + offset; + }, + updateLayout: function() { + // update image layout: + this.applyIntrinsicAspect(); + if( !mw.config.get('EmbedPlayer.IsIframeServer' ) ){ + // Use intrensic container size + return ; + } + // Set window height if in iframe: + var windowHeight; + if( mw.isIOS() && ! this.controlBuilder.isInFullScreen() ) { + windowHeight = $( window.parent.document.getElementById( this.id ) ).height(); + } else { + windowHeight = window.innerHeight; + } + + var newHeight = windowHeight - this.getComponentsHeight(); + var currentHeight = this.getVideoHolder().height(); + // Always update videoHolder height + if( currentHeight !== newHeight ) { + mw.log('EmbedPlayer: updateLayout:: window: ' + windowHeight + ', components: ' + this.getComponentsHeight() + ', videoHolder old height: ' + currentHeight + ', new height: ' + newHeight ); + this.getVideoHolder().height( newHeight ); + } + }, + /** + * Gets a refrence to the main player interface, builds if not avaliable + */ + getInterface: function(){ + if( !this.$interface ){ + // init the control builder + this.controlBuilder = new mw.PlayerControlBuilder( this ); + + // build the interface wrapper + this.$interface = $( this ).wrap( + $('<div />') + .addClass( 'mwPlayerContainer ' + this.controlBuilder.playerClass ) + .append( + $('<div />').addClass( 'videoHolder' ) + ) + ).parent().parent(); + + // pass along any inhereted style: + if( this.style.cssText ){ + this.$interface[0].style.cssText = this.style.cssText; + } + // clear out base style + this.style.cssText = ''; + + // if not displayiung a play button, ( pass through to native player ) + if( ! this.useLargePlayBtn() ){ + this.$interface.css('pointer-events', 'none'); + } + } + return this.$interface; + }, + + /** + * Media fragments handler based on: + * http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#fragment-dimensions + * + * We support seconds and npt ( normal play time ) + * + * Updates the player per fragment url info if present + * + */ + updateTemporalUrl: function(){ + var sourceHash = /[^\#]+$/.exec( this.getSrc() ).toString(); + if( sourceHash.indexOf('t=') === 0 ){ + // parse the times + var times = sourceHash.substr(2).split(','); + if( times[0] ){ + // update the current time + this.currentTime = mw.npt2seconds( times[0].toString() ); + } + if( times[1] ){ + this.pauseTime = mw.npt2seconds( times[1].toString() ); + // ignore invalid ranges: + if( this.pauseTime < this.currentTime ){ + this.pauseTime = null; + } + } + // Update the play head + this.updatePlayHead( this.currentTime / this.duration ); + // Update status: + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) ); + } + }, + /** + * Sets an error message on the player + * + * @param {string} + * errorMsg + */ + setError: function( errorObj ){ + var _this = this; + if ( typeof errorObj == 'string' ) { + this.playerError = { + 'title' : _this.getKalturaMsg( 'ks-GENERIC_ERROR_TITLE' ), + 'message' : errorObj + } + return ; + + } + this.playerError = errorObj; + }, + /** + * Gets the current player error + */ + getError: function() { + if ( !$.isEmptyObject( this.playerError ) ) { + return this.playerError; + } + return null; + }, + + /** + * Show an error message on the player + * + * @param {object} + * errorObj + */ + showErrorMsg: function( errorObj ){ + // Remove a loading spinner + this.hideSpinnerAndPlayBtn(); + if( this.controlBuilder ) { + if( mw.config.get("EmbedPlayer.ShowPlayerAlerts") ) { + var alertObj = $.extend( errorObj, { + 'isModal': true, + 'keepOverlay': true, + 'noButtons': true, + 'isError': true + } ); + this.controlBuilder.displayAlert( alertObj ); + } + } + return ; + }, + + /** + * Blocks the player display by invoking an empty error msg + */ + blockPlayerDisplay: function(){ + this.showErrorMsg(); + this.getInterface().find( '.error' ).hide(); + }, + + /** + * Get missing plugin html (check for user included code) + * + * @param {String} + * [misssingType] missing type mime + */ + showPlayerError: function( ) { + var _this = this; + var $this = $( this ); + mw.log("EmbedPlayer::showPlayerError"); + // Hide loader + this.hideSpinnerAndPlayBtn(); + + // Error in loading media ( trigger the mediaLoadError ) + $this.trigger( 'mediaLoadError' ); + + // We don't distiguish between mediaError and mediaLoadError right now + // TODO fire mediaError only on failed to recive audio/video data. + $this.trigger( 'mediaError' ); + + // Check if we want to block the player display ( no error displayed ) + if( this['data-blockPlayerDisplay'] ){ + this.blockPlayerDisplay(); + return ; + } + + // Check if there is a more specific error: + if( this.getError() ){ + this.showErrorMsg( this.getError() ); + return ; + } + + // If no error is given assume missing sources: + this.showNoInlinePlabackSupport(); + }, + + /** + * Show player missing sources method + */ + showNoInlinePlabackSupport: function(){ + var _this = this; + var $this = $( this); + + // Check if any sources are avaliable: + if( this.mediaElement.sources.length == 0 + || + !mw.config.get('EmbedPlayer.NotPlayableDownloadLink') ) + { + return ; + } + // Set the isLink player flag: + this.isLinkPlayer= true; + // Update the poster and html: + this.updatePosterHTML(); + + // Make sure we have a play btn: + this.addLargePlayBtn(); + + // By default set the direct download url to the first source. + var downloadUrl = this.mediaElement.sources[0].getSrc(); + // Allow plugins to update the download url ( to point to server side tools to select + // stream based on user agent ( i.e IE8 h.264 file, blackberry 3gp file etc ) + this.triggerHelper( 'directDownloadLink', function( dlUrl ){ + if( dlUrl ){ + downloadUrl = dlUrl; + } + }); + // Set the play button to the first available source: + var $pBtn = this.getInterface().find('.play-btn-large') + .attr( 'title', mw.msg('mwe-embedplayer-play_clip') ) + .show() + .unbind( 'click' ) + .click( function() { + _this.triggerHelper( 'firstPlay', [ _this.id ] ); // To send stats event for play + _this.triggerHelper( 'playing' ); + return true; + }); + if( !$pBtn.parent('a').length ){ + $pBtn.wrap( $( '<a />' ).attr("target", "_blank" ) ); + } + $pBtn.parent('a').attr( "href", downloadUrl ); + + $( this ).trigger( 'showInlineDownloadLink' ); + }, + /** + * Update the video time request via a time request string + * + * @param {String} + * timeRequest video time to be updated + */ + updateVideoTimeReq: function( timeRequest ) { + mw.log( 'EmbedPlayer::updateVideoTimeReq:' + timeRequest ); + var timeParts = timeRequest.split( '/' ); + this.updateVideoTime( timeParts[0], timeParts[1] ); + }, + + /** + * Update Video time from provided startNpt and endNpt values + * + * @param {String} + * startNpt the new start time in npt format ( hh:mm:ss.ms ) + * @param {String} + * endNpt the new end time in npt format ( hh:mm:ss.ms ) + */ + updateVideoTime: function( startNpt, endNpt ) { + // update media + this.mediaElement.updateSourceTimes( startNpt, endNpt ); + + // update time + this.controlBuilder.setStatus( startNpt + '/' + endNpt ); + + // reset slider + this.updatePlayHead( 0 ); + + // Reset the serverSeekTime if urlTimeEncoding is enabled + if ( this.supportsURLTimeEncoding() ) { + this.serverSeekTime = 0; + } else { + this.serverSeekTime = mw.npt2seconds( startNpt ); + } + }, + + + /** + * Update Thumb time with npt formated time + * + * @param {String} + * time NPT formated time to update thumbnail + */ + updateThumbTimeNPT: function( time ) { + this.updateThumbTime( mw.npt2seconds( time ) - parseInt( this.startOffset ) ); + }, + + /** + * Update the thumb with a new time + * + * @param {Float} + * floatSeconds Time to update the thumb to + */ + updateThumbTime:function( floatSeconds ) { + // mw.log('updateThumbTime:'+floatSeconds); + var _this = this; + if ( typeof this.orgThumSrc == 'undefined' ) { + this.orgThumSrc = this.poster; + } + if ( this.orgThumSrc.indexOf( 't=' ) !== -1 ) { + this.lastThumbUrl = mw.replaceUrlParams( this.orgThumSrc, + { + 't' : mw.seconds2npt( floatSeconds + parseInt( this.startOffset ) ) + } + ); + if ( !this.thumbnailUpdatingFlag ) { + this.updatePoster( this.lastThumbUrl , false ); + this.lastThumbUrl = null; + } + } + }, + + /** + * Updates the displayed thumbnail via percent of the stream + * + * @param {Float} + * percent Percent of duration to update thumb + */ + updateThumbPerc:function( percent ) { + return this.updateThumbTime( ( this.getDuration() * percent ) ); + }, + + /** + * Update the poster source + * @param {String} + * posterSrc Poster src url + */ + updatePosterSrc: function( posterSrc ){ + if( ! posterSrc ) { + posterSrc = mw.config.get( 'EmbedPlayer.BlackPixel' ); + } + this.poster = posterSrc; + this.updatePosterHTML(); + this.applyIntrinsicAspect(); + }, + + /** + * Called after sources are updated, and your ready for the player to change media + * @return + */ + changeMedia: function( callback ){ + var _this = this; + var $this = $( this ); + mw.log( 'EmbedPlayer:: changeMedia '); + // Empty out embedPlayer object sources + this.emptySources(); + + // onChangeMedia triggered at the start of the change media commands + $this.trigger( 'onChangeMedia' ); + + // Reset first play to true, to count that play event + this.firstPlay = true; + // reset donePlaying count on change media. + this.donePlayingCount = 0; + this.triggeredEndDone = false; + this.preSequence = false; + this.postSequence = false; + + this.setCurrentTime( 0.01 ); + // Reset the playhead + this.updatePlayHead( 0 ); + // update the status: + this.controlBuilder.setStatus( this.getTimeRange() ); + + // Add a loader to the embed player: + this.pauseLoading(); + + // Clear out any player error ( both via attr and object property ): + this.setError( null ); + + // Clear out any player display blocks + this['data-blockPlayerDisplay'] = null + $this.attr( 'data-blockPlayerDisplay', ''); + + // Clear out the player error div: + this.getInterface().find('.error').remove(); + this.controlBuilder.closeAlert(); + this.controlBuilder.closeMenuOverlay(); + + // Restore the control bar: + this.getInterface().find('.control-bar').show(); + // Hide the play btn + this.hideLargePlayBtn(); + + //If we are change playing media add a ready binding: + var bindName = 'playerReady.changeMedia'; + $this.unbind( bindName ).bind( bindName, function(){ + mw.log('EmbedPlayer::changeMedia playerReady callback'); + // hide the loading spinner: + _this.hideSpinnerAndPlayBtn(); + // check for an erro on change media: + if( _this.getError() ){ + _this.showErrorMsg( _this.getError() ); + return ; + } + // Always show the control bar on switch: + if( _this.controlBuilder ){ + _this.controlBuilder.showControlBar(); + } + // Make sure the play button reflects the original play state + if( _this.autoplay ){ + _this.hideLargePlayBtn(); + } else { + _this.addLargePlayBtn(); + } + var source = _this.getSource(); + if( (_this.isPersistentNativePlayer() || _this.useNativePlayerControls()) && source ){ + // If switching a Persistent native player update the source: + // ( stop and play won't refresh the source ) + _this.switchPlaySource( source, function(){ + _this.changeMediaStarted = false; + $this.trigger( 'onChangeMediaDone' ); + if( _this.autoplay ){ + _this.play(); + } else { + // pause is need to keep pause sate, while + // switch source calls .play() that some browsers require. + // to reflect source swiches. + _this.pause(); + _this.addLargePlayBtn(); + } + if( callback ){ + callback() + } + }); + // we are handling trigger and callback asynchronously return here. + return ; + } + + // Reset changeMediaStarted flag + _this.changeMediaStarted = false; + + // Stop should unload the native player + _this.stop(); + + // reload the player + if( _this.autoplay ){ + _this.play(); + } else { + _this.addLargePlayBtn(); + } + + $this.trigger( 'onChangeMediaDone' ); + if( callback ) { + callback(); + } + }); + + // Load new sources per the entry id via the checkPlayerSourcesEvent hook: + $this.triggerQueueCallback( 'checkPlayerSourcesEvent', function(){ + // Start player events leading to playerReady + _this.setupSourcePlayer(); + }); + }, + /** + * Checks if the current player / configuration is an image play screen: + */ + isImagePlayScreen:function(){ + return ( this.useNativePlayerControls() && + !this.isLinkPlayer && + mw.isIphone() && + mw.config.get( 'EmbedPlayer.iPhoneShowHTMLPlayScreen') + ); + }, + /** + * Triggers widgetLoaded event - Needs to be triggered only once, at the first time playerReady is trigerred + */ + triggerWidgetLoaded: function() { + if ( !this.widgetLoaded ) { + this.widgetLoaded = true; + mw.log( "EmbedPlayer:: Trigger: widgetLoaded"); + this.triggerHelper( 'widgetLoaded' ); + } + }, + + /** + * Updates the poster HTML + */ + updatePosterHTML: function () { + mw.log( 'EmbedPlayer:updatePosterHTML::' + this.id ); + + var _this = this, + thumb_html = '', + class_atr = '', + style_atr = '', + profile = $.client.profile(); + + if( this.isImagePlayScreen() ){ + this.addPlayScreenWithNativeOffScreen(); + return ; + } + + // Set by default thumb value if not found + var posterSrc = ( this.poster ) ? this.poster : + mw.config.get( 'EmbedPlayer.BlackPixel' ); + + // Update PersistentNativePlayer poster: + if( this.isPersistentNativePlayer() ){ + var $vid = $( '#' + this.pid ).show(); + $vid.attr( 'poster', posterSrc ); + // Add a quick timeout hide / show ( firefox 4x bug with native poster updates ) + if ( profile.name === 'firefox' ){ + $vid.hide(); + setTimeout( function () { + $vid.show(); + }, 0); + } + } else { + // hide the pid if present: + $( '#' + this.pid ).hide(); + // Poster support is not very consistent in browsers use a jpg poster image: + $( this ) + .html( + $( '<img />' ) + .css({ + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'right': 0, + 'bottom': 0 + }) + .attr({ + 'src' : posterSrc + }) + .addClass( 'playerPoster' ) + .load(function(){ + _this.applyIntrinsicAspect(); + }) + ).show(); + } + if ( this.useLargePlayBtn() && this.controlBuilder + && + this.height > this.controlBuilder.getComponentHeight( 'playButtonLarge' ) + ) { + this.addLargePlayBtn(); + } + }, + /** + * Abstract method, must be set by player inteface + */ + addPlayScreenWithNativeOffScreen: function(){ + mw.log( "Error: EmbedPlayer, Must override 'addPlayScreenWithNativeOffScreen' with player inteface" ); + return ; + }, + /** + * Checks if a large play button should be displayed on the + * otherwise native player + */ + useLargePlayBtn: function(){ + if( this.isPersistantPlayBtn() ){ + return true; + } + // If we are using native controls return false: + return !this.useNativePlayerControls(); + }, + /** + * Checks if the play button should stay on screen during playback, + * cases where a native player is dipalyed such as iPhone. + */ + isPersistantPlayBtn: function(){ + return mw.isAndroid2() || + ( mw.isIphone() && mw.config.get( 'EmbedPlayer.iPhoneShowHTMLPlayScreen' ) ); + }, + /** + * Checks if native controls should be used + * + * @returns boolean true if the mwEmbed player interface should be used + * false if the mwEmbed player interface should not be used + */ + useNativePlayerControls: function() { + if( this.usenativecontrols === true ){ + return true; + } + + if( mw.config.get('EmbedPlayer.NativeControls') === true ) { + return true; + } + + // Check for special webkit property that allows inline iPhone playback: + if( mw.config.get('EmbedPlayer.WebKitPlaysInline') === true && mw.isIphone() ) { + return false; + } + + // Do some device detection devices that don't support overlays + // and go into full screen once play is clicked: + if( mw.isAndroid2() || mw.isIpod() || mw.isIphone() ){ + return true; + } + + // iPad can use html controls if its a persistantPlayer in the dom before loading ) + // else it needs to use native controls: + if( mw.isIpad() ){ + if( mw.config.get('EmbedPlayer.EnableIpadHTMLControls') === true){ + return false; + } else { + // Set warning that your trying to do iPad controls without + // persistent native player: + return true; + } + } + return false; + }, + /** + * Checks if the native player is persistent in the dom since the intial page build out. + */ + isPersistentNativePlayer: function(){ + if( this.isLinkPlayer ){ + return false; + } + // Since we check this early on sometimes the player + // has not yet been updated to the pid location + if( $('#' + this.pid ).length == 0 ){ + return $('#' + this.id ).hasClass('persistentNativePlayer'); + } + return $('#' + this.pid ).hasClass('persistentNativePlayer'); + }, + // + isTouchDevice: function(){ + return mw.isIpad() + || + mw.isAndroid40() + || + mw.isMobileChrome(); + }, + /** + * Hides the large play button + * TODO move to player controls + */ + hideLargePlayBtn: function(){ + if( this.getInterface() ){ + this.getInterface().find( '.play-btn-large' ).hide(); + } + }, + /** + * Add a play button (if not already there ) + */ + addLargePlayBtn: function(){ + // check if we are pauseLoading ( i.e switching media, seeking, etc. and don't display play btn: + if( this.isPauseLoading ){ + mw.log("EmbedPlayer:: addLargePlayBtn ( skip play button, during load )"); + return; + } + // if using native controls make sure we can click the big play button by restoring + // interface click events: + if( this.useNativePlayerControls() ){ + this.getInterface().css('pointer-events', 'auto'); + } + + // iPhone in WebKitPlaysInline mode does not support clickable overlays as of iOS 5.0 + if( mw.config.get( 'EmbedPlayer.WebKitPlaysInline') && mw.isIphone() ) { + return ; + } + if( this.getInterface().find( '.play-btn-large' ).length ){ + this.getInterface().find( '.play-btn-large' ).show(); + } else { + this.getVideoHolder().append( + this.controlBuilder.getComponent( 'playButtonLarge' ) + ); + } + }, + + getVideoHolder: function() { + return this.getInterface().find('.videoHolder'); + }, + + /** + * Abstract method, + * Get native player html ( should be set by mw.EmbedPlayerNative ) + */ + getNativePlayerHtml: function(){ + return $('<div />' ) + .css( 'width', this.getWidth() ) + .html( 'Error: Trying to get native html5 player without native support for codec' ); + }, + + /** + * Should be set via native embed support + */ + applyMediaElementBindings: function(){ + mw.log("Warning applyMediaElementBindings should be implemented by player interface" ); + return ; + }, + + /** + * Gets code to embed the player remotely for "share" this player links + */ + getSharingEmbedCode: function() { + switch( mw.config.get( 'EmbedPlayer.ShareEmbedMode' ) ){ + case 'iframe': + return this.getShareIframeObject(); + break; + case 'videojs': + return this.getShareEmbedVideoJs(); + break; + } + }, + + /** + * Gets code to embed the player in a wiki + */ + getWikiEmbedCode: function() { + if( this.apiTitleKey) { + return '[[File:' + this.apiTitleKey + ']]'; + } else { + return false; + } + }, + + /** + * Get the iframe share code: + */ + getShareIframeObject: function(){ + // TODO move to getShareIframeSrc + var iframeUrl = this.getIframeSourceUrl(); + + // Set up embedFrame src path + var embedCode = '<iframe src="' + mw.html.escape( iframeUrl ) + '" '; + + // Set width / height of embed object + embedCode += 'width="' + this.getPlayerWidth() +'" '; + embedCode += 'height="' + this.getPlayerHeight() + '" '; + embedCode += 'frameborder="0" '; + embedCode += 'webkitAllowFullScreen mozallowfullscreen allowFullScreen'; + + // Close up the embedCode tag: + embedCode+='></iframe>'; + + // Return the embed code + return embedCode; + }, + /** + * Gets the iframe source url + */ + getIframeSourceUrl: function(){ + var iframeUrl = false; + this.triggerHelper( 'getShareIframeSrc', [ function( localIframeSrc ){ + if( iframeUrl){ + mw.log("Error multiple modules binding getShareIframeSrc" ); + } + iframeUrl = localIframeSrc; + }, this.id ]); + if( iframeUrl ){ + return iframeUrl; + } + // old style embed: + var iframeUrl = mw.getMwEmbedPath() + 'mwEmbedFrame.php?'; + var params = {'src[]' : []}; + + // Output all the video sources: + for( var i=0; i < this.mediaElement.sources.length; i++ ){ + var source = this.mediaElement.sources[i]; + if( source.src ) { + params['src[]'].push(mw.absoluteUrl( source.src )); + } + } + // Output the poster attr + if( this.poster ){ + params.poster = this.poster; + } + + // Set the skin if set to something other than default + if( this.skinName ){ + params.skin = this.skinName; + } + + if( this.duration ) { + params.durationHint = parseFloat( this.duration ); + } + iframeUrl += $.param( params ); + return iframeUrl; + }, + /** + * Get the share embed Video tag html to share the embed code. + */ + getShareEmbedVideoJs: function(){ + + // Set the embed tag type: + var embedtag = ( this.isAudio() )? 'audio': 'video'; + + // Set up the mwEmbed js include: + var embedCode = '<script type="text/javascript" ' + + 'src="' + + mw.html.escape( + mw.absoluteUrl( + mw.getMwEmbedSrc() + ) + ) + '"></script>' + + '<' + embedtag + ' '; + + if( this.poster ) { + embedCode += 'poster="' + + mw.html.escape( mw.absoluteUrl( this.poster ) ) + + '" '; + } + + // Set the skin if set to something other than default + if( this.skinName ){ + embedCode += 'class="' + + mw.html.escape( this.skinName ) + + '" '; + } + + if( this.duration ) { + embedCode +='durationHint="' + parseFloat( this.duration ) + '" '; + } + + if( this.width || this.height ){ + embedCode += 'style="'; + embedCode += ( this.width )? 'width:' + this.width +'px;': ''; + embedCode += ( this.height )? 'height:' + this.height +'px;': ''; + embedCode += '" '; + } + + // Close the video attr + embedCode += '>'; + + // Output all the video sources: + for( var i=0; i < this.mediaElement.sources.length; i++ ){ + var source = this.mediaElement.sources[i]; + if( source.src ) { + embedCode +='<source src="' + + mw.absoluteUrl( source.src ) + + '" ></source>'; + } + } + // Close the video tag + embedCode += '</video>'; + + return embedCode; + }, + + + + /** + * Base Embed Controls + */ + + /** + * The Play Action + * + * Handles play requests, updates relevant states: + * seeking =false + * paused =false + * + * Triggers the play event + * + * Updates pause button Starts the "monitor" + */ + firstPlay : true, + preSequence: false, + inPreSequence: false, + replayEventCount : 0, + play: function() { + var _this = this; + var $this = $( this ); + // Store the absolute play time ( to track native events that should not invoke interface updates ) + mw.log( "EmbedPlayer:: play: " + this._propagateEvents + ' poster: ' + this.stopped ); + + this.absoluteStartPlayTime = new Date().getTime(); + + // Check if thumbnail is being displayed and embed html + if ( _this.isStopped() && (_this.preSequence == false || (_this.sequenceProxy && _this.sequenceProxy.isInSequence == false) )) { + if ( !_this.selectedPlayer ) { + _this.showPlayerError(); + return false; + } else { + _this.embedPlayerHTML(); + } + } + // playing, exit stopped state: + _this.stopped = false; + + if( !this.preSequence ) { + this.preSequence = true; + mw.log( "EmbedPlayer:: trigger preSequence " ); + this.triggerHelper( 'preSequence' ); + this.playInterfaceUpdate(); + // if we entered into ad loading return + if( _this.sequenceProxy && _this.sequenceProxy.isInSequence ){ + mw.log("EmbedPlayer:: isInSequence, do NOT play content"); + return false; + } + } + + // We need first play event for analytics purpose + if( this.firstPlay && this._propagateEvents) { + this.firstPlay = false; + this.triggerHelper( 'firstPlay', [ _this.id ] ); + } + + if( this.paused === true ){ + this.paused = false; + // Check if we should Trigger the play event + mw.log("EmbedPlayer:: trigger play event::" + !this.paused + ' events:' + this._propagateEvents ); + // trigger the actual play event: + if( this._propagateEvents ) { + this.triggerHelper( 'onplay' ); + } + } + + // If we previously finished playing this clip run the "replay hook" + if( this.donePlayingCount > 0 && !this.paused && this._propagateEvents ) { + this.replayEventCount++; + // Trigger end done on replay + this.triggeredEndDone = false; + if( this.replayEventCount <= this.donePlayingCount){ + mw.log("EmbedPlayer::play> trigger replayEvent"); + this.triggerHelper( 'replayEvent' ); + } + } + + // If we have start time defined, start playing from that point + if( this.currentTime < this.startTime ) { + $this.bind('playing.startTime', function(){ + $this.unbind('playing.startTime'); + if( !mw.isIOS() ){ + _this.setCurrentTime( _this.startTime ); + _this.startTime = 0; + } else { + // iPad seeking on syncronus play event sucks + setTimeout( function(){ + _this.setCurrentTime( _this.startTime, function(){ + _this.play(); + }); + _this.startTime = 0; + }, 500 ) + } + _this.startTime = 0; + }); + } + + this.playInterfaceUpdate(); + // If play controls are enabled continue to video content element playback: + if( _this._playContorls ){ + return true; + } else { + // return false ( Mock play event, or handled elsewhere ) + return false; + } + }, + /** + * Update the player inteface for playback + * TODO move to controlBuilder + */ + playInterfaceUpdate: function(){ + var _this = this; + mw.log( 'EmbedPlayer:: playInterfaceUpdate' ); + // Hide any overlay: + if( this.controlBuilder ){ + this.controlBuilder.closeMenuOverlay(); + } + // Hide any buttons or errors if present: + this.getInterface().find( '.error' ).remove(); + this.hideLargePlayBtn(); + + this.getInterface().find('.play-btn span') + .removeClass( 'ui-icon-play' ) + .addClass( 'ui-icon-pause' ); + + this.hideSpinnerOncePlaying(); + + this.getInterface().find( '.play-btn' ) + .unbind('click') + .click( function( ) { + if( _this._playContorls ){ + _this.pause(); + } + } ) + .attr( 'title', mw.msg( 'mwe-embedplayer-pause_clip' ) ); + }, + /** + * Pause player, and display a loading animation + * @return + */ + pauseLoading: function(){ + this.pause(); + this.addPlayerSpinner(); + this.isPauseLoading = true; + }, + /** + * Adds a loading spinner to the player. + */ + addPlayerSpinner: function(){ + var sId = 'loadingSpinner_' + this.id; + // remove any old spinner + $( '#' + sId ).remove(); + // hide the play btn if present + this.hideLargePlayBtn(); + // re add an absolute positioned spinner: + $( this ).show().getAbsoluteOverlaySpinner() + .attr( 'id', sId ); + }, + hideSpinner: function(){ + // remove the spinner + $( '#loadingSpinner_' + this.id + ',.loadingSpinner' ).remove(); + }, + /** + * Hides the loading spinner + */ + hideSpinnerAndPlayBtn: function(){ + this.isPauseLoading = false; + this.hideSpinner(); + // hide the play btn + this.hideLargePlayBtn(); + }, + /** + * Hides the loading spinner once playing. + */ + hideSpinnerOncePlaying: function(){ + this._checkHideSpinner = true; + }, + /** + * Base embed pause Updates the play/pause button state. + * + * There is no general way to pause the video must be overwritten by embed + * object to support this functionality. + * + * @param {Boolean} if the event was triggered by user action or propagated by js. + */ + pause: function() { + var _this = this; + // Trigger the pause event if not already paused and using native controls: + if( this.paused === false ){ + this.paused = true; + if( this._propagateEvents ){ + mw.log( 'EmbedPlayer:trigger pause:' + this.paused ); + // we only trigger "onpause" to avoid event propagation to the native object method + // i.e in jQuery ( this ).trigger('pause') also calls: this.pause(); + $( this ).trigger( 'onpause' ); + } + } + _this.pauseInterfaceUpdate(); + }, + /** + * Sets the player interface to paused mode. + */ + pauseInterfaceUpdate: function(){ + var _this =this; + mw.log("EmbedPlayer::pauseInterfaceUpdate"); + // Update the ctrl "paused state" + this.getInterface().find('.play-btn span' ) + .removeClass( 'ui-icon-pause' ) + .addClass( 'ui-icon-play' ); + + this.getInterface().find( '.play-btn' ) + .unbind('click') + .click( function() { + if( _this._playContorls ){ + _this.play(); + } + } ) + .attr( 'title', mw.msg( 'mwe-embedplayer-play_clip' ) ); + }, + /** + * Maps the html5 load request. There is no general way to "load" clips so + * underling plugin-player libs should override. + */ + load: function() { + // should be done by child (no base way to pre-buffer video) + mw.log( 'Waring:: the load method should be overided by player interface' ); + }, + + + /** + * Base embed stop + * + * Updates the player to the stop state. + * + * Shows Thumbnail + * Resets Buffer + * Resets Playhead slider + * Resets Status + * + * Trigger the "doStop" event + */ + stop: function() { + var _this = this; + mw.log( 'EmbedPlayer::stop:' + this.id ); + // update the player to stopped state: + this.stopped = true; + + // Rest the prequecne flag: + this.preSequence = false; + + // Trigger the stop event: + $( this ).trigger( 'doStop' ); + + // no longer seeking: + this.didSeekJump = false; + + // Reset current time and prev time and seek offset + this.currentTime = this.previousTime = this.serverSeekTime = 0; + + this.stopMonitor(); + + // pause playback ( if playing ) + if( !this.paused ){ + this.pause(); + } + // Restore the play button ( if not native controls or is android ) + if( this.useLargePlayBtn() ){ + this.addLargePlayBtn(); + this.pauseInterfaceUpdate(); + } + + // Native player controls: + if( !this.isPersistentNativePlayer() ){ + // Rewrite the html to thumbnail disp + this.showThumbnail(); + this.bufferedPercent = 0; // reset buffer state + this.controlBuilder.setStatus( this.getTimeRange() ); + } + // Reset the playhead + this.updatePlayHead( 0 ); + // update the status: + this.controlBuilder.setStatus( this.getTimeRange() ); + // reset buffer indicator: + this.bufferedPercent = 0; + this.updateBufferStatus(); + }, + + /** + * Base Embed mute + * + * Handles interface updates for toggling mute. Plug-in / player interface + * must handle the actual media player action + */ + toggleMute: function( userAction ) { + mw.log( 'EmbedPlayer::toggleMute> (old state:) ' + this.muted ); + if ( this.muted ) { + this.muted = false; + var percent = this.preMuteVolume; + } else { + this.muted = true; + this.preMuteVolume = this.volume; + var percent = 0; + } + // Change the volume and trigger the volume change so that other plugins can listen. + this.setVolume( percent, true ); + // Update the interface + this.setInterfaceVolume( percent ); + // trigger the onToggleMute event + $( this ).trigger('onToggleMute'); + }, + + /** + * Update volume function ( called from interface updates ) + * + * @param {float} + * percent Percent of full volume + * @param {triggerChange} + * boolean change if the event should be triggered + */ + setVolume: function( percent, triggerChange ) { + var _this = this; + // ignore NaN percent: + if( isNaN( percent ) ){ + return ; + } + // Set the local volume attribute + this.previousVolume = this.volume; + + this.volume = percent; + + // Un-mute if setting positive volume + if( percent != 0 ){ + this.muted = false; + } + + // Update the playerElement volume + this.setPlayerElementVolume( percent ); + //mw.log("EmbedPlayer:: setVolume:: " + percent + ' trigger volumeChanged: ' + triggerChange ); + if( triggerChange ){ + $( _this ).trigger('volumeChanged', percent ); + } + }, + + /** + * Updates the interface volume + * + * TODO should move to controlBuilder + * + * @param {float} + * percent Percentage volume to update interface + */ + setInterfaceVolume: function( percent ) { + if( this.supports[ 'volumeControl' ] && + this.getInterface().find( '.volume-slider' ).length + ) { + this.getInterface().find( '.volume-slider' ).slider( 'value', percent * 100 ); + } + }, + + /** + * Abstract method Update volume Method must be override by plug-in / player interface + * + * @param {float} + * percent Percentage volume to update + */ + setPlayerElementVolume: function( percent ) { + mw.log('Error player does not support volume adjustment' ); + }, + + /** + * Abstract method get volume Method must be override by plug-in / player interface + * (if player does not override we return the abstract player value ) + */ + getPlayerElementVolume: function(){ + // mw.log(' error player does not support getting volume property' ); + return this.volume; + }, + + /** + * Abstract method get volume muted property must be overwritten by plug-in / + * player interface (if player does not override we return the abstract + * player value ) + */ + getPlayerElementMuted: function(){ + // mw.log(' error player does not support getting mute property' ); + return this.muted; + }, + + /** + * Passes a fullscreen request to the controlBuilder interface + */ + fullscreen: function() { + this.controlBuilder.toggleFullscreen(); + }, + + /** + * Abstract method to be run post embedding the player Generally should be + * overwritten by the plug-in / player + */ + postEmbedActions:function() { + return ; + }, + + /** + * Checks the player state based on thumbnail display & paused state + * + * @return {Boolean} true if playing false if not playing + */ + isPlaying : function() { + if ( this.stopped ) { + // in stopped state + return false; + } else if ( this.paused ) { + // paused state + return false; + } else { + return true; + } + }, + + /** + * Get Stopped state + * + * @return {Boolean} true if stopped false if playing + */ + isStopped: function() { + return this.stopped; + }, + /** + * Stop the play state monitor + */ + stopMonitor: function(){ + clearInterval( this.monitorInterval ); + this.monitorInterval = 0; + }, + /** + * Start the play state monitor + */ + startMonitor: function(){ + this.monitor(); + }, + + /** + * Monitor playback and update interface components. underling player classes + * are responsible for updating currentTime + */ + monitor: function() { + var _this = this; + + // Check for current time update outside of embed player + _this.syncCurrentTime(); + + // mw.log( "monitor:: " + this.currentTime + ' propagateEvents: ' + _this._propagateEvents ); + + // update player status + _this.updatePlayheadStatus(); + + // Keep volume proprties set outside of the embed player in sync + _this.syncVolume(); + + // Make sure the monitor continues to run as long as the video is not stoped + _this.syncMonitor() + + if( _this._propagateEvents ){ + + // mw.log('trigger:monitor:: ' + this.currentTime ); + $( _this ).trigger( 'monitorEvent', [ _this.id ] ); + + // Trigger the "progress" event per HTML5 api support + if( _this.progressEventData ) { + $( _this ).trigger( 'progress', _this.progressEventData ); + } + } + }, + /** + * Sync the monitor function + */ + syncMonitor: function(){ + var _this = this; + // Call monitor at this.monitorRate interval. + // ( use setInterval to avoid stacking monitor requests ) + if( ! this.isStopped() ) { + if( !this.monitorInterval ){ + this.monitorInterval = setInterval( function(){ + if( _this.monitor ) + _this.monitor(); + }, this.monitorRate ); + } + } else { + // If stopped "stop" monitor: + this.stopMonitor(); + } + }, + + /** + * Sync the video volume + */ + syncVolume: function(){ + var _this = this; + // Check if volume was set outside of embed player function + // mw.log( ' this.volume: ' + _this.volume + ' prev Volume:: ' + _this.previousVolume ); + if( Math.round( _this.volume * 100 ) != Math.round( _this.previousVolume * 100 ) ) { + _this.setInterfaceVolume( _this.volume ); + } + // Update the previous volume + _this.previousVolume = _this.volume; + + // Update the volume from the player element + _this.volume = this.getPlayerElementVolume(); + + // update the mute state from the player element + if( _this.muted != _this.getPlayerElementMuted() && ! _this.isStopped() ){ + mw.log( "EmbedPlayer::syncVolume: muted does not mach embed player" ); + _this.toggleMute(); + // Make sure they match: + _this.muted = _this.getPlayerElementMuted(); + } + }, + + /** + * Checks if the currentTime was updated outside of the getPlayerElementTime function + */ + syncCurrentTime: function(){ + var _this = this; + + // Hide the spinner once we have time update: + if( _this._checkHideSpinner && _this.currentTime != _this.getPlayerElementTime() ){ + _this._checkHideSpinner = false; + _this.hideSpinnerAndPlayBtn(); + + if( _this.isPersistantPlayBtn() ){ + // add the play button likely iphone or native player that needs the play button on + // non-event "exit native html5 player" + _this.addLargePlayBtn(); + } else{ + // also hide the play button ( in case it was there somehow ) + _this.hideLargePlayBtn(); + } + } + + // Check if a javascript currentTime change based seek has occurred + if( parseInt( _this.previousTime ) != parseInt( _this.currentTime ) && + !this.userSlide && + !this.seeking && + !this.isStopped() + ){ + // If the time has been updated and is in range issue a seek + if( _this.getDuration() && _this.currentTime <= _this.getDuration() ){ + var seekPercent = _this.currentTime / _this.getDuration(); + mw.log("EmbedPlayer::syncCurrentTime::" + _this.previousTime + ' != ' + + _this.currentTime + " javascript based currentTime update to " + + seekPercent + ' == ' + _this.currentTime ); + _this.previousTime = _this.currentTime; + this.seek( seekPercent ); + } + } + + // Update currentTime via embedPlayer + _this.currentTime = _this.getPlayerElementTime(); + + // Update any offsets from server seek + if( _this.serverSeekTime && _this.supportsURLTimeEncoding() ){ + _this.currentTime = parseInt( _this.serverSeekTime ) + parseInt( _this.getPlayerElementTime() ); + } + + // Update the previousTime ( so we can know if the user-javascript changed currentTime ) + _this.previousTime = _this.currentTime; + + // Check for a pauseTime to stop playback in temporal media fragments + if( _this.pauseTime && _this.currentTime > _this.pauseTime ){ + _this.pause(); + _this.pauseTime = null; + } + }, + /** + * Updates the player time and playhead position based on currentTime + */ + updatePlayheadStatus: function(){ + var _this = this; + if ( this.currentTime >= 0 && this.duration ) { + if ( !this.userSlide && !this.seeking ) { + if ( parseInt( this.startOffset ) != 0 ) { + this.updatePlayHead( ( this.currentTime - this.startOffset ) / this.duration ); + var et = ( this.controlBuilder.longTimeDisp ) ? '/' + mw.seconds2npt( parseFloat( this.startOffset ) + parseFloat( this.duration ) ) : ''; + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + et ); + } else { + // use raw currentTIme for playhead updates + var ct = ( this.getPlayerElement() ) ? this.getPlayerElement().currentTime || this.currentTime: this.currentTime; + this.updatePlayHead( ct / this.duration ); + // Only include the end time if longTimeDisp is enabled: + var et = ( this.controlBuilder.longTimeDisp ) ? '/' + mw.seconds2npt( this.duration ) : ''; + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + et ); + } + } + // Check if we are "done" + var endPresentationTime = ( this.startOffset ) ? ( this.startOffset + this.duration ) : this.duration; + if ( this.currentTime >= endPresentationTime && !this.isStopped() ) { + mw.log( "EmbedPlayer::updatePlayheadStatus > should run clip done :: " + this.currentTime + ' > ' + endPresentationTime ); + this.onClipDone(); + } + } else { + // Media lacks duration just show end time + if ( this.isStopped() ) { + this.controlBuilder.setStatus( this.getTimeRange() ); + } else if ( this.paused ) { + this.controlBuilder.setStatus( mw.msg( 'mwe-embedplayer-paused' ) ); + } else if ( this.isPlaying() ) { + if ( this.currentTime && ! this.duration ) + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + ' /' ); + else + this.controlBuilder.setStatus( " - - - " ); + } else { + this.controlBuilder.setStatus( this.getTimeRange() ); + } + } + }, + + /** + * Abstract getPlayerElementTime function + */ + getPlayerElementTime: function(){ + mw.log("Error: getPlayerElementTime should be implemented by embed library"); + }, + + /** + * Abstract getPlayerElementTime function + */ + getPlayerElement: function(){ + mw.log("Error: getPlayerElement should be implemented by embed library, or you may be calling this event too soon"); + }, + + /** + * Update the Buffer status based on the local bufferedPercent var + */ + updateBufferStatus: function() { + // Get the buffer target based for playlist vs clip + var $buffer = this.getInterface().find( '.mw_buffer' ); + // Update the buffer progress bar (if available ) + if ( this.bufferedPercent != 0 ) { + // mw.log('Update buffer css: ' + ( this.bufferedPercent * 100 ) + + // '% ' + $buffer.length ); + if ( this.bufferedPercent > 1 ){ + this.bufferedPercent = 1; + } + $buffer.css({ + "width" : ( this.bufferedPercent * 100 ) + '%' + }); + $( this ).trigger( 'updateBufferPercent', this.bufferedPercent ); + } else { + $buffer.css( "width", '0px' ); + } + + // if we have not already run the buffer start hook + if( this.bufferedPercent > 0 && !this.bufferStartFlag ) { + this.bufferStartFlag = true; + mw.log("EmbedPlayer::bufferStart"); + $( this ).trigger( 'bufferStartEvent' ); + } + + // if we have not already run the buffer end hook + if( this.bufferedPercent == 1 && !this.bufferEndFlag){ + this.bufferEndFlag = true; + $( this ).trigger( 'bufferEndEvent' ); + } + }, + + /** + * Update the player playhead + * + * @param {Float} + * perc Value between 0 and 1 for position of playhead + */ + updatePlayHead: function( perc ) { + //mw.log( 'EmbedPlayer: updatePlayHead: '+ perc); + if( this.getInterface() ){ + var $playHead = this.getInterface().find( '.play_head' ); + if ( !this.useNativePlayerControls() && $playHead.length != 0 ) { + var val = parseInt( perc * 1000 ); + $playHead.slider( 'value', val ); + } + } + $( this ).trigger('updatePlayHeadPercent', perc); + }, + + + /** + * Helper Functions for selected source + */ + + /** + * Get the current selected media source or first source + * + * @param {Number} + * Requested time in seconds to be passed to the server if the + * server supports supportsURLTimeEncoding + * @return src url + */ + getSrc: function( serverSeekTime ) { + if( serverSeekTime ){ + this.serverSeekTime = serverSeekTime; + } + if( this.currentTime && !this.serverSeekTime){ + this.serverSeekTime = this.currentTime; + } + + // No media element we can't return src + if( !this.mediaElement ){ + return false; + } + + // If no source selected auto select the source: + if( !this.mediaElement.selectedSource ){ + this.mediaElement.autoSelectSource(); + }; + + // Return selected source: + if( this.mediaElement.selectedSource ){ + // See if we should pass the requested time to the source generator: + if( this.supportsURLTimeEncoding() ){ + // get the first source: + return this.mediaElement.selectedSource.getSrc( this.serverSeekTime ); + } else { + return this.mediaElement.selectedSource.getSrc(); + } + } + // No selected source return false: + return false; + }, + /** + * Return the currently selected source + */ + getSource: function(){ + // update the current selected source: + this.mediaElement.autoSelectSource(); + return this.mediaElement.selectedSource; + }, + /** + * Static helper to get media sources from a set of videoFiles + * + * Uses mediaElement select logic to chose a + * video file among a set of sources + * + * @param videoFiles + * @return + */ + getCompatibleSource: function( videoFiles ){ + // Convert videoFiles json into HTML element: + // TODO mediaElement should probably accept JSON + var $media = $('<video />'); + $.each(videoFiles, function( inx, source){ + $media.append( $('<source />').attr({ + 'src' : source.src, + 'type' : source.type + })); + mw.log("EmbedPlayer::getCompatibleSource: add " + source.src + ' of type:' + source.type ); + }); + var myMediaElement = new mw.MediaElement( $media[0] ); + var source = myMediaElement.autoSelectSource(); + if( source ){ + mw.log("EmbedPlayer::getCompatibleSource: " + source.getSrc()); + return source; + } + mw.log("Error:: could not find compatible source"); + return false; + }, + /** + * If the selected src supports URL time encoding + * + * @return {Boolean} true if the src supports url time requests false if the + * src does not support url time requests + */ + supportsURLTimeEncoding: function() { + var timeUrls = mw.config.get('EmbedPlayer.EnableURLTimeEncoding') ; + if( timeUrls == 'none' ){ + return false; + } else if( timeUrls == 'always' ){ + return this.mediaElement.selectedSource.URLTimeEncoding; + } else if( timeUrls == 'flash' ){ + if( this.mediaElement.selectedSource && this.mediaElement.selectedSource.URLTimeEncoding){ + // see if the current selected player is flash: + return ( this.instanceOf == 'Kplayer' ); + } + } else { + mw.log("Error:: invalid config value for EmbedPlayer.EnableURLTimeEncoding:: " + mw.config.get('EmbedPlayer.EnableURLTimeEncoding') ); + } + return false; + } + }; + +})( mw, jQuery ); |