From a5f917bbc55e295896b8084f6657eb8b6abaf8a8 Mon Sep 17 00:00:00 2001 From: André Fabian Silva Delgado Date: Fri, 15 Jul 2016 15:33:36 -0300 Subject: Add TimedMediaHandler extension that allows display audio and video files in wiki pages, using the same syntax as for image files --- .../EmbedPlayer/resources/blackvideo.mp4 | Bin 0 -> 4575 bytes .../EmbedPlayer/resources/mw.EmbedPlayer.js | 2760 ++++++++++++++++++++ .../EmbedPlayer/resources/mw.EmbedPlayerGeneric.js | 36 + .../resources/mw.EmbedPlayerIEWebMPrompt.css | 18 + .../resources/mw.EmbedPlayerIEWebMPrompt.js | 46 + .../resources/mw.EmbedPlayerImageOverlay.js | 307 +++ .../EmbedPlayer/resources/mw.EmbedPlayerKplayer.js | 485 ++++ .../EmbedPlayer/resources/mw.EmbedPlayerNative.js | 1088 ++++++++ .../EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js | 222 ++ .../EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js | 101 + .../EmbedPlayer/resources/mw.EmbedPlayerVlc.js | 358 +++ .../EmbedPlayer/resources/mw.EmbedTypes.js | 360 +++ .../EmbedPlayer/resources/mw.MediaElement.js | 491 ++++ .../EmbedPlayer/resources/mw.MediaPlayer.js | 85 + .../EmbedPlayer/resources/mw.MediaPlayers.js | 195 ++ .../EmbedPlayer/resources/mw.MediaSource.js | 490 ++++ .../resources/mw.processEmbedPlayers.js | 353 +++ .../EmbedPlayer/resources/skins/EmbedPlayer.css | 166 ++ .../resources/skins/kskin/PlayerSkinKskin.css | 484 ++++ .../images/kaltura_open_source_video_platform.gif | Bin 0 -> 3368 bytes .../images/kaltura_open_source_video_platform.png | Bin 0 -> 302 bytes .../resources/skins/kskin/images/ksprite.png | Bin 0 -> 13520 bytes .../resources/skins/kskin/mw.PlayerSkinKskin.js | 394 +++ .../resources/skins/mvpcf/PlayerSkinMvpcf.css | 194 ++ .../skins/mvpcf/images/player_big_play_button.png | Bin 0 -> 2935 bytes .../resources/skins/mvpcf/mw.PlayerSkinMvpcf.js | 7 + .../resources/skins/mw.PlayerControlBuilder.js | 2721 +++++++++++++++++++ 27 files changed, 11361 insertions(+) create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerGeneric.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.css create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerImageOverlay.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerKplayer.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVlc.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedTypes.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaElement.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayer.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayers.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaSource.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.processEmbedPlayers.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/EmbedPlayer.css create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/PlayerSkinKskin.css create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gif create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.png create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/mw.PlayerSkinKskin.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/PlayerSkinMvpcf.css create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.png create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/mw.PlayerSkinMvpcf.js create mode 100644 extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mw.PlayerControlBuilder.js (limited to 'extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources') diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 new file mode 100644 index 00000000..cf86d1a8 Binary files /dev/null and b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 differ 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( + $('
') + .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( + $( '' ) + .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 $('
' ) + .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 = $('' ) + .attr( 'href', pluginUrl ) + .attr( 'target', '_blank' ) + .text( mw.msg( 'mwe-embedplayer-iewebmprompt-linktext' ) ); + $( this ).append( $( '
' ) + .width( this.getWidth() ) + .height( this.getHeight() ) + .append( $( '
' ).text( mw.msg( 'mwe-embedplayer-iewebmprompt-intro' ) ) ) + .append( $link ) + .append( $( '
' ).text( mw.msg( 'mwe-embedplayer-iewebmprompt-outro' ) ) ) + ); + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerImageOverlay.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerImageOverlay.js new file mode 100644 index 00000000..f6271621 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerImageOverlay.js @@ -0,0 +1,307 @@ +/** + * Used to Overlay images and take over player controls + * + * extends EmbedPlayerNative object image overlay support + */ + +mw.EmbedPlayerImageOverlay = { + + instanceOf: 'ImageOverlay', + + // If the player is "ready to play" + playerReady : true, + + // Pause time used to track player time between pauses + lastPauseTime: 0, + + // currentTime updated via internal clockStartTime var + currentTime:0, + + // StartOffset support seeking into the virtual player + startOffset:0, + + // The local clock used to emulate playback time + clockStartTime: 0, + + /** + * Build the player interface: + */ + init: function(){ + // Check if features are already updated: + if( this['native_instaceOf'] == 'Native' ){ + return ; + } + // inherit mw.EmbedPlayerNative ( + for( var i in mw.EmbedPlayerNative ){ + if( typeof mw.EmbedPlayerImageOverlay[ i ] != 'undefined' ){ + this['native_' + i ] = mw.EmbedPlayerNative[i]; + } else { + this[ i ] = mw.EmbedPlayerNative[i]; + } + } + }, + + /** + * When on playback method switch remove imageOverlay + * @param {function} callback + */ + updatePlaybackInterface: function( callback ){ + mw.log( 'EmbedPlayerImageOverlay:: updatePlaybackInterface remove imageOverlay: ' + $(this).siblings( '.imageOverlay' ).length ); + // Reset lastPauseTime + this.lastPauseTime = 0; + // Clear imageOverlay sibling: + $( this ).find( '.imageOverlay' ).remove(); + // Restore the video element on screen position: + $( this.getPlayerElement() ).css('left', 0 ); + // Call normal parent updatePlaybackInterface + this.parent_updatePlaybackInterface( callback ); + }, + + /** + * The method called to "show the player" + * For image overlay we want to: + * Set black video urls for player source + * Add an image overlay + */ + updatePosterHTML: function(){ + var vid = this.getPlayerElement(); + $( vid ).empty() + + // Provide modules the opportunity to supply black sources ( for registering event click ) + // this is need for iPad to capture the play click to auto continue after "playing an image" + // ( iOS requires a user gesture to initiate video playback ) + + // We don't just include the sources as part of core config, since it would result in + // a possible privacy leakage i.e hitting the kaltura servers when playing images. + this.triggerHelper( 'AddEmptyBlackSources', [ vid ] ); + + // embed the image: + this.embedPlayerHTML(); + + // add the play btn: + this.addLargePlayBtn(); + }, + + /** + * Play function starts the video playback + */ + play: function() { + mw.log( 'EmbedPlayerImageOverlay::play> lastPauseTime:' + this.lastPauseTime + ' ct: ' + this.currentTime ); + this.applyIntrinsicAspect(); + // Check for image duration + + // Reset playback if currentTime > duration: + if( this.currentTime > this.getDuration() ) { + this.currentTime = this.pauseTime = 0; + } + + // No longer in a stopped state: + this.stopped = false; + + // Capture the play event on the native player: ( should just be black silent sources ) + // This is needed so that if a playlist starts with image, it can continue to play the + // subsequent video without on iOS without requiring another click. + if( ! $( this ).data('previousInstanceOf') ){ + // Update the previousInstanceOf flag: + $( this ).data('previousInstanceOf', this.instanceOf ); + var vid = this.getPlayerElement(); + // populate the video with black video sources: + this.triggerHelper( 'AddEmptyBlackSources', [ vid ] ); + // run play: + vid.play(); + // inline pause + setTimeout(function(){ + vid.pause(); + },0); + // add another pause request after 500 ms ( iOS sometimes does not listen the first time ) + setTimeout(function(){ + vid.pause(); + }, mw.config.get( 'EmbedPlayer.MonitorRate' ) * 2 ); + } + // call the parent play ( to update interface and call respective triggers ) + this.parent_play(); + // make sure we are in play interface: + this.playInterfaceUpdate(); + + this.clockStartTime = new Date().getTime(); + // Start up monitor: + this.monitor(); + }, + getDuration: function(){ + if( this.duration ){ + return this.duration; + } + if( this.imageDuration ){ + this.duration = this.imageDuration ; + } else { + this.duration = mw.config.get( "EmbedPlayer.DefaultImageDuration" ); + } + // make sure duration has type float: + this.duration = parseFloat( this.duration ); + return this.duration; + }, + /** + * Stops the playback + */ + stop: function() { + this.currentTime = 0; + this.parent_stop(); + }, + _onpause: function(){ + // catch the native event ( and do nothing ) + }, + /** + * Preserves the pause time across for timed playback + */ + pause: function( ) { + this.lastPauseTime = this.currentTime; + mw.log( 'EmbedPlayerImageOverlay::pause, lastPauseTime: ' + this.lastPauseTime ); + // run parent pause; + this.parent_pause(); + this.stopMonitor(); + }, + + monitor: function(){ + if( this.duration == 0 ){ + this.disablePlayControls(); + return ; + } + $( this ).trigger( 'timeupdate' ); + + if ( this.currentTime >= this.duration ) { + $( this ).trigger( 'ended' ); + } else { + // Run the parent monitor: + this.parent_monitor(); + } + }, + /** + * Seeks to a given percent and updates the lastPauseTime + * + * @param {Float} seekPercent Percentage to seek into the virtual player + */ + seek: function( seekPercent ) { + this.lastPauseTime = seekPercent * this.getDuration(); + this.seeking = false; + // start seeking: + $( this ).trigger( 'seeking' ); + // Done seeking + $( this ).trigger( 'seeked' ); + this.play(); + }, + + /** + * Sets the current Time + * + * @param {Float} perc Percentage to seek into the virtual player + * @param {Function} callback Function called once time has been updated + */ + setCurrentTime: function( time, callback ) { + this.lastPauseTime = time; + // start seeking: + $( this ).trigger( 'seeking' ); + // Done seeking + $( this ).trigger( 'seeked' ); + if( callback ){ + callback(); + } + }, + /** + * Switch the image playback + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + var _this = this; + this.selectedSource = source; + this.embedPlayerHTML(); + this.applyIntrinsicAspect(); + this.play(); + if( switchCallback ){ + switchCallback( this ); + } + // Wait for ended event to tr + $( this ).bind('ended.playerSwitchSource', function(){ + $( _this ).unbind('ended.playerSwitchSource'); + if( doneCallback ) { + doneCallback( this ); + } + }) + }, + /** + * Get the embed player time + */ + getPlayerElementTime: function() { + this.currentTime = ( ( new Date().getTime() - this.clockStartTime ) / 1000 ) + this.lastPauseTime; + return this.currentTime; + }, + /** + * Get the "embed" html for the html player + */ + embedPlayerHTML: function() { + var _this = this; + // remove any old imageOverlay: + this.$interface.find('.imageOverlay').remove(); + mw.log( 'EmbedPlayerImageOverlay :doEmbedHTML: ' + this.id ); + + var currentSoruceObj = this.selectedSource; + + if( !currentSoruceObj ){ + mw.log("Error:: EmbedPlayerImageOverlay:embedPlayerHTML> missing source" ); + return ; + } + var $image = + $( '' ) + .css({ + 'position': 'relative', + 'width': '100%', + 'height': '100%' + }) + .attr({ + 'src' : currentSoruceObj.getSrc() + }) + .addClass( 'imageOverlay' ) + .load( function(){ + // reset clock time: + _this.clockStartTime = new Date().getTime(); + _this.monitor(); + }) + + // move the video element off screen: + $( this.getPlayerElement() ).css({ + 'left': this.getWidth()+50, + 'position' : 'absolute' + }); + + // Add the image before the video element or before the playerInterface + $( this ).html( $image ); + + this.applyIntrinsicAspect(); + }, + // wrap the parent rewize player to apply intensic apsect + resizePlayer: function( size , animate, callback){ + this.parent_resizePlayer( size , animate, callback ); + this.applyIntrinsicAspect(); + }, + applyIntrinsicAspect: function(){ + var $this = this.$interface; + // Check if a image thumbnail is present: + /*if( this.$interface && this.$interface.find('.imageOverlay').length ){ + var img = this.$interface.find('.imageOverlay')[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' + }); + } + }*/ + } +}; \ No newline at end of file diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerKplayer.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerKplayer.js new file mode 100644 index 00000000..739a7769 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerKplayer.js @@ -0,0 +1,485 @@ +/* + * The "kaltura player" embedPlayer interface for fallback h.264 and flv video format support + */ +( function( mw, $ ) { "use strict"; + +// Called from the kdp.swf +window.jsInterfaceReadyFunc = function() { + return true; +} + +mw.EmbedPlayerKplayer = { + + // Instance name: + instanceOf : 'Kplayer', + + bindPostfix: '.kPlayer', + + // List of supported features: + supports : { + 'playHead' : true, + 'pause' : true, + 'stop' : true, + 'timeDisplay' : true, + 'volumeControl' : true, + 'overlays' : true, + 'fullscreen' : true + }, + + // Stores the current time as set from flash player + flashCurrentTime : 0, + + /* + * Write the Embed html to the target + */ + embedPlayerHTML : function() { + var _this = this; + + mw.log("EmbedPlayerKplayer:: embed src::" + _this.getSrc()); + var flashvars = {}; + flashvars.autoPlay = "true"; + flashvars.loop = "false"; + + var playerPath = mw.absoluteUrl( mw.getEmbedPlayerPath() + '/binPlayers/kaltura-player' ); + flashvars.entryId = mw.absoluteUrl( _this.getSrc() ); + + // Use a relative url if the protocol is file:// + if ( new mw.Uri( document.URL ).protocol == 'file' ) { + playerPath = mw.getRelativeMwEmbedPath() + 'modules/EmbedPlayer/binPlayers/kaltura-player'; + flashvars.entryId = _this.getSrc(); + } + + flashvars.debugMode = "false"; + flashvars.fileSystemMode = "true"; + flashvars.widgetId = "_7463"; + flashvars.partnerId = "7463"; + flashvars.pluginDomain = "kdp3/plugins/"; + flashvars.kml = "local"; + flashvars.kmlPath = playerPath + '/config.xml'; + flashvars.sourceType = "url"; + flashvars.jsInterfaceReadyFunc = "jsInterfaceReadyFunc"; + + // flashvars.host = "www.kaltura.com"; + flashvars.externalInterfaceDisabled = "false"; + flashvars.skinPath = playerPath + '/skin.swf'; + + flashvars["full.skinPath"] = playerPath + '/LightDoodleskin.swf'; + var flashVarParam = ''; + $.each( flashvars, function( fKey, fVal ){ + flashVarParam += '&' + fKey + '=' + encodeURIComponent( fVal ); + } ); + + var kdpPath = playerPath + "/kdp3.3.5.27.swf"; + + mw.log( "KPlayer:: embedPlayerHTML" ); + // remove any existing pid ( if present ) + $( '#' + this.pid ).remove(); + + var orgJsReadyCallback = window.jsCallbackReady; + window.jsCallbackReady = function( playerId ){ + _this.postEmbedActions(); + window.jsCallbackReady = orgJsReadyCallback; + }; + // attributes and params: + flashembed( $( this ).attr('id'), + { + id : this.pid, + src : kdpPath, + height : '100%', + width : '100%', + bgcolor : "#000000", + allowNetworking : "all", + version : [10,0], + wmode : "opaque" + }, + flashvars + ) + // Remove any old bindings: + $(_this).unbind( this.bindPostfix ); + + // Flash player loses its bindings once it changes sizes:: + $(_this).bind('onOpenFullScreen' + this.bindPostfix , function() { + _this.postEmbedActions(); + }); + $(_this).bind('onCloseFullScreen' + this.bindPostfix, function() { + _this.postEmbedActions(); + }); + }, + + // The number of times we have tried to bind the player + bindTryCount : 0, + + /** + * javascript run post player embedding + */ + postEmbedActions : function() { + var _this = this; + this.getPlayerElement(); + if ( this.playerElement && this.playerElement.addJsListener ) { + var bindEventMap = { + 'playerPaused' : 'onPause', + 'playerPlayed' : 'onPlay', + 'durationChange' : 'onDurationChange', + 'playerPlayEnd' : 'onClipDone', + 'playerUpdatePlayhead' : 'onUpdatePlayhead', + 'bytesTotalChange' : 'onBytesTotalChange', + 'bytesDownloadedChange' : 'onBytesDownloadedChange' + }; + + $.each( bindEventMap, function( bindName, localMethod ) { + _this.bindPlayerFunction(bindName, localMethod); + }); + this.bindTryCount = 0; + // Start the monitor + this.monitor(); + } else { + this.bindTryCount++; + // Keep trying to get the player element + if( this.bindTryCount > 500 ){ // 5 seconds + mw.log('Error:: KDP player never ready for bindings!'); + return ; + } + setTimeout(function() { + _this.postEmbedActions(); + }, 100); + } + }, + + /** + * Bind a Player Function, + * + * Build a global callback to bind to "this" player instance: + * + * @param {String} + * flash binding name + * @param {String} + * function callback name + */ + bindPlayerFunction : function(bindName, methodName) { + mw.log( 'EmbedPlayerKplayer:: bindPlayerFunction:' + bindName ); + // The kaltura kdp can only call a global function by given name + var gKdpCallbackName = 'kdp_' + methodName + '_cb_' + this.id.replace(/[^a-zA-Z 0-9]+/g,''); + + // Create an anonymous function with local player scope + var createGlobalCB = function(cName, embedPlayer) { + window[ cName ] = function(data) { + // Track all events ( except for playerUpdatePlayhead ) + if( bindName != 'playerUpdatePlayhead' ){ + mw.log("EmbedPlayerKplayer:: event: " + bindName); + } + if ( embedPlayer._propagateEvents ) { + embedPlayer[methodName](data); + } + }; + }(gKdpCallbackName, this); + // Remove the listener ( if it exists already ) + this.playerElement.removeJsListener( bindName, gKdpCallbackName ); + // Add the listener to the KDP flash player: + this.playerElement.addJsListener( bindName, gKdpCallbackName); + }, + + /** + * on Pause callback from the kaltura flash player calls parent_pause to + * update the interface + */ + onPause : function() { + this.parent_pause(); + }, + + /** + * onPlay function callback from the kaltura flash player directly call the + * parent_play + */ + onPlay : function() { + this.parent_play(); + }, + + onDurationChange : function(data, id) { + // Update the duration ( only if not in url time encoding mode: + if( !this.supportsURLTimeEncoding() ){ + this.duration = data.newValue; + this.triggerHelper('durationchange'); + } + }, + + /** + * play method calls parent_play to update the interface + */ + play: function() { + if ( this.playerElement && this.playerElement.sendNotification ) { + this.playerElement.sendNotification('doPlay'); + } + this.parent_play(); + }, + + /** + * pause method calls parent_pause to update the interface + */ + pause: function() { + if (this.playerElement && this.playerElement.sendNotification) { + this.playerElement.sendNotification('doPause'); + } + this.parent_pause(); + }, + /** + * playerSwitchSource switches the player source working around a few bugs in browsers + * + * @param {object} + * source Video Source object to switch to. + * @param {function} + * switchCallback Function to call once the source has been switched + * @param {function} + * doneCallback Function to call once the clip has completed playback + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + var _this = this; + var waitCount = 0; + var src = source.getSrc(); + // Check if the source is already set to the target: + if( !src || src == this.getSrc() ){ + if( switchCallback ){ + switchCallback(); + } + setTimeout(function(){ + if( doneCallback ) + doneCallback(); + }, 100); + return ; + } + + var waitForJsListen = function( callback ){ + if( _this.getPlayerElement() && _this.getPlayerElement().addJsListener ){ + callback(); + } else { + // waited for 2 seconds fail + if( waitCount > 20 ){ + mw.log( "Error: Failed to swtich player source!"); + if( switchCallback ) + switchCallback(); + if( doneCallback ) + doneCallback(); + return; + } + + setTimeout(function(){ + waitCount++; + waitForJsListen( callback ); + },100) + } + }; + // wait for jslistener to be ready: + waitForJsListen( function(){ + var gPlayerReady = 'kdp_switch_' + _this.id + '_switchSrcReady'; + var gDoneName = 'kdp_switch_' + _this.id + '_switchSrcEnd'; + var gChangeMedia = 'kdp_switch_' + _this.id + '_changeMedia'; + window[ gPlayerReady ] = function(){ + mw.log("EmbedPlayerKplayer:: playerSwitchSource: " + src); + // remove the binding as soon as possible ( we only want this event once ) + _this.getPlayerElement().removeJsListener( 'playerReady', gPlayerReady ); + + _this.getPlayerElement().sendNotification("changeMedia", { 'entryId': src } ); + + window[ gChangeMedia ] = function (){ + mw.log("EmbedPlayerKplayer:: Media changed: " + src); + if( $.isFunction( switchCallback) ){ + switchCallback( _this ); + switchCallback = null + } + // restore monitor: + _this.monitor(); + } + // Add change media binding + _this.getPlayerElement().removeJsListener('changeMedia', gChangeMedia); + _this.getPlayerElement().addJsListener( 'changeMedia', gChangeMedia); + + window[ gDoneName ] = function(){ + if( $.isFunction( doneCallback ) ){ + doneCallback(); + doneCallback = null; + } + }; + _this.getPlayerElement().removeJsListener('playerPlayEnd', gDoneName); + _this.getPlayerElement().addJsListener( 'playerPlayEnd', gDoneName); + }; + // Remove then add the event: + _this.getPlayerElement().removeJsListener( 'playerReady', gPlayerReady ); + _this.getPlayerElement().addJsListener( 'playerReady', gPlayerReady ); + }); + }, + + /** + * Issues a seek to the playerElement + * + * @param {Float} + * percentage Percentage of total stream length to seek to + */ + seek : function(percentage) { + var _this = this; + var seekTime = percentage * this.getDuration(); + mw.log( 'EmbedPlayerKalturaKplayer:: seek: ' + percentage + ' time:' + seekTime ); + if (this.supportsURLTimeEncoding()) { + + // Make sure we could not do a local seek instead: + if (!(percentage < this.bufferedPercent + && this.playerElement.duration && !this.didSeekJump)) { + // We support URLTimeEncoding call parent seek: + this.parent_seek( percentage ); + return; + } + } + // Add a seeked callback event: + var seekedCallback = 'kdp_seek_' + this.id + '_' + new Date().getTime(); + window[ seekedCallback ] = function(){ + _this.seeking = false; + _this.triggerHelper( 'seeked' ); + if( seekInterval ) { + clearInterval( seekInterval ); + } + }; + this.playerElement.addJsListener('playerSeekEnd', seekedCallback ); + + if ( this.getPlayerElement() ) { + // trigger the html5 event: + _this.triggerHelper( 'seeking' ); + + // Issue the seek to the flash player: + this.playerElement.sendNotification('doSeek', seekTime); + + // Include a fallback seek timer: in case the kdp does not fire 'playerSeekEnd' + var orgTime = this.flashCurrentTime; + var seekInterval = setInterval( function(){ + if( _this.flashCurrentTime != orgTime ){ + _this.seeking = false; + clearInterval( seekInterval ); + this.triggerHelper( 'seeked' ); + } + }, mw.config.get( 'EmbedPlayer.MonitorRate' ) ); + + } else { + // try to do a play then seek: + this.doPlayThenSeek(percentage); + } + + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + }, + + /** + * Seek in a existing stream + * + * @param {Float} + * percentage Percentage of the stream to seek to between 0 and 1 + */ + doPlayThenSeek : function(percentage) { + mw.log('EmbedPlayerKplayer::doPlayThenSeek::'); + var _this = this; + // issue the play request + this.play(); + + // let the player know we are seeking + _this.seeking = true; + this.triggerHelper( 'seeking' ); + + var getPlayerCount = 0; + var readyForSeek = function() { + _this.getPlayerElement(); + // if we have duration then we are ready to do the seek ( flash can't + // seek untill there is some buffer ) + if (_this.playerElement && _this.playerElement.sendNotification + && _this.getDuration() && _this.bufferedPercent) { + var seekTime = percentage * _this.getDuration(); + // Issue the seek to the flash player: + _this.playerElement.sendNotification('doSeek', seekTime); + } else { + // Try to get player for 20 seconds: + if (getPlayerCount < 400) { + setTimeout(readyForSeek, 50); + getPlayerCount++; + } else { + mw.log('Error: doPlayThenSeek failed'); + } + } + }; + readyForSeek(); + }, + + /** + * Issues a volume update to the playerElement + * + * @param {Float} + * percentage Percentage to update volume to + */ + setPlayerElementVolume : function(percentage) { + if ( this.getPlayerElement() && this.playerElement.sendNotification ) { + this.playerElement.sendNotification('changeVolume', percentage); + } + }, + + /** + * function called by flash at set interval to update the playhead. + */ + onUpdatePlayhead : function( playheadValue ) { + //mw.log('Update play head::' + playheadValue); + this.flashCurrentTime = playheadValue; + }, + + /** + * function called by flash when the total media size changes + */ + onBytesTotalChange : function(data, id) { + this.bytesTotal = data.newValue; + }, + + /** + * function called by flash applet when download bytes changes + */ + onBytesDownloadedChange : function(data, id) { + //mw.log('onBytesDownloadedChange'); + this.bytesLoaded = data.newValue; + this.bufferedPercent = this.bytesLoaded / this.bytesTotal; + + // Fire the parent html5 action + this.triggerHelper('progress', { + 'loaded' : this.bytesLoaded, + 'total' : this.bytesTotal + }); + }, + + /** + * Get the embed player time + */ + getPlayerElementTime : function() { + // update currentTime + return this.flashCurrentTime; + }, + + /** + * Get the embed fla object player Element + */ + getPlayerElement : function() { + this.playerElement = document.getElementById( this.pid ); + return this.playerElement; + } +}; + +} )( mediaWiki, jQuery ); + +/* + * jQuery Tools 1.2.5 - The missing UI library for the Web + * + * [toolbox.flashembed] + * + * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE. + * + * http://flowplayer.org/tools/ + * + * File generated: Fri Oct 22 13:51:38 GMT 2010 + */ +(function(){function f(a,b){if(b)for(var c in b)if(b.hasOwnProperty(c))a[c]=b[c];return a}function l(a,b){var c=[];for(var d in a)if(a.hasOwnProperty(d))c[d]=b(a[d]);return c}function m(a,b,c){if(e.isSupported(b.version))a.innerHTML=e.getHTML(b,c);else if(b.expressInstall&&e.isSupported([6,65]))a.innerHTML=e.getHTML(f(b,{src:b.expressInstall}),{MMredirectURL:location.href,MMplayerType:"PlugIn",MMdoctitle:document.title});else{if(!a.innerHTML.replace(/\s/g,"")){a.innerHTML="

Flash version "+b.version+ +" or greater is required

"+(g[0]>0?"Your version is "+g:"You have no flash plugin installed")+"

"+(a.tagName=="A"?"

Click here to download latest version

":"

Download latest version from here

");if(a.tagName=="A")a.onclick=function(){location.href=k}}if(b.onFail){var d=b.onFail.call(this);if(typeof d=="string")a.innerHTML=d}}if(i)window[b.id]=document.getElementById(b.id);f(this,{getRoot:function(){return a},getOptions:function(){return b},getConf:function(){return c}, +getApi:function(){return a.firstChild}})}var i=document.all,k="http://www.adobe.com/go/getflashplayer",n=typeof jQuery=="function",o=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,j={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};window.attachEvent&&window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}}); +window.flashembed=function(a,b,c){if(typeof a=="string")a=document.getElementById(a.replace("#",""));if(a){if(typeof b=="string")b={src:b};return new m(a,f(f({},j),b),c)}};var e=f(window.flashembed,{conf:j,getVersion:function(){var a,b;try{b=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(c){try{b=(a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"))&&a.GetVariable("$version")}catch(d){try{b=(a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"))&&a.GetVariable("$version")}catch(h){}}}return(b= +o.exec(b))?[b[1],b[3]]:[0,0]},asString:function(a){if(a===null||a===undefined)return null;var b=typeof a;if(b=="object"&&a.push)b="array";switch(b){case "string":a=a.replace(new RegExp('(["\\\\])',"g"),"\\$1");a=a.replace(/^\s?(\d+\.?\d+)%/,"$1pct");return'"'+a+'"';case "array":return"["+l(a,function(d){return e.asString(d)}).join(",")+"]";case "function":return'"function()"';case "object":b=[];for(var c in a)a.hasOwnProperty(c)&&b.push('"'+c+'":'+e.asString(a[c]));return"{"+b.join(",")+"}"}return String(a).replace(/\s/g, +" ").replace(/\'/g,'"')},getHTML:function(a,b){a=f({},a);var c='';a.width=a.height=a.id=a.w3c=a.src=null;a.onFail=a.version=a.expressInstall=null;for(var d in a)if(a[d])c+= +'';a="";if(b){for(var h in b)if(b[h]){d=b[h];a+=h+"="+(/function|object/.test(typeof d)?e.asString(d):d)+"&"}a=a.slice(0,-1);c+='"}c+="";return c},isSupported:function(a){return g[0]>a[0]||g[0]==a[0]&&g[1]>=a[1]}}),g=e.getVersion();if(n){jQuery.tools=jQuery.tools||{version:"1.2.5"};jQuery.tools.flashembed={conf:j};jQuery.fn.flashembed=function(a,b){return this.each(function(){$(this).data("flashembed",flashembed(this, +a,b))})}}})(); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js new file mode 100644 index 00000000..8c514f70 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js @@ -0,0 +1,1088 @@ +/** +* Native embed library: +* +* Enables embedPlayer support for native html5 browser playback system +*/ +( function( mw, $ ) { "use strict"; + +mw.EmbedPlayerNative = { + + //Instance Name + instanceOf: 'Native', + + // Flag to only load the video ( not play it ) + onlyLoadFlag:false, + + //Callback fired once video is "loaded" + onLoadedCallback: null, + + // The previous "currentTime" to sniff seek actions + // NOTE the bug where onSeeked does not seem fire consistently may no longer be applicable + prevCurrentTime: -1, + + // Store the progress event ( updated during monitor ) + progressEventData: null, + + // If the media loaded event has been fired + mediaLoadedFlag: null, + + // A flag to keep the video tag offscreen. + keepPlayerOffScreenFlag: null, + + // A flag to designate the first play event, as to not propagate the native event in this case + isFirstEmbedPlay: null, + + // A local var to store the current seek target time: + currentSeekTargetTime: null, + + // All the native events per: + // http://www.w3.org/TR/html5/video.html#mediaevents + nativeEvents : [ + 'loadstart', + 'progress', + 'suspend', + 'abort', + 'error', + 'emptied', + 'stalled', + 'play', + 'pause', + 'loadedmetadata', + 'loadeddata', + 'waiting', + 'playing', + 'canplay', + 'canplaythrough', + 'seeking', + 'seeked', + 'timeupdate', + 'ended', + 'ratechange', + 'durationchange', + 'volumechange' + ], + + // Native player supported feature set + supports: { + 'playHead' : true, + 'pause' : true, + 'fullscreen' : true, + 'sourceSwitch': true, + 'timeDisplay' : true, + 'volumeControl' : true, + 'overlays' : true + }, + /** + * Updates the supported features given the "type of player" + */ + updateFeatureSupport: function(){ + // The native controls function checks for overly support + // especially the special case of iPad in-dom or not support + if( this.useNativePlayerControls() ) { + this.supports.overlays = false; + this.supports.volumeControl = false; + } + // iOS does not support volume control + if( mw.isIpad() ){ + this.supports.volumeControl = false; + } + // Check if we already have a selected source and a player in the page, + if( this.getPlayerElement() && this.getSrc() ){ + $( this.getPlayerElement() ).attr( 'src', this.getSrc() ); + } + // Check if we already have a video element an apply bindings ( for native interfaces ) + if( this.getPlayerElement() ){ + this.applyMediaElementBindings(); + } + + this.parent_updateFeatureSupport(); + }, + /** + * Adds an HTML screen and moves the video tag off screen, works around some iPhone bugs + */ + addPlayScreenWithNativeOffScreen: function(){ + var _this = this; + // Hide the player offscreen: + this.hidePlayerOffScreen(); + this.keepPlayerOffScreenFlag = true; + + // Add a play button on the native player: + this.addLargePlayBtn(); + + // Add a binding to show loader once clicked to show the loader + // bad ui to leave the play button displayed + this.$interface.find( '.play-btn-large' ).click( function(){ + _this.$interface.find( '.play-btn-large' ).hide(); + _this.addPlayerSpinner(); + _this.hideSpinnerOncePlaying(); + }); + + // Add an image poster: + var posterSrc = ( this.poster ) ? this.poster : + mw.config.get( 'EmbedPlayer.BlackPixel' ); + + // Check if the poster is already present: + if( $( this ).find( '.playerPoster' ).length ){ + $( this ).find( '.playerPoster' ).attr('src', posterSrc ); + } else { + $( this ).append( + $('').css({ + 'margin' : '0', + 'width': '100%', + 'height': '100%' + }) + .attr( 'src', posterSrc) + .addClass('playerPoster') + ) + } + $( this ).show(); + }, + /** + * Return the embed code + */ + embedPlayerHTML : function () { + var _this = this; + var vid = _this.getPlayerElement(); + this.isFirstEmbedPlay = true; + + // Check if we should have a play button on the native player: + if( this.useLargePlayBtn() ){ + this.addLargePlayBtn(); + } + + if( vid && $( vid ).attr('src') == this.getSrc( this.currentTime ) ){ + _this.postEmbedActions(); + return ; + } + mw.log( "EmbedPlayerNative::embedPlayerHTML > play url:" + this.getSrc( this.currentTime ) + ' startOffset: ' + this.start_ntp + ' end: ' + this.end_ntp ); + + // Check if using native controls and already the "pid" is already in the DOM + if( this.isPersistentNativePlayer() && vid ) { + _this.postEmbedActions(); + return ; + } + // Reset some play state flags: + _this.bufferStartFlag = false; + _this.bufferEndFlag = false; + + $( this ).html( + _this.getNativePlayerHtml() + ); + + // Directly run postEmbedActions ( if playerElement is not available it will retry ) + _this.postEmbedActions(); + }, + + /** + * Get the native player embed code. + * + * @param {object} playerAttributes Attributes to be override in function call + * @return {object} cssSet css to apply to the player + */ + getNativePlayerHtml: function( playerAttributes, cssSet ){ + if( !playerAttributes) { + playerAttributes = {}; + } + // Update required attributes + if( !playerAttributes['id'] ){ + playerAttributes['id'] = this.pid; + } + if( !playerAttributes['src'] ){ + playerAttributes['src'] = this.getSrc( this.currentTime); + } + + // If autoplay pass along to attribute ( needed for iPad / iPod no js autoplay support + if( this.autoplay ) { + playerAttributes['autoplay'] = 'true'; + } + + if( !cssSet ){ + cssSet = {}; + } + + // Set default width height to 100% of parent container + if( !cssSet['width'] ) cssSet['width'] = '100%'; + if( !cssSet['height'] ) cssSet['height'] = '100%'; + + // Also need to set the loop param directly for iPad / iPod + if( this.loop ) { + playerAttributes['loop'] = 'true'; + } + + var tagName = this.isAudio() ? 'audio' : 'video'; + + return $( '<' + tagName + ' />' ) + // Add the special nativeEmbedPlayer to avoid any rewrites of of this video tag. + .addClass( 'nativeEmbedPlayerPid' ) + .attr( playerAttributes ) + .css( cssSet ) + }, + + /** + * Post element javascript, binds event listeners and starts monitor + */ + postEmbedActions: function() { + var _this = this; + // Setup local pointer: + var vid = this.getPlayerElement(); + if( !vid ){ + return ; + } + // Update the player source ( if needed ) + if( $( vid).attr( 'src' ) != this.getSrc( this.currentTime ) ){ + $( vid ).attr( 'src', this.getSrc( this.currentTime ) ); + } + // Update the WebKitPlaysInline value + if( mw.config.get( 'EmbedPlayer.WebKitPlaysInline') ){ + $( vid ).attr( 'webkit-playsinline', 1 ); + } + // Update the EmbedPlayer.WebKitAllowAirplay option: + if( mw.config.get( 'EmbedPlayer.WebKitAllowAirplay' ) ){ + $( vid ).attr( 'x-webkit-airplay', "allow" ); + } + // make sure to display native controls if enabled: + if( this.useNativePlayerControls() ){ + $( vid ).attr( 'controls', "true" ); + } + + // Apply media element bindings: + _this.applyMediaElementBindings(); + + // Make sure we start playing in the correct place: + if( this.currentTime != vid.currentTime ){ + var waitReadyStateCount = 0; + var checkReadyState = function(){ + if( vid.readyState > 0 ){ + vid.currentTime = this.currentTime; + return ; + } + if( waitReadyStateCount > 1000 ){ + mw.log("Error: EmbedPlayerNative: could not run native seek"); + return ; + } + waitReadyStateCount++; + setTimeout( function() { + checkReadyState(); + }, 10 ); + }; + } + // Some mobile devices ( iOS need a load call before play will work ) + if ( !_this.loop ) { + vid.load(); + } + }, + /** + * Apply media element bindings + */ + applyMediaElementBindings: function(){ + var _this = this; + mw.log("EmbedPlayerNative::MediaElementBindings"); + var vid = this.getPlayerElement(); + if( ! vid ){ + mw.log( " Error: applyMediaElementBindings without player elemnet"); + return ; + } + $.each( _this.nativeEvents, function( inx, eventName ){ + $( vid ).unbind( eventName + '.embedPlayerNative').bind( eventName + '.embedPlayerNative', function(){ + if( _this._propagateEvents ){ + var argArray = $.makeArray( arguments ); + // always pass the current ref id as the last argument + // helps check against some event trigger ref issues in jQuery + argArray.push( _this.id ); + // Check if there is local handler: + if( _this[ '_on' + eventName ] ){ + _this[ '_on' + eventName ].apply( _this, argArray); + } else { + // No local handler directly propagate the event to the abstract object: + $( _this ).trigger( eventName, argArray ); + } + } + }); + }); + }, + + // basic monitor function to update buffer + monitor: function(){ + var _this = this; + var vid = _this.getPlayerElement(); + + // Update the bufferedPercent + if( vid && vid.buffered && vid.buffered.end && vid.duration ) { + try{ + this.bufferedPercent = ( vid.buffered.end(0) / vid.duration ); + } catch ( e ){ + // opera does not have buffered.end zero index support ? + } + } + _this.parent_monitor(); + }, + + + /** + * Issue a seeking request. + * + * @param {Float} percent + * @param {bollean} stopAfterSeek if the player should stop after the seek + */ + seek: function( percent, stopAfterSeek ) { + // bounds check + if( percent < 0 ){ + percent = 0; + } + + if( percent > 1 ){ + percent = 1; + } + mw.log( 'EmbedPlayerNative::seek p: ' + percent + ' : ' + this.supportsURLTimeEncoding() + ' dur: ' + this.getDuration() + ' sts:' + this.seekTimeSec ); + + // Trigger preSeek event for plugins that want to store pre seek conditions. + this.triggerHelper( 'preSeek', percent ); + + this.seeking = true; + // Update the current time ( local property ) + this.currentTime = ( percent * this.duration ).toFixed( 2 ) ; + + // trigger the seeking event: + mw.log( 'EmbedPlayerNative::seek:trigger' ); + this.triggerHelper( 'seeking' ); + + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + + // @@todo check if the clip is loaded here (if so we can do a local seek) + if ( this.supportsURLTimeEncoding() ) { + // Make sure we could not do a local seek instead: + if ( percent < this.bufferedPercent && this.playerElement.duration && !this.didSeekJump ) { + mw.log( "EmbedPlayerNative::seek local seek " + percent + ' is already buffered < ' + this.bufferedPercent ); + this.doNativeSeek( percent ); + } else { + // We support URLTimeEncoding call parent seek: + this.parent_seek( percent ); + } + } else { + // Try to do a play then seek: + this.doNativeSeek( percent ); + } + }, + + /** + * Do a native seek by updating the currentTime + * @param {float} percent + * Percent to seek to of full time + */ + doNativeSeek: function( percent, callback ) { + // If player already seeking, exit + var _this = this; + // chrome crashes with multiple seeks: + if( (navigator.userAgent.indexOf('Chrome') === -1) && _this.playerElement.seeking ) { + return ; + } + + mw.log( 'EmbedPlayerNative::doNativeSeek::' + percent ); + this.seeking = true; + + this.seekTimeSec = 0; + + // Hide iPad video off screen ( iOS shows quicktime logo during seek ) + if( mw.isIOS() ){ + this.hidePlayerOffScreen(); + } + + this.setCurrentTime( ( percent * this.duration ) , function(){ + // Update the current time ( so that there is not a monitor delay in reflecting "seeked time" ) + _this.currentTime = _this.getPlayerElement().currentTime; + // Done seeking ( should be a fallback trigger event ) : + if( _this.seeking ){ + _this.seeking = false; + $( _this ).trigger( 'seeked' ); + } + // restore iPad video position: + _this.restorePlayerOnScreen(); + + _this.monitor(); + // issue the callback: + if( callback ){ + callback(); + } + }); + }, + + /** + * Seek in a existing stream, we first play then seek to work around issues with iPad seeking. + * + * @param {Float} percent + * percent of the stream to seek to between 0 and 1 + */ + doPlayThenSeek: function( percent ) { + mw.log( 'EmbedPlayerNative::doPlayThenSeek::' + percent + ' isPaused ' + this.paused); + var _this = this; + var oldPauseState = this.paused; + this.play(); + var retryCount = 0; + var readyForSeek = function() { + _this.getPlayerElement(); + // If we have duration then we are ready to do the seek + if ( _this.playerElement && _this.playerElement.duration ) { + _this.doNativeSeek( percent, function(){ + // restore pause if paused: + if( oldPauseState ){ + _this.pause(); + } + } ); + } else { + // Try to get player for 30 seconds: + // (it would be nice if the onmetadata type callbacks where fired consistently) + if ( retryCount < 800 ) { + setTimeout( readyForSeek, 10 ); + retryCount++; + } else { + mw.log( 'EmbedPlayerNative:: Error: doPlayThenSeek failed :' + _this.playerElement.duration); + } + } + }; + readyForSeek(); + }, + + /** + * Set the current time with a callback + * + * @param {Float} position + * Seconds to set the time to + * @param {Function} callback + * Function called once time has been set. + */ + setCurrentTime: function( seekTime , callback, callbackCount ) { + var _this = this; + if( !callbackCount ){ + callbackCount = 0; + } + mw.log( "EmbedPlayerNative:: setCurrentTime seekTime:" + seekTime + ' count:' + callbackCount ); + + // Make sure all the timeouts don't seek to an expired target: + $( this ).data('currentSeekTarget', seekTime ); + + var vid = this.getPlayerElement(); + // add a callback handler to null out callback: + var callbackHandler = function(){ + if( $.isFunction( callback ) ){ + callback(); + callback = null; + } + } + if( !vid ) { + callbackHandler(); + _this.currentSeekTargetTime = seekTime.toFixed( 2 ); + return; + } + // Check if player is ready for seek: + if( vid.readyState < 1 ){ + // Try to seek for 4 seconds: + if( callbackCount >= 40 ){ + mw.log("Error:: EmbedPlayerNative: with seek request, media never in ready state"); + callbackHandler(); + return ; + } + setTimeout( function(){ + // Check that this seek did not expire: + if( $( _this ).data('currentSeekTarget') != seekTime ){ + mw.log("EmbedPlayerNative:: expired seek target"); + return ; + } + _this.setCurrentTime( seekTime, callback , callbackCount+1); + }, 100 ); + return ; + } + // Check if currentTime is already set to the seek target: + if( vid.currentTime.toFixed(2) == seekTime.toFixed(2) ){ + mw.log("EmbedPlayerNative:: setCurrentTime: current time matches seek target: " + + vid.currentTime.toFixed(2) + ' == ' + seekTime.toFixed(2) ); + callbackHandler(); + return; + } + // setup a namespaced seek bind: + var seekBind = 'seeked.nativeSeekBind'; + + // Remove any old listeners + $( vid ).unbind( seekBind ); + // Bind a seeked listener for the callback + $( vid ).bind( seekBind, function( event ) { + // Remove the listener: + $( vid ).unbind( seekBind ); + + // Check if seeking to zero: + if( seekTime == 0 && vid.currentTime == 0 ){ + callbackHandler(); + return ; + } + + // Check if we got a valid seek: + if( vid.currentTime > 0 ){ + callbackHandler(); + } else { + mw.log( "Error:: EmbedPlayerNative: seek callback without time updatet " + vid.currentTime ); + } + }); + setTimeout(function(){ + // Check that this seek did not expire: + if( $( _this ).data('currentSeekTarget') != seekTime ){ + mw.log("EmbedPlayerNative:: Expired seek target"); + return ; + } + + if( $.isFunction( callback ) ){ + // if seek is within 5 seconds of the target assume success. ( key frame intervals can mess with seek accuracy ) + // this only runs where the seek callback failed ( i.e broken html5 seek ? ) + if( Math.abs( vid.currentTime - seekTime ) < 5 ){ + mw.log( "EmbedPlayerNative:: Seek time is within 5 seconds of target, sucessfull seek"); + callback(); + } else { + mw.log( "Error:: EmbedPlayerNative: Seek still has not made a callback after 5 seconds, retry"); + _this.setCurrentTime( seekTime, callback , callbackCount++ ); + } + } + }, 5000); + + // Try to update the playerElement time: + try { + _this.currentSeekTargetTime = seekTime.toFixed( 2 ); + // use toFixed ( iOS issue with float seek times ) + vid.currentTime = _this.currentSeekTargetTime; + } catch ( e ) { + mw.log("Error:: EmbedPlayerNative: Could not set video tag seekTime"); + callbackHandler(); + return ; + } + + // Check for seeking state ( some player iOS / iPad can only seek while playing ) + if(! vid.seeking ){ + mw.log( "Error:: not entering seek state, play and wait for positive time" ); + vid.play(); + setTimeout(function(){ + _this.waitForPositiveCurrentTime( function(){ + mw.log("EmbedPlayerNative:: Got possitive time:" + vid.currentTime.toFixed(3) + ", trying to seek again"); + _this.setCurrentTime( seekTime , callback, callbackCount+1 ); + }); + }, mw.config.get( 'EmbedPlayer.MonitorRate' ) ); + } + }, + waitForPositiveCurrentTime: function( callback ){ + var _this = this; + var vid = this.getPlayerElement(); + this.waitForPositiveCurrentTimeCount++; + // Wait for playback for 10 seconds + if( vid.currentTime > 0 ){ + mw.log( 'EmbedPlayerNative:: waitForPositiveCurrentTime success' ); + callback(); + } else if( this.waitForPositiveCurrentTimeCount > 200 ){ + mw.log( "Error:: waitForPositiveCurrentTime failed to reach possitve time"); + callback(); + } else { + setTimeout(function(){ _this.waitForPositiveCurrentTime( callback ) }, 50 ) + } + }, + /** + * Get the embed player time + */ + getPlayerElementTime: function() { + var _this = this; + // Make sure we have .vid obj + this.getPlayerElement(); + if ( !this.playerElement ) { + mw.log( 'EmbedPlayerNative::getPlayerElementTime: ' + this.id + ' not in dom ( stop monitor)' ); + this.stop(); + return false; + } + var ct = this.playerElement.currentTime; + // Return 0 or a positive number: + if( ! ct || isNaN( ct ) || ct < 0 || ! isFinite( ct ) ){ + return 0; + } + // Return the playerElement currentTime + return this.playerElement.currentTime; + }, + + // Update the poster src ( updates the native object if in dom ) + updatePosterSrc: function( src ){ + if( this.getPlayerElement() ){ + $( this.getPlayerElement() ).attr('poster', src ); + } + // Also update the embedPlayer poster + this.parent_updatePosterSrc( src ); + }, + /** + * Empty player sources from the active video tag element + */ + emptySources: function(){ + // empty player source: + $( this.getPlayerElement() ).attr( 'src', null ); + // empty out generic sources: + this.parent_emptySources(); + }, + /** + * playerSwitchSource switches the player source working around a few bugs in browsers + * + * @param {Object} + * Source object to switch to. + * @param {function} + * switchCallback Function to call once the source has been switched + * @param {function} + * doneCallback Function to call once the clip has completed playback + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + var _this = this; + var src = source.getSrc(); + var vid = this.getPlayerElement(); + var switchBindPostfix = '.playerSwitchSource'; + this.isPauseLoading = false; + // Make sure the switch source is different: + if( !src || src == vid.src ){ + if( $.isFunction( switchCallback ) ){ + switchCallback( vid ); + } + // Delay done callback to allow any non-blocking switch callback code to fully execute + if( $.isFunction( doneCallback ) ){ + doneCallback(); + } + return ; + } + + // only display switch msg if actually switching: + mw.log( 'EmbedPlayerNative:: playerSwitchSource: ' + src + ' native time: ' + vid.currentTime ); + + // Update some parent embedPlayer vars: + this.currentTime = 0; + this.previousTime = 0; + if ( vid ) { + try { + // Remove all switch player bindings + $( vid ).unbind( switchBindPostfix ); + + // pause before switching source + vid.pause(); + + var orginalControlsState = vid.controls; + // Hide controls ( to not display native play button while switching sources ) + vid.removeAttribute('controls'); + + // dissable seeking ( if we were in a seeking state before the switch ) + _this.seeking = false; + + // add a loading indicator: + _this.addPlayerSpinner(); + + // Do the actual source switch: + vid.src = src; + // load the updated src + vid.load(); + + // hide the player offscreen while we switch + _this.hidePlayerOffScreen(); + // restore position once we have metadata + $( vid ).bind( 'loadedmetadata' + switchBindPostfix, function(){ + $( vid ).unbind( 'loadedmetadata' + switchBindPostfix); + mw.log("EmbedPlayerNative:: playerSwitchSource> loadedmetadata callback for:" + src + ' switchCallback: ' + switchCallback ); + // Only update duration if we didn't get it server side + // Some browsers report bad duration (e.g. Android native browser) + // So avoid using the browser detected value if possible. + if ( !_this.duration && isFinite( vid.duration ) ) { + _this.duration = vid.duration; + } + // keep going towards playback! if switchCallback has not been called yet + // we need the "playing" event to trigger the switch callback + if ( $.isFunction( switchCallback ) ){ + vid.play(); + } + }); + + var handleSwitchCallback = function(){ + // restore video position ( now that we are playing with metadata size ) + _this.restorePlayerOnScreen(); + // play hide loading spinner: + _this.hideSpinnerAndPlayBtn(); + // Restore + vid.controls = orginalControlsState; + // check if we have a switch callback and issue it now: + if ( $.isFunction( switchCallback ) ){ + switchCallback( vid ); + switchCallback = null; + } + } + + // once playing issue callbacks: + $( vid ).bind( 'playing' + switchBindPostfix, function(){ + $( vid ).unbind( 'playing' + switchBindPostfix ); + mw.log("EmbedPlayerNative:: playerSwitchSource> playing callback"); + handleSwitchCallback(); + }); + + // Add the end binding if we have a post event: + if( $.isFunction( doneCallback ) ){ + $( vid ).bind( 'ended' + switchBindPostfix , function( event ) { + // remove end binding: + $( vid ).unbind( switchBindPostfix ); + // issue the doneCallback + doneCallback(); + + // Support loop for older iOS + // Temporarly disabled pending more testing or refactor into a better place. + //if ( _this.loop ) { + // vid.play(); + //} + return false; + }); + } + + // issue the play request: + vid.play(); + + // check if ready state is loading or doing anything ( iOS play restriction ) + // give iOS 5 seconds to ~start~ loading media + setTimeout(function(){ + // Check that the player got out of readyState 0 + if( vid.readyState === 0 && $.isFunction( switchCallback ) ){ + mw.log("EmbedPlayerNative:: possible iOS play without gesture failed, issue callback"); + // hand off to the swtich callback method. + handleSwitchCallback(); + // make sure we are in a pause state ( failed to change and play media ); + _this.pause(); + // show the big play button so the user can give us a user gesture: + if( ! _this.useLargePlayBtn() ){ + _this.addLargePlayBtn(); + } + } + }, 5000 ); + + + } catch (e) { + mw.log("Error: EmbedPlayerNative Error in switching source playback"); + } + } + }, + hidePlayerOffScreen:function( vid ){ + var vid = this.getPlayerElement(); + // Move the video offscreen while it switches ( hides quicktime logo only applies to iPad ) + $( vid ).css( { + 'position' : 'absolute', + 'left': '-4048px' + }); + }, + restorePlayerOnScreen: function( vid ){ + var vid = this.getPlayerElement(); + if( this.keepPlayerOffScreenFlag ){ + return ; + } + + // Remove any poster div ( that would overlay the player ) + $( this ).find( '.playerPoster' ).remove(); + // Restore video pos before calling sync syze + $( vid ).css( { + 'left': '0px' + }); + }, + /** + * Pause the video playback + * calls parent_pause to update the interface + */ + pause: function( ) { + this.getPlayerElement(); + this.parent_pause(); // update interface + if ( this.playerElement ) { // update player + this.playerElement.pause(); + } + }, + + /** + * Play back the video stream + * calls parent_play to update the interface + */ + play: function() { + var _this = this; + // if starting playback from stoped state and not in an ad or otherise blocked controls state: + // restore player: + if( this.isStopped() && this._playContorls ){ + this.restorePlayerOnScreen(); + } + // Run parent play: + if( _this.parent_play() ){ + if ( this.getPlayerElement() && this.getPlayerElement().play ) { + mw.log( "EmbedPlayerNative:: issue native play call" ); + // If in pauseloading state make sure the loading spinner is present: + if( this.isPauseLoading ){ + this.hideSpinnerOncePlaying(); + } + // issue a play request + this.getPlayerElement().play(); + // re-start the monitor: + this.monitor(); + } + } else { + mw.log( "EmbedPlayerNative:: parent play returned false, don't issue play on native element"); + } + }, + + /** + * Stop the player ( end all listeners ) + */ + stop: function(){ + var _this = this; + if( this.playerElement && this.playerElement.currentTime){ + this.playerElement.currentTime = 0; + this.playerElement.pause(); + } + this.parent_stop(); + }, + + /** + * Toggle the Mute + * calls parent_toggleMute to update the interface + */ + toggleMute: function() { + this.parent_toggleMute(); + this.getPlayerElement(); + if ( this.playerElement ) + this.playerElement.muted = this.muted; + }, + + /** + * Update Volume + * + * @param {Float} percent Value between 0 and 1 to set audio volume + */ + setPlayerElementVolume : function( percent ) { + if ( this.getPlayerElement() ) { + // Disable mute if positive volume + if( percent != 0 ) { + this.playerElement.muted = false; + } + this.playerElement.volume = percent; + } + }, + + /** + * get Volume + * + * @return {Float} + * Audio volume between 0 and 1. + */ + getPlayerElementVolume: function() { + if ( this.getPlayerElement() ) { + return this.playerElement.volume; + } + }, + /** + * get the native muted state + */ + getPlayerElementMuted: function(){ + if ( this.getPlayerElement() ) { + return this.playerElement.muted; + } + }, + + /** + * Get the native media duration + */ + getNativeDuration: function() { + if ( this.playerElement ) { + return this.playerElement.duration; + } + }, + + /** + * Load the video stream with a callback fired once the video is "loaded" + * + * @parma {Function} callbcak Function called once video is loaded + */ + load: function( callback ) { + this.getPlayerElement(); + if ( !this.playerElement ) { + // No vid loaded + mw.log( 'EmbedPlayerNative::load() ... doEmbed' ); + this.onlyLoadFlag = true; + this.embedPlayerHTML(); + this.onLoadedCallback = callback; + } else { + // Should not happen offten + this.playerElement.load(); + if( callback ){ + callback(); + } + } + }, + + /** + * Get /update the playerElement value + */ + getPlayerElement: function () { + this.playerElement = $( '#' + this.pid ).get( 0 ); + return this.playerElement; + }, + + /** + * Bindings for the Video Element Events + */ + + /** + * Local method for seeking event + * fired when "seeking" + */ + _onseeking: function() { + mw.log( "EmbedPlayerNative::onSeeking " + this.seeking + ' new time: ' + this.getPlayerElement().currentTime ); + if( this.seeking && Math.round( this.getPlayerElement().currentTime - this.currentSeekTargetTime ) > 2 ){ + mw.log( "Error:: EmbedPlayerNative Seek time missmatch: target:" + this.getPlayerElement().currentTime + + ' actual ' + this.currentSeekTargetTime + ', note apple HLS can only seek to 10 second targets'); + } + // Trigger the html5 seeking event + //( if not already set from interface ) + if( !this.seeking ) { + this.currentSeekTargetTime = this.getPlayerElement().currentTime; + this.seeking = true; + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + + // Trigger the html5 "seeking" trigger + mw.log("EmbedPlayerNative::seeking:trigger:: " + this.seeking); + if( this._propagateEvents ){ + this.triggerHelper( 'seeking' ); + } + } + }, + + /** + * Local method for seeked event + * fired when done seeking + */ + _onseeked: function() { + mw.log("EmbedPlayerNative::onSeeked " + this.seeking + ' ct:' + this.playerElement.currentTime ); + // sync the seek checks so that we don't re-issue the seek request + this.previousTime = this.currentTime = this.playerElement.currentTime; + + // Trigger the html5 action on the parent + if( this.seeking ){ + + // HLS safari triggers onseek when its not even close to the target time, + // we don't want to trigger the seek event for these "fake" onseeked triggers + if( Math.abs( this.currentSeekTargetTime - this.getPlayerElement().currentTime ) > 2 ){ + mw.log( "Error:: EmbedPlayerNative:seeked triggred with time mismatch: target:" + + this.currentSeekTargetTime + + ' actual:' + this.getPlayerElement().currentTime ); + return ; + } + this.seeking = false; + if( this._propagateEvents ){ + mw.log( "EmbedPlayerNative:: trigger: seeked" ); + this.triggerHelper( 'seeked' ); + } + } + this.hideSpinner(); + // update the playhead status + if( this.isStopped() ){ + this.addLargePlayBtn(); + } + this.monitor(); + }, + + /** + * Handle the native paused event + */ + _onpause: function(){ + var _this = this; + var timeSincePlay = Math.abs( this.absoluteStartPlayTime - new Date().getTime() ); + mw.log( "EmbedPlayerNative:: OnPaused:: propagate:" + this._propagateEvents + ' time since play: ' + timeSincePlay + ' isNative=true' ); + // Only trigger parent pause if more than MonitorRate time has gone by. + // Some browsers trigger native pause events when they "play" or after a src switch + if( timeSincePlay > mw.config.get( 'EmbedPlayer.MonitorRate' ) ){ + _this.parent_pause(); + } else { + // continue playback: + this.getPlayerElement().play(); + } + }, + + /** + * Handle the native play event + */ + _onplay: function(){ + mw.log("EmbedPlayerNative:: OnPlay:: propogate:" + this._propagateEvents + ' paused: ' + this.paused); + // if using native controls make sure the inteface does not block the native controls interface: + if( this.useNativePlayerControls() ){ + this.$interface.css('pointer-events', 'none'); + } + + // Update the interface ( if paused ) + if( ! this.isFirstEmbedPlay && this._propagateEvents && this.paused ){ + this.parent_play(); + } else { + // make sure the interface reflects the current play state if not calling parent_play() + this.playInterfaceUpdate(); + } + // Set firstEmbedPlay state to false to avoid initial play invocation : + this.isFirstEmbedPlay = false; + }, + + /** + * Local method for metadata ready + * fired when metadata becomes available + * + * Used to update the media duration to + * accurately reflect the src duration + */ + _onloadedmetadata: function() { + this.getPlayerElement(); + + if ( this.playerElement && !isNaN( this.playerElement.duration ) && isFinite( this.playerElement.duration) ) { + mw.log( 'EmbedPlayerNative :onloadedmetadata metadata ready Update duration:' + this.playerElement.duration + ' old dur: ' + this.getDuration() ); + // Only update duration if we didn't get it server side + // Some browsers report bad duration (e.g. Android native browser) + // So avoid using the browser detected value if possible. + if( !this.duration && this.playerElement && isFinite( this.playerElement.duration ) ) { + this.duration = this.playerElement.duration; + } + } + + // Check if in "playing" state and we are _propagateEvents events and continue to playback: + if( !this.paused && this._propagateEvents ){ + this.getPlayerElement().play(); + } + + //Fire "onLoaded" flags if set + if( typeof this.onLoadedCallback == 'function' ) { + this.onLoadedCallback(); + } + + // Trigger "media loaded" + if( ! this.mediaLoadedFlag ){ + $( this ).trigger( 'mediaLoaded' ); + this.mediaLoadedFlag = true; + } + }, + + /** + * Local method for end of media event + */ + _onended: function( event ) { + var _this = this; + if( this.getPlayerElement() ){ + mw.log( 'EmbedPlayer:native: onended:' + this.playerElement.currentTime + ' real dur:' + this.getDuration() + ' ended ' + this._propagateEvents ); + if( this._propagateEvents ){ + this.onClipDone(); + } + } + }, + /** + * Local onClip done function for native player. + */ + onClipDone: function(){ + var _this = this; + // add clip done binding ( will only run on sequence complete ) + $(this).unbind('onEndedDone.onClipDone').bind( 'onEndedDone.onClipDone', function(){ + _this.addPlayScreenWithNativeOffScreen(); + // if not a legitmate play screen don't keep the player offscreen when playback starts: + if( !_this.isImagePlayScreen() ){ + _this.keepPlayerOffScreenFlag =false; + } + }); + this.parent_onClipDone(); + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js new file mode 100644 index 00000000..791459f3 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js @@ -0,0 +1,222 @@ +( function( mw, $ ) { "use strict"; + +var support = mw.OgvJsSupport; + +mw.EmbedPlayerOgvJs = { + + // Instance name: + instanceOf: 'OgvJs', + + // Supported feature set of the OGVPlayer widget: + supports: { + 'playHead' : true, + 'pause' : true, + 'stop' : true, + 'fullscreen' : true, + 'sourceSwitch': true, + 'timeDisplay' : true, + 'volumeControl' : false, + 'overlays': true, + 'timedText': true + }, + + /** + * Perform setup in response to a play start command. + * This means loading the code asynchronously if needed, + * and enabling web audio for iOS Safari inside the event + * handler. + * + * @return jQuery.Deferred + */ + _ogvJsPreInit: function() { + if( mw.isIOS() ) { + this._initializeAudioForiOS(); + } + return support.loadOgvJs(); + }, + + /** + * Actually initialize the player. + * + * @return OGVPlayer + */ + _ogvJsInit: function() { + var options = {}; + if ( this._iOSAudioContext ) { + // Reuse the audio context we opened earlier + options.audioContext = this._iOSAudioContext; + } + return new OGVPlayer( options ); + }, + + _iOSAudioContext: undefined, + + _initializeAudioForiOS: function() { + // iOS Safari Web Audio API must be initialized from an input event handler + if ( this._iOSAudioContext ) { + return; + } + this._iOSAudioContext = support.initAudioContext(); + }, + + /** + * Output the the embed html + */ + embedPlayerHTML: function (optionalCallback) { + + $( this ) + .empty() + .append( $.createSpinner( { + size: 'large', + type: 'block' + } ) ); + + var _this = this; + if( mw.isIOS() ) { + _this._initializeAudioForiOS(); + } + support.loadOgvJs().done( function() { + + var player = _this._ogvJsInit(); + player.id = _this.pid; + player.style.width = '100%'; + player.style.height = '100%'; + player.src = _this.getSrc(); + if ( _this.getDuration() ) { + player.durationHint = parseFloat( _this.getDuration() ); + } + player.addEventListener('ended', function() { + _this.onClipDone(); + }); + + // simulate timeupdate events, needed for subtitles + // @todo switch this to native timeupdate event when available upstream + var lastTime = 0, + timeupdateInterval = 0.25; + player.addEventListener( 'framecallback', function( event ) { + var player = _this.getPlayerElement(), + now = player ? player.currentTime : lastTime; + // Don't spam time updates on every frame + if ( Math.abs( now - lastTime ) >= timeupdateInterval ) { + lastTime = now; + $( _this ).trigger( 'timeupdate', [event, _this.id] ); + } + }); + + $( _this ).empty().append( player ); + player.play(); + + // Start the monitor: + _this.monitor(); + + if ( optionalCallback ) { + optionalCallback(); + } + }); + }, + + /** + * Get the embed player time + */ + getPlayerElementTime: function() { + this.getPlayerElement(); + var currentTime = 0; + if ( this.playerElement ) { + currentTime = this.playerElement.currentTime; + } else { + mw.log( "EmbedPlayerOgvJs:: Could not find playerElement" ); + } + return currentTime; + }, + + /** + * Update the playerElement instance with a pointer to the embed object + */ + getPlayerElement: function() { + // this.pid is in the form 'pid_mwe_player_'; inherited from mw.EmbedPlayer.js + var $el = $( '#' + this.pid ); + if( !$el.length ) { + return false; + } + this.playerElement = $el.get( 0 ); + return this.playerElement; + }, + + /** + * Issue the doPlay request to the playerElement + * calls parent_play to update interface + */ + play: function() { + this.getPlayerElement(); + this.parent_play(); + if ( this.playerElement ) { + this.playerElement.play(); + // Restart the monitor if on second playthrough + this.monitor(); + } + }, + + /** + * Pause playback + * calls parent_pause to update interface + */ + pause: function() { + this.getPlayerElement(); + // Update the interface + this.parent_pause(); + // Call the pause function if it exists: + if ( this.playerElement ) { + this.playerElement.pause(); + } + }, + + /** + * Switch the source! + * For simplicity we just replace the player here. + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + var _this = this; + var src = source.getSrc(); + var vid = this.getPlayerElement(); + if ( typeof vid.stop !== 'undefined' ) { + vid.stop(); + } + + switchCallback(); + + // Currently have to tear down the player and make a new one + this.embedPlayerHTML( doneCallback ); + }, + + /** + * Seek in the ogg stream + * @param {Float} percentage Percentage to seek into the stream + */ + seek: function( percentage ) { + this.setCurrentTime( percentage * parseFloat( this.getDuration() ) ); + }, + + setCurrentTime: function( time, callback ) { + this.getPlayerElement(); + + if ( this.playerElement ) { + this.playerElement.currentTime = time; + } + + this.currentTime = time; + this.previousTime = time; // prevent weird double-seek. MwEmbedPlyer is weird! + + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + // @todo add proper events upstream + if( this.seeking ){ + this.seeking = false; + $( this ).trigger( 'seeked' ); + } + if ( $.isFunction( callback ) ) { + callback(); + } + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js new file mode 100644 index 00000000..5fa6c6f8 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js @@ -0,0 +1,101 @@ +/** + * Play the video using the vlc app on iOS + */ + +( function( mw, $ ) { "use strict"; + +mw.EmbedPlayerVLCApp = { + // List of supported features (or lack thereof) + supports: { + 'playHead':false, /* The seek slider */ + 'pause':true, /* Play pause button in control bar */ + 'stop':true, /* Does this actually do anything?? */ + 'fullscreen':false, + 'timeDisplay':false, + 'volumeControl':false + }, + + // Instance name: + instanceOf:'VLCApp', + + /* + * Embed this "fake" player + * + * @return {String} + * embed code to link to VLC app + */ + embedPlayerHTML: function() { + var fileUrl = this.getSrc( this.seekTimeSec ), + vlcUrl = 'vlc://' + (new mw.Uri( fileUrl )).toString(), + appStoreUrl = '//itunes.apple.com/us/app/vlc-for-ios/id650377962', + appInstalled = false, + promptInstallTimeout, + $link, + startTime; + + // Replace video with download in vlc link. + // the ends up being not used as we get the html via .html() + $link = $( '' ).append( $( '' ).attr( 'href', appStoreUrl ).append( + mw.html.escape( mw.msg( 'mwe-embedplayer-vlcapp-vlcapplinktext' ) ) + ) ); + $( this ).html( $( '
' ) + .width( this.getWidth() ) + .height( this.getHeight() ) + .append( + // mw.msg doesn't have rawParams() equivalent. Lame. + mw.html.escape( + mw.msg( 'mwe-embedplayer-vlcapp-intro' ) + ).replace( /\$1/g, $link.html() ) + ).append( $( '
    ' ) + .append( $( '
  • ' ).append( $( '' ).attr( 'href', appStoreUrl ) + .text( mw.msg( 'mwe-embedplayer-vlcapp-downloadapp' ) ) ) + ).append( $( '
  • ' ).append( $( '' ).attr( 'href', vlcUrl ) + .text( mw.msg( 'mwe-embedplayer-vlcapp-openvideo' ) ) ) + ).append( $( '
  • ' ).append( $( '' ).attr( 'href', fileUrl ) + .text( mw.msg( 'mwe-embedplayer-vlcapp-downloadvideo' ) ) ) + ) + ) + ); + + // Try to auto-open in vlc. + // Based on http://stackoverflow.com/questions/627916/check-if-url-scheme-is-supported-in-javascript + + $( window ).one( 'pagehide', function() { + appInstalled = true; + if ( promptInstallTimeout ) { + window.clearTimeout( promptInstallTimeout ); + } + } ); + startTime = (new Date).getTime(); + try { + window.location = vlcUrl; + } catch( e ) { + // Just to be safe, ignore any exceptions + // However, it appears iOS doesn't throw any. Other browsers do. + } + promptInstallTimeout = window.setTimeout( function() { + var install; + if ( appInstalled ) { + return; + } + if ( document.hidden || document.webkitHidden ) { + // browser still running, but in background. + // probably means an App was opened up. + return; + } + if ( (new Date).getTime() - 15000 > startTime ) { + // We switched to VLC more than fifteen seconds ago. + // Probably we succesfully switched and the other detection + // methods failed. + return; + } + install = confirm( mw.msg( 'mwe-embedplayer-vlcapp-vlcapppopup' ) ); + if ( install ) { + window.location = appStoreUrl; + } + // Note about timeout: iPad air needs longer than an iPhone + }, 1000 ); + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVlc.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVlc.js new file mode 100644 index 00000000..c668cf40 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVlc.js @@ -0,0 +1,358 @@ +/* +* VLC embed based on: http://people.videolan.org/~damienf/plugin-0.8.6.html +* javascript api: http://www.videolan.org/doc/play-howto/en/ch04.html +* assume version > 0.8.5.1 +*/ +( function( mw, $ ) { "use strict"; + +mw.EmbedPlayerVlc = { + + //Instance Name: + instanceOf : 'Vlc', + + //What the vlc player / plug-in supports: + supports : { + 'playHead':true, + 'pause':true, + 'stop':true, + 'fullscreen':true, + 'timeDisplay':true, + 'volumeControl':true, + + 'playlist_driver':true, // if the object supports playlist functions + 'overlay':false + }, + + // The previous state of the player instance + prevState : 0, + + // Counter for waiting for vlc embed to be ready + waitForVlcCount:0, + + // Store the current play time for vlc + vlcCurrentTime: 0, + + /** + * Get embed HTML + */ + embedPlayerHTML: function() { + var _this = this; + $( this ).html( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ) + // Call postEmbedActions directly ( postEmbedJs will wait for player ready ) + _this.postEmbedActions(); + }, + + /** + * Javascript to run post vlc embedding + * Inserts the requested src to the embed instance + */ + postEmbedActions: function() { + var _this = this; + // Load a pointer to the vlc into the object (this.playerElement) + this.getPlayerElement(); + if ( this.playerElement && this.playerElement.playlist) { + // manipulate the dom object to make sure vlc has the correct size: + this.playerElement.style.width = this.getWidth(); + this.playerElement.style.height = this.getHeight(); + this.playerElement.playlist.items.clear(); + + // VLC likes absolute urls: + var src = mw.absoluteUrl( this.getSrc() ) ; + + mw.log( "EmbedPlayerVlc:: postEmbedActions play src:" + src ); + var itemId = this.playerElement.playlist.add( src ); + if ( itemId != -1 ) { + // Play + this.playerElement.playlist.playItem( itemId ); + } else { + mw.log( "Error:: EmbedPlayerVlc can not play" ); + } + setTimeout( function() { + _this.monitor(); + }, 100 ); + } else { + mw.log( 'EmbedPlayerVlc:: postEmbedActions: vlc not ready' ); + this.waitForVlcCount++; + if ( this.waitForVlcCount < 10 ) { + setTimeout( function() { + _this.postEmbedActions(); + }, 100 ); + } else { + mw.log( 'EmbedPlayerVlc:: vlc never ready' ); + } + } + }, + + /** + * Handles seek requests based on temporal media source type support + * + * @param {Float} percent Seek to this percent of the stream + */ + seek : function( percent ) { + this.getPlayerElement(); + // Use the parent (re) embed with new seek url method if urlTimeEncoding is supported. + if ( this.supportsURLTimeEncoding() ) { + this.parent_seek( percent ); + } else if ( this.playerElement ) { + this.seeking = true; + mw.log( "EmbedPlayerVlc:: seek to: " + percent ) + if ( ( this.playerElement.input.state == 3 ) && ( this.playerElement.input.position != percent ) ) + { + this.playerElement.input.position = percent; + this.controlBuilder.setStatus( mw.msg('mwe-embedplayer-seeking') ); + } + } else { + this.doPlayThenSeek( percent ); + } + this.parent_monitor(); + }, + + /** + * Issues a play request then seeks to a given time + * + * @param {Float} percent Seek to this percent of the stream after playing + */ + doPlayThenSeek:function( percent ) { + mw.log( 'EmbedPlayerVlc:: doPlayThenSeek' ); + var _this = this; + this.play(); + var seekCount = 0; + var readyForSeek = function() { + _this.getPlayerElement(); + var newState = _this.playerElement.input.state; + // If playing we are ready to do the seek + if ( newState == 3 ) { + _this.seek( percent ); + } else { + // Try to get player for 10 seconds: + if ( seekCount < 200 ) { + setTimeout( readyForSeek, 50 ); + rfsCount++; + } else { + mw.log( 'Error: EmbedPlayerVlc doPlayThenSeek failed' ); + } + } + } + readyForSeek(); + }, + + /** + * Updates the status time and player state + */ + monitor: function() { + this.getPlayerElement(); + if ( ! this.playerElement ){ + return ; + } + // Try to get relay vlc log messages to the console. + try{ + if ( this.playerElement.log.messages.count > 0 ) { + // there is one or more messages in the log + var iter = this.playerElement.log.messages.iterator(); + while ( iter.hasNext ) { + var msg = iter.next(); + var msgtype = msg.type.toString(); + if ( ( msg.severity == 1 ) && ( msgtype == "input" ) ) + { + mw.log( msg.message ); + } + } + // clear the log once finished to avoid clogging + this.playerElement.log.messages.clear(); + } + + var newState = this.playerElement.input.state; + if ( this.prevState != newState ) { + if ( newState == 0 ) + { + // current media has stopped + this.onStop(); + } + else if ( newState == 1 ) + { + // current media is opening/connecting + this.onOpen(); + } + else if ( newState == 2 ) + { + // current media is buffering data + this.onBuffer(); + } + else if ( newState == 3 ) + { + // current media is now playing + this.onPlay(); + } + else if ( this.playerElement.input.state == 4 ) { + // current media is now paused + this.onPause(); + } + this.prevState = newState; + } else if ( newState == 3 ) { + // current media is playing + this.onPlaying(); + } + } catch( e ){ + mw.log("EmbedPlayerVlc:: Monitor error"); + } + // Update the status and check timer via universal parent monitor + this.parent_monitor(); + }, + + /** + * Events: + */ + onOpen: function() { + // Open is considered equivalent to other players buffer status: + this.controlBuilder.setStatus( mw.msg('mwe-embedplayer-buffering') ); + }, + onBuffer: function() { + this.controlBuilder.setStatus( mw.msg('mwe-embedplayer-buffering') ); + }, + onPlay: function() { + this.onPlaying(); + }, + onPlaying: function() { + this.seeking = false; + // For now trust the duration from url over vlc input.length + if ( !this.getDuration() && this.playerElement.input.length > 0 ) + { + // mw.log('setting duration to ' + this.playerElement.input.length /1000); + this.duration = this.playerElement.input.length / 1000; + } + this.vlcCurrentTime = this.playerElement.input.time / 1000; + }, + + /** + * Get the embed player time + */ + getPlayerElementTime: function(){ + return this.vlcCurrentTime; + }, + + onPause: function() { + // Update the interface if paused via native plugin + this.parent_pause(); + }, + onStop: function() { + mw.log( 'EmbedPlayerVlc:: onStop' ); + if ( !this.seeking ){ + this.onClipDone(); + } + }, + + /** + * Handles play requests + */ + play: function() { + mw.log( 'EmbedPlayerVlc:: play' ); + // Update the interface + this.parent_play(); + if ( this.getPlayerElement() ) { + // plugin is already being present send play call: + // clear the message log and enable error logging + if ( this.playerElement.log ) { + this.playerElement.log.messages.clear(); + } + if ( this.playerElement.playlist && typeof this.playerElement.playlist.play == 'function') + this.playerElement.playlist.play(); + + if( typeof this.playerElement.play == 'function' ) + this.playerElement.play(); + + this.paused = false; + + // re-start the monitor: + this.monitor(); + } + }, + + /** + * Passes the Pause request to the plugin. + * calls parent "pause" to update interface + */ + pause: function() { + this.parent_pause(); // update the interface if paused via native control + if ( this.getPlayerElement() ) { + try{ + this.playerElement.playlist.togglePause(); + } catch( e ){ + mw.log("EmbedPlayerVlc:: Could not pause video " + e); + } + } + }, + + /** + * Mutes the video + * calls parent "toggleMute" to update interface + */ + toggleMute:function() { + this.parent_toggleMute(); + if ( this.getPlayerElement() ){ + this.playerElement.audio.toggleMute(); + } + }, + + /** + * Update the player volume + * @parm {Float} percent Percent of total volume + */ + setPlayerElementVolume: function ( percent ) { + if ( this.getPlayerElement() && this.playerElement.audio ) { + this.playerElement.audio.volume = percent * 100; + } + }, + + /** + * Gets the current volume + * @return {Float} percent percent of total volume + */ + getVolumen:function() { + if ( this.getPlayerElement() ){ + return this.playerElement.audio.volume / 100; + } + }, + + /** + * Passes fullscreen request to plugin + */ + fullscreen : function() { + if ( this.playerElement ) { + if ( this.playerElement.video ){ + try{ + this.playerElement.video.toggleFullscreen(); + } catch ( e ){ + mw.log("EmbedPlayerVlc:: toggle fullscreen : possible error: " + e); + } + } + } + }, + + /** + * Get the embed vlc object + */ + getPlayerElement : function() { + this.playerElement = $( '#' + this.pid )[0]; + return this.playerElement; + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedTypes.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedTypes.js new file mode 100644 index 00000000..21b2f452 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedTypes.js @@ -0,0 +1,360 @@ +/** + * mw.EmbedTypes object handles setting and getting of supported embed types: + * closely mirrors OggHandler so that its easier to share efforts in this area: + * http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/OggHandler/OggPlayer.js + */ +( function( mw, $ ) { "use strict"; + +/** + * Setup local players and supported mime types In an ideal world we would query the plugin + * to get what mime types it supports in practice not always reliable/available + * + * We can't cleanly store these values per library since player library is sometimes + * loaded post player detection + */ +// Flash based players: +var kplayer = new mw.MediaPlayer('kplayer', [ + 'video/x-flv', + 'video/h264', + 'video/mp4; codecs="avc1.42E01E"', + 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + 'audio/mpeg' +], 'Kplayer'); + +// Native html5 players +var oggNativePlayer = new mw.MediaPlayer( 'oggNative', [ + 'video/ogg', + 'video/ogg; codecs="theora"', + 'video/ogg; codecs="theora, vorbis"', + 'audio/ogg', + 'audio/ogg; codecs="vorbis"', + 'application/ogg' +], 'Native' ); +// Native html5 players +var opusNativePlayer = new mw.MediaPlayer( 'opusNative', [ + 'audio/ogg; codecs="opus"', +], 'Native' ); +var h264NativePlayer = new mw.MediaPlayer( 'h264Native', [ + 'video/h264', + 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' +], 'Native' ); +var appleVdnPlayer = new mw.MediaPlayer( 'appleVdn', [ + 'application/vnd.apple.mpegurl', + 'application/vnd.apple.mpegurl; codecs="avc1.42E01E"' +], 'Native'); +var mp3NativePlayer = new mw.MediaPlayer( 'mp3Native', [ + 'audio/mpeg', + 'audio/mp3' +], 'Native' ); +var aacNativePlayer = new mw.MediaPlayer( 'aacNative', [ + 'audio/mp4', + 'audio/mp4; codecs="mp4a.40.5"' +], 'Native' ); +var webmNativePlayer = new mw.MediaPlayer( 'webmNative', [ + 'video/webm', + 'video/webm; codecs="vp8"', + 'video/webm; codecs="vp8, vorbis"', + 'audio/webm', + 'audio/webm; codecs="vorbis"' +], 'Native' ); +var vp9NativePlayer = new mw.MediaPlayer( 'vp9Native', [ + 'video/webm; codecs="vp9"', + 'video/webm; codecs="vp9, opus"', + 'video/webm; codecs="vp9, vorbis"', + 'audio/webm; codecs="opus"' +], 'Native' ); + +// Image Overlay player ( extends native ) +var imageOverlayPlayer = new mw.MediaPlayer( 'imageOverlay', [ + 'image/jpeg', + 'image/png' +], 'ImageOverlay' ); + +// VLC player +//var vlcMimeList = ['video/ogg', 'audio/ogg', 'audio/mpeg', 'application/ogg', 'video/x-flv', 'video/mp4', 'video/h264', 'video/x-msvideo', 'video/mpeg', 'video/3gp']; +//var vlcPlayer = new mw.MediaPlayer( 'vlc-player', vlcMimeList, 'Vlc' ); + +var vlcAppPlayer = new mw.MediaPlayer( 'vlcAppPlayer', [ + 'video/ogg', + 'video/ogg; codecs="theora"', + 'video/ogg; codecs="theora, vorbis"', + 'audio/ogg', + 'audio/ogg; codecs="vorbis"', + 'audio/ogg; codecs="opus"', + 'application/ogg', + 'video/webm', + 'video/webm; codecs="vp8"', + 'video/webm; codecs="vp8, vorbis"', +], 'VLCApp' ); + +var IEWebMPrompt = new mw.MediaPlayer( 'IEWebMPrompt', [ + 'video/webm', + 'video/webm; codecs="vp8"', + 'video/webm; codecs="vp8, vorbis"' +], 'IEWebMPrompt' ); + +var ogvJsPlayer = new mw.MediaPlayer( 'ogvJsPlayer', [ + 'video/ogg', + 'video/ogg; codecs="theora"', + 'video/ogg; codecs="theora, vorbis"', + 'video/ogg; codecs="theora, opus"', + 'audio/ogg', + 'audio/ogg; codecs="vorbis"', + 'audio/ogg; codecs="opus"', + 'application/ogg' +], 'OgvJs' ); + +// Generic plugin +//var oggPluginPlayer = new mw.MediaPlayer( 'oggPlugin', ['video/ogg', 'application/ogg'], 'Generic' ); + + +mw.EmbedTypes = { + + // MediaPlayers object ( supports methods for quering set of browser players ) + mediaPlayers: null, + + // Detect flag for completion + detect_done:false, + + /** + * Runs the detect method and update the detect_done flag + * + * @constructor + */ + init: function() { + // detect supported types + this.detect(); + this.detect_done = true; + }, + + getMediaPlayers: function(){ + if( this.mediaPlayers ){ + return this.mediaPlayers; + } + this.mediaPlayers = new mw.MediaPlayers(); + // detect available players + this.detectPlayers(); + return this.mediaPlayers; + }, + + /** + * If the browsers supports a given mimetype + * + * @param {String} + * mimeType Mime type for browser plug-in check + */ + supportedMimeType: function( mimeType ) { + for ( var i =0; i < navigator.plugins.length; i++ ) { + var plugin = navigator.plugins[i]; + if ( typeof plugin[ mimeType ] != "undefined" ){ + return true; + } + } + return false; + }, + addFlashPlayer: function(){ + if( !mw.config.get( 'EmbedPlayer.DisableHTML5FlashFallback' ) ){ + this.mediaPlayers.addPlayer( kplayer ); + } + }, + /** + * Detects what plug-ins the client supports + */ + detectPlayers: function() { + mw.log( "EmbedTypes::detectPlayers running detect" ); + + // All players support for playing "images" + this.mediaPlayers.addPlayer( imageOverlayPlayer ); + + // Some browsers filter out duplicate mime types, hiding some plugins + var uniqueMimesOnly = $.client.test( { opera: null, safari: null } ); + + // Use core mw.supportsFlash check: + if( mw.supportsFlash() ){ + this.addFlashPlayer(); + } + + // ActiveX plugins + if ( $.client.profile().name === 'msie' ) { + // VLC + //if ( this.testActiveX( 'VideoLAN.VLCPlugin.2' ) ) { + // this.mediaPlayers.addPlayer( vlcPlayer ); + //} + + // quicktime (currently off) + // if ( this.testActiveX( + // 'QuickTimeCheckObject.QuickTimeCheck.1' ) ) + // this.mediaPlayers.addPlayer(quicktimeActiveXPlayer); + } + //