/** * 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( $('
') .addClass( 'mwPlayerContainer ' + this.controlBuilder.playerClass ) .append( $('').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( $( '' ).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( $( '