diff options
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources')
27 files changed, 11361 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 Binary files differnew file mode 100644 index 00000000..cf86d1a8 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4 diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js new file mode 100644 index 00000000..131302a2 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js @@ -0,0 +1,2760 @@ +/** +* embedPlayer is the base class for html5 video tag javascript abstraction library +* embedPlayer include a few subclasses: +* +* mediaPlayer Media player embed system ie: java, vlc or native. +* mediaElement Represents source media elements +* mw.PlayerControlBuilder Handles skinning of the player controls +*/ +( function( mw, $ ) {"use strict"; + /** + * Merge in the default video attributes supported by embedPlayer: + */ + mw.mergeConfig('EmbedPlayer.Attributes', { + /* + * Base html element attributes: + */ + + // id: Auto-populated if unset + "id" : null, + + // Width: alternate to "style" to set player width + "width" : null, + + // Height: alternative to "style" to set player height + "height" : null, + + /* + * Base html5 video element attributes / states also see: + * http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html + */ + + // Media src URI, can be relative or absolute URI + "src" : null, + + // Poster attribute for displaying a place holder image before loading + // or playing the video + "poster" : null, + + // Autoplay if the media should start playing + "autoplay" : false, + + // Loop attribute if the media should repeat on complete + "loop" : false, + + // If the player controls should be displayed + "controls" : true, + + // Video starts "paused" + "paused" : true, + + // ReadyState an attribute informs clients of video loading state: + // see: http://www.whatwg.org/specs/web-apps/current-work/#readystate + "readyState" : 0, + + // Loading state of the video element + "networkState" : 0, + + // Current playback position + "currentTime" : 0, + + // Previous player set time + // Lets javascript use $('#videoId')[0].currentTime = newTime; + "previousTime" : 0, + + // Previous player set volume + // Lets javascript use $('#videoId')[0].volume = newVolume; + "previousVolume" : 1, + + // Initial player volume: + "volume" : 0.75, + + // Caches the volume before a mute toggle + "preMuteVolume" : 0.75, + + // Media duration: Value is populated via + // custom data-durationhint attribute or via the media file once its played + "duration" : null, + + // A hint to the duration of the media file so that duration + // can be displayed in the player without loading the media file + 'data-durationhint': null, + + // to disable menu or timedText for a given embed + 'data-disablecontrols': null, + + // Also support direct durationHint attribute ( backwards compatibly ) + // @deprecated please use data-durationhint instead. + 'durationHint' : null, + + // Mute state + "muted" : false, + + /** + * Custom attributes for embedPlayer player: (not part of the html5 + * video spec) + */ + + // Default video aspect ratio + 'videoAspect' : '4:3', + + // Start time of the clip + "start" : 0, + + // End time of the clip + "end" : null, + + // If the player controls should be overlaid + // ( Global default via config EmbedPlayer.OverlayControls in module + // loader.js) + "overlaycontrols" : true, + + // Attribute to use 'native' controls + "usenativecontrols" : false, + + // If the player should include an attribution button: + 'attributionbutton' : true, + + // A player error object (Includes title and message) + // * Used to display an error instead of a play button + // * The full player api available + 'playerError' : {}, + + // A flag to hide the player gui and disable autoplay + // * Used for empty players or a player where you want to dynamically set sources, then play. + // * The player API remains active. + 'data-blockPlayerDisplay': null, + + // If serving an ogg_chop segment use this to offset the presentation time + // ( for some plugins that use ogg page time rather than presentation time ) + "startOffset" : 0, + + // If the download link should be shown + "downloadLink" : true, + + // Content type of the media + "type" : null + + } ); + + + /** + * The base source attribute checks also see: + * http://dev.w3.org/html5/spec/Overview.html#the-source-element + */ + mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [ + // source id + 'id', + + // media url + 'src', + + // Title string for the source asset + 'title', + + // The html5 spec uses label instead of 'title' for naming sources + 'label', + + // boolean if we support temporal url requests on the source media + 'URLTimeEncoding', + + // Media has a startOffset ( used for plugins that + // display ogg page time rather than presentation time + 'startOffset', + + // Media start time + 'start', + + // Media end time + 'end', + + // If the source is the default source + 'default', + + // Title of the source + 'title', + + // titleKey ( used for api lookups TODO move into mediaWiki specific support + 'titleKey' + ] ); + + + /** + * Base embedPlayer object + * + * @param {Element} + * element, the element used for initialization. + * @constructor + */ + mw.EmbedPlayer = function( element ) { + return this.init( element ); + }; + + mw.EmbedPlayer.prototype = { + + // The mediaElement object containing all mediaSource objects + 'mediaElement' : null, + + // Object that describes the supported feature set of the underling plugin / + // Support list is described in PlayerControlBuilder components + 'supports': { }, + + // If the player is done loading ( does not guarantee playability ) + // for example if there is an error playerReadyFlag is still set to true once + // no more loading is to be done + 'playerReadyFlag' : false, + + // Stores the loading errors + 'loadError' : false, + + // Thumbnail updating flag ( to avoid rewriting an thumbnail thats already + // being updated) + 'thumbnailUpdatingFlag' : false, + + // Stopped state flag + 'stopped' : true, + + // Local variable to hold CMML meeta data about the current clip + // for more on CMML see: http://wiki.xiph.org/CMML + 'cmmlData': null, + + // Stores the seek time request, Updated by the seek function + 'serverSeekTime' : 0, + + // If the embedPlayer is current 'seeking' + 'seeking' : false, + + // Percent of the clip buffered: + 'bufferedPercent' : 0, + + // Holds the timer interval function + 'monitorTimerId' : null, + + // Buffer flags + 'bufferStartFlag' : false, + 'bufferEndFlag' : false, + + // For supporting media fragments stores the play end time + 'pauseTime' : null, + + // On done playing + 'donePlayingCount' : 0 + , + // if player events should be Propagated + '_propagateEvents': true, + + // If the onDone interface should be displayed + 'onDoneInterfaceFlag': true, + + // if we should check for a loading spinner in the monitor function: + '_checkHideSpinner' : false, + + // If pause play controls click controls should be active: + '_playContorls' : true, + + // If player should be displayed (in some caused like audio, we don't need the player to be visible + 'displayPlayer': true, + + // Widget loaded should only fire once + 'widgetLoaded': false, + + /** + * embedPlayer + * + * @constructor + * + * @param {Element} + * element DOM element that we are building the player interface for. + */ + init: function( element ) { + var _this = this; + mw.log('EmbedPlayer: initEmbedPlayer: ' + $(element).width() ); + + var playerAttributes = mw.config.get( 'EmbedPlayer.Attributes' ); + + // Store the rewrite element tag type + this.rewriteElementTagName = element.tagName.toLowerCase(); + + this.noPlayerFallbackHTML = $( element ).html(); + + // Setup the player Interface from supported attributes: + for ( var attr in playerAttributes ) { + // We can't use $(element).attr( attr ) because we have to check for boolean attributes: + if ( element.getAttribute( attr ) != null ) { + // boolean attributes + if( element.getAttribute( attr ) == '' ){ + this[ attr ] = true; + } else { + this[ attr ] = element.getAttribute( attr ); + } + } else { + this[attr] = playerAttributes[attr]; + } + // string -> boolean + if( this[ attr ] == "false" ) this[attr] = false; + if( this[ attr ] == "true" ) this[attr] = true; + } + + // Hide "controls" if using native player controls: + if( this.useNativePlayerControls() ){ + _this.controls = true; + } + // Set the skin name from the class + var sn = $(element).attr( 'class' ); + + if ( sn && sn != '' ) { + var skinList = mw.config.get('EmbedPlayer.SkinList'); + for ( var n = 0; n < skinList.length; n++ ) { + if ( sn.indexOf( skinList[n].toLowerCase() ) !== -1 ) { + this.skinName = skinList[ n ]; + } + } + } + // Set the default skin if unset: + if ( !this.skinName ) { + this.skinName = mw.config.get( 'EmbedPlayer.DefaultSkin' ); + } + + // Support custom monitorRate Attribute ( if not use default ) + if( !this.monitorRate ){ + this.monitorRate = mw.config.get( 'EmbedPlayer.MonitorRate' ); + } + + // Make sure startOffset is cast as an float: + if ( this.startOffset && this.startOffset.split( ':' ).length >= 2 ) { + this.startOffset = parseFloat( mw.npt2seconds( this.startOffset ) ); + } + + // Make sure offset is in float: + this.startOffset = parseFloat( this.startOffset ); + + // Set the source duration + if ( $( element ).attr( 'duration' ) ) { + _this.duration = $( element ).attr( 'duration' ); + } + // Add durationHint property form data-durationhint: + if( _this['data-durationhint']){ + _this.durationHint = _this['data-durationhint']; + } + // Update duration from provided durationHint + if ( _this.durationHint && ! _this.duration){ + _this.duration = mw.npt2seconds( _this.durationHint ); + } + + // Make sure duration is a float: + this.duration = parseFloat( this.duration ); + mw.log( 'EmbedPlayer::init:' + this.id + " duration is: " + this.duration ); + + // Add disablecontrols property form data-disablecontrols: + if( _this['data-disablecontrols'] ){ + _this.disablecontrols = _this['data-disablecontrols']; + } + + // Set the playerElementId id + this.pid = 'pid_' + this.id; + + // Add the mediaElement object with the elements sources: + this.mediaElement = new mw.MediaElement( element ); + + this.bindHelper( 'updateLayout', function() { + _this.updateLayout(); + }); + }, + /** + * Bind helpers to help iOS retain bind context + * + * Yes, iOS will fail when you run $( embedPlayer ).bind() + * but "work" when you run embedPlayer.bind() if the script urls are from diffrent "resources" + */ + bindHelper: function( name, callback ){ + $( this ).bind( name, callback ); + return this; + }, + unbindHelper: function( bindName ){ + if( bindName ) { + $( this ).unbind( bindName ); + } + return this; + }, + triggerQueueCallback: function( name, callback ){ + $( this ).triggerQueueCallback( name, callback ); + }, + triggerHelper: function( name, obj ){ + try{ + $( this ).trigger( name, obj ); + } catch( e ){ + // ignore try catch calls + // mw.log( "EmbedPlayer:: possible error in trgger: " + name + " " + e.toString() ); + } + }, + /** + * Stop events from Propagation and blocks interface updates and trigger events. + * @return + */ + stopEventPropagation: function(){ + mw.log("EmbedPlayer:: stopEventPropagation"); + this.stopMonitor(); + this._propagateEvents = false; + }, + + /** + * Restores event propagation + * @return + */ + restoreEventPropagation: function(){ + mw.log("EmbedPlayer:: restoreEventPropagation"); + this._propagateEvents = true; + this.startMonitor(); + }, + + /** + * Enables the play controls ( for example when an ad is done ) + */ + enablePlayControls: function(){ + mw.log("EmbedPlayer:: enablePlayControls" ); + if( this.useNativePlayerControls() ){ + return ; + } + this._playContorls = true; + // re-enable hover: + this.getInterface().find( '.play-btn' ) + .buttonHover() + .css('cursor', 'pointer' ); + + this.controlBuilder.enableSeekBar(); + /* + * We should pass an array with enabled components, and the controlBuilder will listen + * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer. + * [ 'playButton', 'seekBar' ] + */ + $( this ).trigger( 'onEnableInterfaceComponents'); + }, + + /** + * Disables play controls, for example when an ad is playing back + */ + disablePlayControls: function(){ + if( this.useNativePlayerControls() ){ + return ; + } + this._playContorls = false; + // turn off hover: + this.getInterface().find( '.play-btn' ) + .unbind('mouseenter mouseleave') + .css('cursor', 'default' ); + + this.controlBuilder.disableSeekBar(); + /** + * We should pass an array with disabled components, and the controlBuilder will listen + * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer. + * [ 'playButton', 'seekBar' ] + */ + $( this ).trigger( 'onDisableInterfaceComponents'); + }, + + /** + * For plugin-players to update supported features + */ + updateFeatureSupport: function(){ + $( this ).trigger('updateFeatureSupportEvent', this.supports ); + return ; + }, + /** + * Apply Intrinsic Aspect ratio of a given image to a poster image layout + */ + applyIntrinsicAspect: function(){ + var $this = $( this ); + // Check if a image thumbnail is present: + if( this.getInterface().find('.playerPoster').length ){ + var img = this.getInterface().find('.playerPoster')[0]; + var pHeight = $this.height(); + // Check for intrinsic width and maintain aspect ratio + if( img.naturalWidth && img.naturalHeight ){ + var pWidth = parseInt( img.naturalWidth / img.naturalHeight * pHeight); + if( pWidth > $this.width() ){ + pWidth = $this.width(); + pHeight = parseInt( img.naturalHeight / img.naturalWidth * pWidth ); + } + $( img ).css({ + 'height' : pHeight + 'px', + 'width': pWidth + 'px', + 'left': ( ( $this.width() - pWidth ) * .5 ) + 'px', + 'top': ( ( $this.height() - pHeight ) * .5 ) + 'px', + 'position' : 'absolute' + }); + } + } + }, + /** + * Set the width & height from css style attribute, element attribute, or by + * default value if no css or attribute is provided set a callback to + * resize. + * + * Updates this.width & this.height + * + * @param {Element} + * element Source element to grab size from + */ + loadPlayerSize: function( element ) { + // check for direct element attribute: + this.height = element.height > 0 ? element.height + '' : $(element).css( 'height' ); + this.width = element.width > 0 ? element.width + '' : $(element).css( 'width' ); + + // Special check for chrome 100% with re-mapping to 32px + // Video embed at 32x32 will have to wait for intrinsic video size later on + if( this.height == '32px' || this.height =='32px' ){ + this.width = '100%'; + this.height = '100%'; + } + mw.log('EmbedPlayer::loadPlayerSize: css size:' + this.width + ' h: ' + this.height); + + // Set to parent size ( resize events will cause player size updates) + if( this.height.indexOf('100%') != -1 || this.width.indexOf('100%') != -1 ){ + var $relativeParent = $(element).parents().filter(function() { + // reduce to only relative position or "body" elements + return $( this ).is('body') || $( this ).css('position') == 'relative'; + }).slice(0,1); // grab only the "first" + this.width = $relativeParent.width(); + this.height = $relativeParent.height(); + } + // Make sure height and width are a number + this.height = parseInt( this.height ); + this.width = parseInt( this.width ); + + // Set via attribute if CSS is zero or NaN and we have an attribute value: + this.height = ( this.height==0 || isNaN( this.height ) + && $(element).attr( 'height' ) ) ? + parseInt( $(element).attr( 'height' ) ): this.height; + this.width = ( this.width == 0 || isNaN( this.width ) + && $(element).attr( 'width' ) )? + parseInt( $(element).attr( 'width' ) ): this.width; + + + // Special case for audio + + // Firefox sets audio height to "0px" while webkit uses 32px .. force zero: + if( this.isAudio() && this.height == '32' ) { + this.height = 20; + } + + // Use default aspect ration to get height or width ( if rewriting a non-audio player ) + if( this.isAudio() && this.videoAspect ) { + var aspect = this.videoAspect.split( ':' ); + if( this.height && !this.width ) { + this.width = parseInt( this.height * ( aspect[0] / aspect[1] ) ); + } + if( this.width && !this.height ) { + var apectRatio = ( aspect[1] / aspect[0] ); + this.height = parseInt( this.width * ( aspect[1] / aspect[0] ) ); + } + } + + // On load sometimes attr is temporally -1 as we don't have video metadata yet. + // or in IE we get NaN for width height + // + // NOTE: browsers that do support height width should set "waitForMeta" flag in addElement + if( ( isNaN( this.height )|| isNaN( this.width ) ) || + ( this.height == -1 || this.width == -1 ) || + // Check for firefox defaults + // Note: ideally firefox would not do random guesses at css + // values + ( (this.height == 150 || this.height == 64 ) && this.width == 300 ) + ) { + var defaultSize = mw.config.get( 'EmbedPlayer.DefaultSize' ).split( 'x' ); + if( isNaN( this.width ) ){ + this.width = defaultSize[0]; + } + + // Special height default for audio tag ( if not set ) + if( this.isAudio() ) { + this.height = 20; + }else{ + this.height = defaultSize[1]; + } + } + }, + + /** + * Get the player pixel width not including controls + * + * @return {Number} pixel height of the video + */ + getPlayerWidth: function() { + var profile = $.client.profile(); + + if ( profile.name === 'firefox' && profile.versionNumber < 2 ) { + return ( $( this ).parent().parent().width() ); + } + return $( this ).width(); + }, + + /** + * Get the player pixel height not including controls + * + * @return {Number} pixel height of the video + */ + getPlayerHeight: function() { + return $( this ).height(); + }, + + /** + * Check player for sources. If we need to get media sources form an + * external file that request is issued here + */ + checkPlayerSources: function() { + mw.log( 'EmbedPlayer::checkPlayerSources: ' + this.id ); + var _this = this; + // Allow plugins to listen to a preCheckPlayerSources ( for registering the source loading point ) + $( _this ).trigger( 'preCheckPlayerSources' ); + + // Allow plugins to block on sources lookup ( cases where we just have an api key for example ) + $( _this ).triggerQueueCallback( 'checkPlayerSourcesEvent', function(){ + _this.setupSourcePlayer(); + }); + }, + + /** + * Get text tracks from the mediaElement + */ + getTextTracks: function(){ + if( !this.mediaElement ){ + return []; + } + return this.mediaElement.getTextTracks(); + }, + /** + * Empty the player sources + */ + emptySources: function(){ + if( this.mediaElement ){ + this.mediaElement.sources = []; + this.mediaElement.selectedSource = null; + } + // setup pointer to old source: + this.prevPlayer = this.selectedPlayer; + // don't null out the selected player on empty sources + //this.selectedPlayer =null; + }, + + /** + * Switch and play a video source + * + * Checks if the target source is the same playback mode and does player switch if needed. + * and calls playerSwitchSource + */ + switchPlaySource: function( source, switchCallback, doneCallback ){ + var _this = this; + var targetPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( source.mimeType ) ; + if( targetPlayer.library != this.selectedPlayer.library ){ + this.selectedPlayer = targetPlayer; + this.updatePlaybackInterface( function(){ + _this.playerSwitchSource( source, switchCallback, doneCallback ); + }); + } else { + // Call the player switch directly: + _this.playerSwitchSource( source, switchCallback, doneCallback ); + } + }, + /** + * abstract function player interface must support actual source switch + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + mw.log( "Error player interface must support actual source switch"); + }, + + /** + * Set up the select source player + * + * issues autoSelectSource call + * + * Sets load error if no source is playable + */ + setupSourcePlayer: function() { + var _this = this; + mw.log("EmbedPlayer::setupSourcePlayer: " + this.id + ' sources: ' + this.mediaElement.sources.length ); + + // Check for source replace configuration: + if( mw.config.get('EmbedPlayer.ReplaceSources' ) ){ + this.emptySources(); + $.each( mw.config.get('EmbedPlayer.ReplaceSources' ), function( inx, source ){ + _this.mediaElement.tryAddSource( source ); + }); + } + + // Autoseletct the media source + this.mediaElement.autoSelectSource(); + + // Auto select player based on default order + if( this.mediaElement.selectedSource ){ + this.selectedPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( this.mediaElement.selectedSource.mimeType ); + // Check if we need to switch player rendering libraries: + if ( this.selectedPlayer && ( !this.prevPlayer || this.prevPlayer.library != this.selectedPlayer.library ) ) { + // Inherit the playback system of the selected player: + this.updatePlaybackInterface(); + return ; + } + } + + // Check if no player is selected + if( !this.selectedPlayer || !this.mediaElement.selectedSource ){ + this.showPlayerError(); + mw.log( "EmbedPlayer:: setupSourcePlayer > player ready ( but with errors ) "); + } else { + // Trigger layout ready event + $( this ).trigger( 'layoutReady' ); + // Show the interface: + this.getInterface().find( '.control-bar').show(); + this.addLargePlayBtn(); + } + // We still do the playerReady sequence on errors to provide an api + // and player error events + this.playerReadyFlag = true; + // trigger the player ready event; + $( this ).trigger( 'playerReady' ); + this.triggerWidgetLoaded(); + }, + + /** + * Updates the player interface + * + * Loads and inherit methods from the selected player interface. + * + * @param {Function} + * callback Function to be called once playback-system has been + * inherited + */ + updatePlaybackInterface: function( callback ) { + var _this = this; + mw.log( "EmbedPlayer::updatePlaybackInterface: duration is: " + this.getDuration() + ' playerId: ' + this.id ); + // Clear out any non-base embedObj methods: + if ( this.instanceOf ) { + // Update the prev instance var used for swiching interfaces to know the previous instance. + $( this ).data( 'previousInstanceOf', this.instanceOf ); + var tmpObj = window['mw.EmbedPlayer' + this.instanceOf ]; + for ( var i in tmpObj ) { + // Restore parent into local location + if ( typeof this[ 'parent_' + i ] != 'undefined' ) { + this[i] = this[ 'parent_' + i]; + } else { + this[i] = null; + } + } + } + // Set up the new embedObj + mw.log( 'EmbedPlayer::updatePlaybackInterface: embedding with ' + this.selectedPlayer.library ); + this.selectedPlayer.load( function() { + _this.updateLoadedPlayerInterface( callback ); + }); + }, + /** + * Update a loaded player interface by setting local methods to the + * updated player prototype methods + * + * @parma {function} + * callback function called once player has been loaded + */ + updateLoadedPlayerInterface: function( callback ){ + var _this = this; + mw.log( 'EmbedPlayer::updateLoadedPlayerInterface ' + _this.selectedPlayer.library + " player loaded for " + _this.id ); + + // Get embed library player Interface + var playerInterface = mw[ 'EmbedPlayer' + _this.selectedPlayer.library ]; + + // Build the player interface ( if the interface includes an init ) + if( playerInterface.init ){ + playerInterface.init(); + } + + for ( var method in playerInterface ) { + if ( typeof _this[method] != 'undefined' && !_this['parent_' + method] ) { + _this['parent_' + method] = _this[method]; + } + _this[ method ] = playerInterface[ method ]; + } + // Update feature support + _this.updateFeatureSupport(); + // Update duration + _this.getDuration(); + // show player inline + _this.showPlayer(); + // Run the callback if provided + if ( callback && $.isFunction( callback ) ){ + callback(); + } + }, + + /** + * Select a player playback system + * + * @param {Object} + * player Player playback system to be selected player playback + * system include vlc, native, java etc. + */ + selectPlayer: function( player ) { + mw.log("EmbedPlayer:: selectPlayer " + player.id ); + var _this = this; + if ( this.selectedPlayer.id != player.id ) { + this.selectedPlayer = player; + this.updatePlaybackInterface( function(){ + // Hide / remove track container + _this.getInterface().find( '.track' ).remove(); + // We have to re-bind hoverIntent ( has to happen in this scope ) + if( !_this.useNativePlayerControls() && _this.controls && _this.controlBuilder.isOverlayControls() ){ + _this.controlBuilder.showControlBar(); + _this.getInterface().hoverIntent({ + 'sensitivity': 4, + 'timeout' : 2000, + 'over' : function(){ + _this.controlBuilder.showControlBar(); + }, + 'out' : function(){ + _this.controlBuilder.hideControlBar(); + } + }); + } + }); + } + }, + + /** + * Get a time range from the media start and end time + * + * @return startNpt and endNpt time if present + */ + getTimeRange: function() { + var end_time = ( this.controlBuilder.longTimeDisp )? '/' + mw.seconds2npt( this.getDuration() ) : ''; + var defaultTimeRange = '0:00' + end_time; + if ( !this.mediaElement ){ + return defaultTimeRange; + } + if ( !this.mediaElement.selectedSource ){ + return defaultTimeRange; + } + if ( !this.mediaElement.selectedSource.endNpt ){ + return defaultTimeRange; + } + return this.mediaElement.selectedSource.startNpt + this.mediaElement.selectedSource.endNpt; + }, + + /** + * Get the duration of the embed player + */ + getDuration: function() { + if ( isNaN(this.duration) && this.mediaElement && this.mediaElement.selectedSource && + typeof this.mediaElement.selectedSource.durationHint != 'undefined' ){ + this.duration = this.mediaElement.selectedSource.durationHint; + } + return this.duration; + }, + + /** + * Get the player height + */ + getHeight: function() { + return this.getInterface().height(); + }, + + /** + * Get the player width + */ + getWidth: function(){ + return this.getInterface().width(); + }, + + /** + * Check if the selected source is an audio element: + */ + isAudio: function(){ + return ( this.rewriteElementTagName == 'audio' + || + ( this.mediaElement && this.mediaElement.selectedSource && this.mediaElement.selectedSource.mimeType.indexOf('audio/') !== -1 ) + ); + }, + + /** + * Get the plugin embed html ( should be implemented by embed player interface ) + */ + embedPlayerHTML: function() { + return 'Error: function embedPlayerHTML should be implemented by embed player interface '; + }, + + /** + * Seek function ( should be implemented by embedPlayer interface + * playerNative, playerKplayer etc. ) embedPlayer seek only handles URL + * time seeks + * @param {Float} + * percent of the video total length to seek to + */ + seek: function( percent ) { + var _this = this; + this.seeking = true; + // Trigger preSeek event for plugins that want to store pre seek conditions. + $( this ).trigger( 'preSeek', percent ); + + // Do argument checking: + if( percent < 0 ){ + percent = 0; + } + + if( percent > 1 ){ + percent = 1; + } + // set the playhead to the target position + this.updatePlayHead( percent ); + + // See if we should do a server side seek ( player independent ) + if ( this.supportsURLTimeEncoding() ) { + mw.log( 'EmbedPlayer::seek:: updated serverSeekTime: ' + mw.seconds2npt ( this.serverSeekTime ) + + ' currentTime: ' + _this.currentTime ); + // make sure we need to seek: + if( _this.currentTime == _this.serverSeekTime ){ + return ; + } + + this.stop(); + this.didSeekJump = true; + // Make sure this.serverSeekTime is up-to-date: + this.serverSeekTime = mw.npt2seconds( this.startNpt ) + parseFloat( percent * this.getDuration() ); + } + // Run the onSeeking interface update + // NOTE controlBuilder should really bind to html5 events rather + // than explicitly calling it or inheriting stuff. + this.controlBuilder.onSeek(); + }, + + /** + * Seeks to the requested time and issues a callback when ready (should be + * overwritten by client that supports frame serving) + */ + setCurrentTime: function( time, callback ) { + mw.log( 'Error: EmbedPlayer, setCurrentTime not overriden' ); + if( $.isFunction( callback ) ){ + callback(); + } + }, + + /** + * On clip done action. Called once a clip is done playing + * TODO clean up end sequence flow + */ + triggeredEndDone: false, + postSequence: false, + onClipDone: function() { + var _this = this; + // Don't run onclipdone if _propagateEvents is off + if( !_this._propagateEvents ){ + return ; + } + mw.log( 'EmbedPlayer::onClipDone: propagate:' + _this._propagateEvents + ' id:' + this.id + ' doneCount:' + this.donePlayingCount + ' stop state:' +this.isStopped() ); + // Only run stopped once: + if( !this.isStopped() ){ + // set the "stopped" flag: + this.stopped = true; + + // Show the control bar: + this.controlBuilder.showControlBar(); + + // TOOD we should improve the end event flow + // First end event for ads or current clip ended bindings + if( ! this.onDoneInterfaceFlag ){ + this.stopEventPropagation(); + } + + mw.log("EmbedPlayer:: trigger: ended ( inteface continue pre-check: " + this.onDoneInterfaceFlag + ' )' ); + $( this ).trigger( 'ended' ); + mw.log("EmbedPlayer::onClipDone:Trigged ended, continue? " + this.onDoneInterfaceFlag); + + + if( ! this.onDoneInterfaceFlag ){ + // Restore events if we are not running the interface done actions + this.restoreEventPropagation(); + return ; + } + + // A secondary end event for playlist and clip sequence endings + if( this.onDoneInterfaceFlag ){ + // We trigger two end events to match KDP and ensure playbackComplete always comes before playerPlayEnd + // in content ends. + mw.log("EmbedPlayer:: trigger: playbackComplete"); + $( this ).trigger( 'playbackComplete' ); + // now trigger postEnd for( playerPlayEnd ) + mw.log("EmbedPlayer:: trigger: postEnded"); + $( this ).trigger( 'postEnded' ); + } + // if the ended event did not trigger more timeline actions run the actual stop: + if( this.onDoneInterfaceFlag ){ + mw.log("EmbedPlayer::onDoneInterfaceFlag=true do interface done"); + // Prevent the native "onPlay" event from propagating that happens when we rewind: + this.stopEventPropagation(); + + // Update the clip done playing count ( for keeping track of replays ) + _this.donePlayingCount ++; + + // Rewind the player to the start: + // NOTE: Setting to 0 causes lags on iPad when replaying, thus setting to 0.01 + this.setCurrentTime(0.01, function(){ + + // Set to stopped state: + _this.stop(); + + // Restore events after we rewind the player + _this.restoreEventPropagation(); + + // Check if we have the "loop" property set + if( _this.loop ) { + _this.stopped = false; + _this.play(); + return; + } else { + // make sure we are in a paused state. + _this.pause(); + } + // Check if have a force display of the large play button + if( mw.config.get('EmbedPlayer.ForceLargeReplayButton') === true ){ + _this.addLargePlayBtn(); + } else{ + // Check if we should hide the large play button on end: + if( $( _this ).data( 'hideEndPlayButton' ) || !_this.useLargePlayBtn() ){ + _this.hideLargePlayBtn(); + } else { + _this.addLargePlayBtn(); + } + } + // An event for once the all ended events are done. + mw.log("EmbedPlayer:: trigger: onEndedDone"); + if ( !_this.triggeredEndDone ){ + _this.triggeredEndDone = true; + $( _this ).trigger( 'onEndedDone', [_this.id] ); + } + }) + } + } + }, + + + /** + * Shows the video Thumbnail, updates pause state + */ + showThumbnail: function() { + var _this = this; + mw.log( 'EmbedPlayer::showThumbnail::' + this.stopped ); + + // Close Menu Overlay: + this.controlBuilder.closeMenuOverlay(); + + // update the thumbnail html: + this.updatePosterHTML(); + + this.paused = true; + this.stopped = true; + + // Once the thumbnail is shown run the mediaReady trigger (if not using native controls) + if( !this.useNativePlayerControls() ){ + mw.log("mediaLoaded"); + $( this ).trigger( 'mediaLoaded' ); + } + }, + + /** + * Show the player + */ + showPlayer: function () { + mw.log( 'EmbedPlayer:: showPlayer: ' + this.id + ' interface: w:' + this.width + ' h:' + this.height ); + var _this = this; + + // Remove the player loader spinner if it exists + this.hideSpinnerAndPlayBtn(); + // If a isPersistentNativePlayer ( overlay the controls ) + if( !this.useNativePlayerControls() && this.isPersistentNativePlayer() ){ + $( this ).show(); + } + // Add controls if enabled: + if ( this.controls ) { + if( this.useNativePlayerControls() ){ + if( this.getPlayerElement() ){ + $( this.getPlayerElement() ).attr('controls', "true"); + } + } else { + this.controlBuilder.addControls(); + } + } + + // Update Thumbnail for the "player" + this.updatePosterHTML(); + + // Update temporal url if present + this.updateTemporalUrl(); + + // Do we need to show the player? + if( this.displayPlayer === false ) { + _this.getVideoHolder().hide(); + _this.getInterface().height( _this.getComponentsHeight() ); + _this.triggerHelper('updateLayout'); + } + + // Update layout + this.updateLayout(); + + // Make sure we have a play btn: + this.addLargePlayBtn(); + + // Update the playerReady flag + this.playerReadyFlag = true; + mw.log("EmbedPlayer:: Trigger: playerReady"); + // trigger the player ready event; + $( this ).trigger( 'playerReady' ); + this.triggerWidgetLoaded(); + + // Check if we want to block the player display + if( this['data-blockPlayerDisplay'] ){ + this.blockPlayerDisplay(); + return ; + } + + // Check if there are any errors to be displayed: + if( this.getError() ){ + this.showErrorMsg( this.getError() ); + return ; + } + // Auto play stopped ( no playerReady has already started playback ) and if not on an iPad with iOS > 3 + if ( this.isStopped() && this.autoplay && (!mw.isIOS() || mw.isIpad3() ) ) { + mw.log( 'EmbedPlayer::showPlayer::Do autoPlay' ); + _this.play(); + } + }, + + getComponentsHeight: function() { + var height = 0; + + // Go over all playerContainer direct children with .block class + this.getInterface().find('.block').each(function() { + height += $( this ).outerHeight( true ); + }); + + // FIXME embedPlayer should know nothing about playlist layout + /* If we're in vertical playlist mode, and not in fullscreen add playlist height + if( $('#container').hasClass('vertical') && ! this.controlBuilder.isInFullScreen() && this.displayPlayer ) { + height += $('#playlistContainer').outerHeight( true ); + } + */ + + // + var offset = (mw.isIOS()) ? 5 : 0; + + return height + offset; + }, + updateLayout: function() { + // update image layout: + this.applyIntrinsicAspect(); + if( !mw.config.get('EmbedPlayer.IsIframeServer' ) ){ + // Use intrensic container size + return ; + } + // Set window height if in iframe: + var windowHeight; + if( mw.isIOS() && ! this.controlBuilder.isInFullScreen() ) { + windowHeight = $( window.parent.document.getElementById( this.id ) ).height(); + } else { + windowHeight = window.innerHeight; + } + + var newHeight = windowHeight - this.getComponentsHeight(); + var currentHeight = this.getVideoHolder().height(); + // Always update videoHolder height + if( currentHeight !== newHeight ) { + mw.log('EmbedPlayer: updateLayout:: window: ' + windowHeight + ', components: ' + this.getComponentsHeight() + ', videoHolder old height: ' + currentHeight + ', new height: ' + newHeight ); + this.getVideoHolder().height( newHeight ); + } + }, + /** + * Gets a refrence to the main player interface, builds if not avaliable + */ + getInterface: function(){ + if( !this.$interface ){ + // init the control builder + this.controlBuilder = new mw.PlayerControlBuilder( this ); + + // build the interface wrapper + this.$interface = $( this ).wrap( + $('<div />') + .addClass( 'mwPlayerContainer ' + this.controlBuilder.playerClass ) + .append( + $('<div />').addClass( 'videoHolder' ) + ) + ).parent().parent(); + + // pass along any inhereted style: + if( this.style.cssText ){ + this.$interface[0].style.cssText = this.style.cssText; + } + // clear out base style + this.style.cssText = ''; + + // if not displayiung a play button, ( pass through to native player ) + if( ! this.useLargePlayBtn() ){ + this.$interface.css('pointer-events', 'none'); + } + } + return this.$interface; + }, + + /** + * Media fragments handler based on: + * http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#fragment-dimensions + * + * We support seconds and npt ( normal play time ) + * + * Updates the player per fragment url info if present + * + */ + updateTemporalUrl: function(){ + var sourceHash = /[^\#]+$/.exec( this.getSrc() ).toString(); + if( sourceHash.indexOf('t=') === 0 ){ + // parse the times + var times = sourceHash.substr(2).split(','); + if( times[0] ){ + // update the current time + this.currentTime = mw.npt2seconds( times[0].toString() ); + } + if( times[1] ){ + this.pauseTime = mw.npt2seconds( times[1].toString() ); + // ignore invalid ranges: + if( this.pauseTime < this.currentTime ){ + this.pauseTime = null; + } + } + // Update the play head + this.updatePlayHead( this.currentTime / this.duration ); + // Update status: + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) ); + } + }, + /** + * Sets an error message on the player + * + * @param {string} + * errorMsg + */ + setError: function( errorObj ){ + var _this = this; + if ( typeof errorObj == 'string' ) { + this.playerError = { + 'title' : _this.getKalturaMsg( 'ks-GENERIC_ERROR_TITLE' ), + 'message' : errorObj + } + return ; + + } + this.playerError = errorObj; + }, + /** + * Gets the current player error + */ + getError: function() { + if ( !$.isEmptyObject( this.playerError ) ) { + return this.playerError; + } + return null; + }, + + /** + * Show an error message on the player + * + * @param {object} + * errorObj + */ + showErrorMsg: function( errorObj ){ + // Remove a loading spinner + this.hideSpinnerAndPlayBtn(); + if( this.controlBuilder ) { + if( mw.config.get("EmbedPlayer.ShowPlayerAlerts") ) { + var alertObj = $.extend( errorObj, { + 'isModal': true, + 'keepOverlay': true, + 'noButtons': true, + 'isError': true + } ); + this.controlBuilder.displayAlert( alertObj ); + } + } + return ; + }, + + /** + * Blocks the player display by invoking an empty error msg + */ + blockPlayerDisplay: function(){ + this.showErrorMsg(); + this.getInterface().find( '.error' ).hide(); + }, + + /** + * Get missing plugin html (check for user included code) + * + * @param {String} + * [misssingType] missing type mime + */ + showPlayerError: function( ) { + var _this = this; + var $this = $( this ); + mw.log("EmbedPlayer::showPlayerError"); + // Hide loader + this.hideSpinnerAndPlayBtn(); + + // Error in loading media ( trigger the mediaLoadError ) + $this.trigger( 'mediaLoadError' ); + + // We don't distiguish between mediaError and mediaLoadError right now + // TODO fire mediaError only on failed to recive audio/video data. + $this.trigger( 'mediaError' ); + + // Check if we want to block the player display ( no error displayed ) + if( this['data-blockPlayerDisplay'] ){ + this.blockPlayerDisplay(); + return ; + } + + // Check if there is a more specific error: + if( this.getError() ){ + this.showErrorMsg( this.getError() ); + return ; + } + + // If no error is given assume missing sources: + this.showNoInlinePlabackSupport(); + }, + + /** + * Show player missing sources method + */ + showNoInlinePlabackSupport: function(){ + var _this = this; + var $this = $( this); + + // Check if any sources are avaliable: + if( this.mediaElement.sources.length == 0 + || + !mw.config.get('EmbedPlayer.NotPlayableDownloadLink') ) + { + return ; + } + // Set the isLink player flag: + this.isLinkPlayer= true; + // Update the poster and html: + this.updatePosterHTML(); + + // Make sure we have a play btn: + this.addLargePlayBtn(); + + // By default set the direct download url to the first source. + var downloadUrl = this.mediaElement.sources[0].getSrc(); + // Allow plugins to update the download url ( to point to server side tools to select + // stream based on user agent ( i.e IE8 h.264 file, blackberry 3gp file etc ) + this.triggerHelper( 'directDownloadLink', function( dlUrl ){ + if( dlUrl ){ + downloadUrl = dlUrl; + } + }); + // Set the play button to the first available source: + var $pBtn = this.getInterface().find('.play-btn-large') + .attr( 'title', mw.msg('mwe-embedplayer-play_clip') ) + .show() + .unbind( 'click' ) + .click( function() { + _this.triggerHelper( 'firstPlay', [ _this.id ] ); // To send stats event for play + _this.triggerHelper( 'playing' ); + return true; + }); + if( !$pBtn.parent('a').length ){ + $pBtn.wrap( $( '<a />' ).attr("target", "_blank" ) ); + } + $pBtn.parent('a').attr( "href", downloadUrl ); + + $( this ).trigger( 'showInlineDownloadLink' ); + }, + /** + * Update the video time request via a time request string + * + * @param {String} + * timeRequest video time to be updated + */ + updateVideoTimeReq: function( timeRequest ) { + mw.log( 'EmbedPlayer::updateVideoTimeReq:' + timeRequest ); + var timeParts = timeRequest.split( '/' ); + this.updateVideoTime( timeParts[0], timeParts[1] ); + }, + + /** + * Update Video time from provided startNpt and endNpt values + * + * @param {String} + * startNpt the new start time in npt format ( hh:mm:ss.ms ) + * @param {String} + * endNpt the new end time in npt format ( hh:mm:ss.ms ) + */ + updateVideoTime: function( startNpt, endNpt ) { + // update media + this.mediaElement.updateSourceTimes( startNpt, endNpt ); + + // update time + this.controlBuilder.setStatus( startNpt + '/' + endNpt ); + + // reset slider + this.updatePlayHead( 0 ); + + // Reset the serverSeekTime if urlTimeEncoding is enabled + if ( this.supportsURLTimeEncoding() ) { + this.serverSeekTime = 0; + } else { + this.serverSeekTime = mw.npt2seconds( startNpt ); + } + }, + + + /** + * Update Thumb time with npt formated time + * + * @param {String} + * time NPT formated time to update thumbnail + */ + updateThumbTimeNPT: function( time ) { + this.updateThumbTime( mw.npt2seconds( time ) - parseInt( this.startOffset ) ); + }, + + /** + * Update the thumb with a new time + * + * @param {Float} + * floatSeconds Time to update the thumb to + */ + updateThumbTime:function( floatSeconds ) { + // mw.log('updateThumbTime:'+floatSeconds); + var _this = this; + if ( typeof this.orgThumSrc == 'undefined' ) { + this.orgThumSrc = this.poster; + } + if ( this.orgThumSrc.indexOf( 't=' ) !== -1 ) { + this.lastThumbUrl = mw.replaceUrlParams( this.orgThumSrc, + { + 't' : mw.seconds2npt( floatSeconds + parseInt( this.startOffset ) ) + } + ); + if ( !this.thumbnailUpdatingFlag ) { + this.updatePoster( this.lastThumbUrl , false ); + this.lastThumbUrl = null; + } + } + }, + + /** + * Updates the displayed thumbnail via percent of the stream + * + * @param {Float} + * percent Percent of duration to update thumb + */ + updateThumbPerc:function( percent ) { + return this.updateThumbTime( ( this.getDuration() * percent ) ); + }, + + /** + * Update the poster source + * @param {String} + * posterSrc Poster src url + */ + updatePosterSrc: function( posterSrc ){ + if( ! posterSrc ) { + posterSrc = mw.config.get( 'EmbedPlayer.BlackPixel' ); + } + this.poster = posterSrc; + this.updatePosterHTML(); + this.applyIntrinsicAspect(); + }, + + /** + * Called after sources are updated, and your ready for the player to change media + * @return + */ + changeMedia: function( callback ){ + var _this = this; + var $this = $( this ); + mw.log( 'EmbedPlayer:: changeMedia '); + // Empty out embedPlayer object sources + this.emptySources(); + + // onChangeMedia triggered at the start of the change media commands + $this.trigger( 'onChangeMedia' ); + + // Reset first play to true, to count that play event + this.firstPlay = true; + // reset donePlaying count on change media. + this.donePlayingCount = 0; + this.triggeredEndDone = false; + this.preSequence = false; + this.postSequence = false; + + this.setCurrentTime( 0.01 ); + // Reset the playhead + this.updatePlayHead( 0 ); + // update the status: + this.controlBuilder.setStatus( this.getTimeRange() ); + + // Add a loader to the embed player: + this.pauseLoading(); + + // Clear out any player error ( both via attr and object property ): + this.setError( null ); + + // Clear out any player display blocks + this['data-blockPlayerDisplay'] = null + $this.attr( 'data-blockPlayerDisplay', ''); + + // Clear out the player error div: + this.getInterface().find('.error').remove(); + this.controlBuilder.closeAlert(); + this.controlBuilder.closeMenuOverlay(); + + // Restore the control bar: + this.getInterface().find('.control-bar').show(); + // Hide the play btn + this.hideLargePlayBtn(); + + //If we are change playing media add a ready binding: + var bindName = 'playerReady.changeMedia'; + $this.unbind( bindName ).bind( bindName, function(){ + mw.log('EmbedPlayer::changeMedia playerReady callback'); + // hide the loading spinner: + _this.hideSpinnerAndPlayBtn(); + // check for an erro on change media: + if( _this.getError() ){ + _this.showErrorMsg( _this.getError() ); + return ; + } + // Always show the control bar on switch: + if( _this.controlBuilder ){ + _this.controlBuilder.showControlBar(); + } + // Make sure the play button reflects the original play state + if( _this.autoplay ){ + _this.hideLargePlayBtn(); + } else { + _this.addLargePlayBtn(); + } + var source = _this.getSource(); + if( (_this.isPersistentNativePlayer() || _this.useNativePlayerControls()) && source ){ + // If switching a Persistent native player update the source: + // ( stop and play won't refresh the source ) + _this.switchPlaySource( source, function(){ + _this.changeMediaStarted = false; + $this.trigger( 'onChangeMediaDone' ); + if( _this.autoplay ){ + _this.play(); + } else { + // pause is need to keep pause sate, while + // switch source calls .play() that some browsers require. + // to reflect source swiches. + _this.pause(); + _this.addLargePlayBtn(); + } + if( callback ){ + callback() + } + }); + // we are handling trigger and callback asynchronously return here. + return ; + } + + // Reset changeMediaStarted flag + _this.changeMediaStarted = false; + + // Stop should unload the native player + _this.stop(); + + // reload the player + if( _this.autoplay ){ + _this.play(); + } else { + _this.addLargePlayBtn(); + } + + $this.trigger( 'onChangeMediaDone' ); + if( callback ) { + callback(); + } + }); + + // Load new sources per the entry id via the checkPlayerSourcesEvent hook: + $this.triggerQueueCallback( 'checkPlayerSourcesEvent', function(){ + // Start player events leading to playerReady + _this.setupSourcePlayer(); + }); + }, + /** + * Checks if the current player / configuration is an image play screen: + */ + isImagePlayScreen:function(){ + return ( this.useNativePlayerControls() && + !this.isLinkPlayer && + mw.isIphone() && + mw.config.get( 'EmbedPlayer.iPhoneShowHTMLPlayScreen') + ); + }, + /** + * Triggers widgetLoaded event - Needs to be triggered only once, at the first time playerReady is trigerred + */ + triggerWidgetLoaded: function() { + if ( !this.widgetLoaded ) { + this.widgetLoaded = true; + mw.log( "EmbedPlayer:: Trigger: widgetLoaded"); + this.triggerHelper( 'widgetLoaded' ); + } + }, + + /** + * Updates the poster HTML + */ + updatePosterHTML: function () { + mw.log( 'EmbedPlayer:updatePosterHTML::' + this.id ); + + var _this = this, + thumb_html = '', + class_atr = '', + style_atr = '', + profile = $.client.profile(); + + if( this.isImagePlayScreen() ){ + this.addPlayScreenWithNativeOffScreen(); + return ; + } + + // Set by default thumb value if not found + var posterSrc = ( this.poster ) ? this.poster : + mw.config.get( 'EmbedPlayer.BlackPixel' ); + + // Update PersistentNativePlayer poster: + if( this.isPersistentNativePlayer() ){ + var $vid = $( '#' + this.pid ).show(); + $vid.attr( 'poster', posterSrc ); + // Add a quick timeout hide / show ( firefox 4x bug with native poster updates ) + if ( profile.name === 'firefox' ){ + $vid.hide(); + setTimeout( function () { + $vid.show(); + }, 0); + } + } else { + // hide the pid if present: + $( '#' + this.pid ).hide(); + // Poster support is not very consistent in browsers use a jpg poster image: + $( this ) + .html( + $( '<img />' ) + .css({ + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'right': 0, + 'bottom': 0 + }) + .attr({ + 'src' : posterSrc + }) + .addClass( 'playerPoster' ) + .load(function(){ + _this.applyIntrinsicAspect(); + }) + ).show(); + } + if ( this.useLargePlayBtn() && this.controlBuilder + && + this.height > this.controlBuilder.getComponentHeight( 'playButtonLarge' ) + ) { + this.addLargePlayBtn(); + } + }, + /** + * Abstract method, must be set by player inteface + */ + addPlayScreenWithNativeOffScreen: function(){ + mw.log( "Error: EmbedPlayer, Must override 'addPlayScreenWithNativeOffScreen' with player inteface" ); + return ; + }, + /** + * Checks if a large play button should be displayed on the + * otherwise native player + */ + useLargePlayBtn: function(){ + if( this.isPersistantPlayBtn() ){ + return true; + } + // If we are using native controls return false: + return !this.useNativePlayerControls(); + }, + /** + * Checks if the play button should stay on screen during playback, + * cases where a native player is dipalyed such as iPhone. + */ + isPersistantPlayBtn: function(){ + return mw.isAndroid2() || + ( mw.isIphone() && mw.config.get( 'EmbedPlayer.iPhoneShowHTMLPlayScreen' ) ); + }, + /** + * Checks if native controls should be used + * + * @returns boolean true if the mwEmbed player interface should be used + * false if the mwEmbed player interface should not be used + */ + useNativePlayerControls: function() { + if( this.usenativecontrols === true ){ + return true; + } + + if( mw.config.get('EmbedPlayer.NativeControls') === true ) { + return true; + } + + // Check for special webkit property that allows inline iPhone playback: + if( mw.config.get('EmbedPlayer.WebKitPlaysInline') === true && mw.isIphone() ) { + return false; + } + + // Do some device detection devices that don't support overlays + // and go into full screen once play is clicked: + if( mw.isAndroid2() || mw.isIpod() || mw.isIphone() ){ + return true; + } + + // iPad can use html controls if its a persistantPlayer in the dom before loading ) + // else it needs to use native controls: + if( mw.isIpad() ){ + if( mw.config.get('EmbedPlayer.EnableIpadHTMLControls') === true){ + return false; + } else { + // Set warning that your trying to do iPad controls without + // persistent native player: + return true; + } + } + return false; + }, + /** + * Checks if the native player is persistent in the dom since the intial page build out. + */ + isPersistentNativePlayer: function(){ + if( this.isLinkPlayer ){ + return false; + } + // Since we check this early on sometimes the player + // has not yet been updated to the pid location + if( $('#' + this.pid ).length == 0 ){ + return $('#' + this.id ).hasClass('persistentNativePlayer'); + } + return $('#' + this.pid ).hasClass('persistentNativePlayer'); + }, + // + isTouchDevice: function(){ + return mw.isIpad() + || + mw.isAndroid40() + || + mw.isMobileChrome(); + }, + /** + * Hides the large play button + * TODO move to player controls + */ + hideLargePlayBtn: function(){ + if( this.getInterface() ){ + this.getInterface().find( '.play-btn-large' ).hide(); + } + }, + /** + * Add a play button (if not already there ) + */ + addLargePlayBtn: function(){ + // check if we are pauseLoading ( i.e switching media, seeking, etc. and don't display play btn: + if( this.isPauseLoading ){ + mw.log("EmbedPlayer:: addLargePlayBtn ( skip play button, during load )"); + return; + } + // if using native controls make sure we can click the big play button by restoring + // interface click events: + if( this.useNativePlayerControls() ){ + this.getInterface().css('pointer-events', 'auto'); + } + + // iPhone in WebKitPlaysInline mode does not support clickable overlays as of iOS 5.0 + if( mw.config.get( 'EmbedPlayer.WebKitPlaysInline') && mw.isIphone() ) { + return ; + } + if( this.getInterface().find( '.play-btn-large' ).length ){ + this.getInterface().find( '.play-btn-large' ).show(); + } else { + this.getVideoHolder().append( + this.controlBuilder.getComponent( 'playButtonLarge' ) + ); + } + }, + + getVideoHolder: function() { + return this.getInterface().find('.videoHolder'); + }, + + /** + * Abstract method, + * Get native player html ( should be set by mw.EmbedPlayerNative ) + */ + getNativePlayerHtml: function(){ + return $('<div />' ) + .css( 'width', this.getWidth() ) + .html( 'Error: Trying to get native html5 player without native support for codec' ); + }, + + /** + * Should be set via native embed support + */ + applyMediaElementBindings: function(){ + mw.log("Warning applyMediaElementBindings should be implemented by player interface" ); + return ; + }, + + /** + * Gets code to embed the player remotely for "share" this player links + */ + getSharingEmbedCode: function() { + switch( mw.config.get( 'EmbedPlayer.ShareEmbedMode' ) ){ + case 'iframe': + return this.getShareIframeObject(); + break; + case 'videojs': + return this.getShareEmbedVideoJs(); + break; + } + }, + + /** + * Gets code to embed the player in a wiki + */ + getWikiEmbedCode: function() { + if( this.apiTitleKey) { + return '[[File:' + this.apiTitleKey + ']]'; + } else { + return false; + } + }, + + /** + * Get the iframe share code: + */ + getShareIframeObject: function(){ + // TODO move to getShareIframeSrc + var iframeUrl = this.getIframeSourceUrl(); + + // Set up embedFrame src path + var embedCode = '<iframe src="' + mw.html.escape( iframeUrl ) + '" '; + + // Set width / height of embed object + embedCode += 'width="' + this.getPlayerWidth() +'" '; + embedCode += 'height="' + this.getPlayerHeight() + '" '; + embedCode += 'frameborder="0" '; + embedCode += 'webkitAllowFullScreen mozallowfullscreen allowFullScreen'; + + // Close up the embedCode tag: + embedCode+='></iframe>'; + + // Return the embed code + return embedCode; + }, + /** + * Gets the iframe source url + */ + getIframeSourceUrl: function(){ + var iframeUrl = false; + this.triggerHelper( 'getShareIframeSrc', [ function( localIframeSrc ){ + if( iframeUrl){ + mw.log("Error multiple modules binding getShareIframeSrc" ); + } + iframeUrl = localIframeSrc; + }, this.id ]); + if( iframeUrl ){ + return iframeUrl; + } + // old style embed: + var iframeUrl = mw.getMwEmbedPath() + 'mwEmbedFrame.php?'; + var params = {'src[]' : []}; + + // Output all the video sources: + for( var i=0; i < this.mediaElement.sources.length; i++ ){ + var source = this.mediaElement.sources[i]; + if( source.src ) { + params['src[]'].push(mw.absoluteUrl( source.src )); + } + } + // Output the poster attr + if( this.poster ){ + params.poster = this.poster; + } + + // Set the skin if set to something other than default + if( this.skinName ){ + params.skin = this.skinName; + } + + if( this.duration ) { + params.durationHint = parseFloat( this.duration ); + } + iframeUrl += $.param( params ); + return iframeUrl; + }, + /** + * Get the share embed Video tag html to share the embed code. + */ + getShareEmbedVideoJs: function(){ + + // Set the embed tag type: + var embedtag = ( this.isAudio() )? 'audio': 'video'; + + // Set up the mwEmbed js include: + var embedCode = '<script type="text/javascript" ' + + 'src="' + + mw.html.escape( + mw.absoluteUrl( + mw.getMwEmbedSrc() + ) + ) + '"></script>' + + '<' + embedtag + ' '; + + if( this.poster ) { + embedCode += 'poster="' + + mw.html.escape( mw.absoluteUrl( this.poster ) ) + + '" '; + } + + // Set the skin if set to something other than default + if( this.skinName ){ + embedCode += 'class="' + + mw.html.escape( this.skinName ) + + '" '; + } + + if( this.duration ) { + embedCode +='durationHint="' + parseFloat( this.duration ) + '" '; + } + + if( this.width || this.height ){ + embedCode += 'style="'; + embedCode += ( this.width )? 'width:' + this.width +'px;': ''; + embedCode += ( this.height )? 'height:' + this.height +'px;': ''; + embedCode += '" '; + } + + // Close the video attr + embedCode += '>'; + + // Output all the video sources: + for( var i=0; i < this.mediaElement.sources.length; i++ ){ + var source = this.mediaElement.sources[i]; + if( source.src ) { + embedCode +='<source src="' + + mw.absoluteUrl( source.src ) + + '" ></source>'; + } + } + // Close the video tag + embedCode += '</video>'; + + return embedCode; + }, + + + + /** + * Base Embed Controls + */ + + /** + * The Play Action + * + * Handles play requests, updates relevant states: + * seeking =false + * paused =false + * + * Triggers the play event + * + * Updates pause button Starts the "monitor" + */ + firstPlay : true, + preSequence: false, + inPreSequence: false, + replayEventCount : 0, + play: function() { + var _this = this; + var $this = $( this ); + // Store the absolute play time ( to track native events that should not invoke interface updates ) + mw.log( "EmbedPlayer:: play: " + this._propagateEvents + ' poster: ' + this.stopped ); + + this.absoluteStartPlayTime = new Date().getTime(); + + // Check if thumbnail is being displayed and embed html + if ( _this.isStopped() && (_this.preSequence == false || (_this.sequenceProxy && _this.sequenceProxy.isInSequence == false) )) { + if ( !_this.selectedPlayer ) { + _this.showPlayerError(); + return false; + } else { + _this.embedPlayerHTML(); + } + } + // playing, exit stopped state: + _this.stopped = false; + + if( !this.preSequence ) { + this.preSequence = true; + mw.log( "EmbedPlayer:: trigger preSequence " ); + this.triggerHelper( 'preSequence' ); + this.playInterfaceUpdate(); + // if we entered into ad loading return + if( _this.sequenceProxy && _this.sequenceProxy.isInSequence ){ + mw.log("EmbedPlayer:: isInSequence, do NOT play content"); + return false; + } + } + + // We need first play event for analytics purpose + if( this.firstPlay && this._propagateEvents) { + this.firstPlay = false; + this.triggerHelper( 'firstPlay', [ _this.id ] ); + } + + if( this.paused === true ){ + this.paused = false; + // Check if we should Trigger the play event + mw.log("EmbedPlayer:: trigger play event::" + !this.paused + ' events:' + this._propagateEvents ); + // trigger the actual play event: + if( this._propagateEvents ) { + this.triggerHelper( 'onplay' ); + } + } + + // If we previously finished playing this clip run the "replay hook" + if( this.donePlayingCount > 0 && !this.paused && this._propagateEvents ) { + this.replayEventCount++; + // Trigger end done on replay + this.triggeredEndDone = false; + if( this.replayEventCount <= this.donePlayingCount){ + mw.log("EmbedPlayer::play> trigger replayEvent"); + this.triggerHelper( 'replayEvent' ); + } + } + + // If we have start time defined, start playing from that point + if( this.currentTime < this.startTime ) { + $this.bind('playing.startTime', function(){ + $this.unbind('playing.startTime'); + if( !mw.isIOS() ){ + _this.setCurrentTime( _this.startTime ); + _this.startTime = 0; + } else { + // iPad seeking on syncronus play event sucks + setTimeout( function(){ + _this.setCurrentTime( _this.startTime, function(){ + _this.play(); + }); + _this.startTime = 0; + }, 500 ) + } + _this.startTime = 0; + }); + } + + this.playInterfaceUpdate(); + // If play controls are enabled continue to video content element playback: + if( _this._playContorls ){ + return true; + } else { + // return false ( Mock play event, or handled elsewhere ) + return false; + } + }, + /** + * Update the player inteface for playback + * TODO move to controlBuilder + */ + playInterfaceUpdate: function(){ + var _this = this; + mw.log( 'EmbedPlayer:: playInterfaceUpdate' ); + // Hide any overlay: + if( this.controlBuilder ){ + this.controlBuilder.closeMenuOverlay(); + } + // Hide any buttons or errors if present: + this.getInterface().find( '.error' ).remove(); + this.hideLargePlayBtn(); + + this.getInterface().find('.play-btn span') + .removeClass( 'ui-icon-play' ) + .addClass( 'ui-icon-pause' ); + + this.hideSpinnerOncePlaying(); + + this.getInterface().find( '.play-btn' ) + .unbind('click') + .click( function( ) { + if( _this._playContorls ){ + _this.pause(); + } + } ) + .attr( 'title', mw.msg( 'mwe-embedplayer-pause_clip' ) ); + }, + /** + * Pause player, and display a loading animation + * @return + */ + pauseLoading: function(){ + this.pause(); + this.addPlayerSpinner(); + this.isPauseLoading = true; + }, + /** + * Adds a loading spinner to the player. + */ + addPlayerSpinner: function(){ + var sId = 'loadingSpinner_' + this.id; + // remove any old spinner + $( '#' + sId ).remove(); + // hide the play btn if present + this.hideLargePlayBtn(); + // re add an absolute positioned spinner: + $( this ).show().getAbsoluteOverlaySpinner() + .attr( 'id', sId ); + }, + hideSpinner: function(){ + // remove the spinner + $( '#loadingSpinner_' + this.id + ',.loadingSpinner' ).remove(); + }, + /** + * Hides the loading spinner + */ + hideSpinnerAndPlayBtn: function(){ + this.isPauseLoading = false; + this.hideSpinner(); + // hide the play btn + this.hideLargePlayBtn(); + }, + /** + * Hides the loading spinner once playing. + */ + hideSpinnerOncePlaying: function(){ + this._checkHideSpinner = true; + }, + /** + * Base embed pause Updates the play/pause button state. + * + * There is no general way to pause the video must be overwritten by embed + * object to support this functionality. + * + * @param {Boolean} if the event was triggered by user action or propagated by js. + */ + pause: function() { + var _this = this; + // Trigger the pause event if not already paused and using native controls: + if( this.paused === false ){ + this.paused = true; + if( this._propagateEvents ){ + mw.log( 'EmbedPlayer:trigger pause:' + this.paused ); + // we only trigger "onpause" to avoid event propagation to the native object method + // i.e in jQuery ( this ).trigger('pause') also calls: this.pause(); + $( this ).trigger( 'onpause' ); + } + } + _this.pauseInterfaceUpdate(); + }, + /** + * Sets the player interface to paused mode. + */ + pauseInterfaceUpdate: function(){ + var _this =this; + mw.log("EmbedPlayer::pauseInterfaceUpdate"); + // Update the ctrl "paused state" + this.getInterface().find('.play-btn span' ) + .removeClass( 'ui-icon-pause' ) + .addClass( 'ui-icon-play' ); + + this.getInterface().find( '.play-btn' ) + .unbind('click') + .click( function() { + if( _this._playContorls ){ + _this.play(); + } + } ) + .attr( 'title', mw.msg( 'mwe-embedplayer-play_clip' ) ); + }, + /** + * Maps the html5 load request. There is no general way to "load" clips so + * underling plugin-player libs should override. + */ + load: function() { + // should be done by child (no base way to pre-buffer video) + mw.log( 'Waring:: the load method should be overided by player interface' ); + }, + + + /** + * Base embed stop + * + * Updates the player to the stop state. + * + * Shows Thumbnail + * Resets Buffer + * Resets Playhead slider + * Resets Status + * + * Trigger the "doStop" event + */ + stop: function() { + var _this = this; + mw.log( 'EmbedPlayer::stop:' + this.id ); + // update the player to stopped state: + this.stopped = true; + + // Rest the prequecne flag: + this.preSequence = false; + + // Trigger the stop event: + $( this ).trigger( 'doStop' ); + + // no longer seeking: + this.didSeekJump = false; + + // Reset current time and prev time and seek offset + this.currentTime = this.previousTime = this.serverSeekTime = 0; + + this.stopMonitor(); + + // pause playback ( if playing ) + if( !this.paused ){ + this.pause(); + } + // Restore the play button ( if not native controls or is android ) + if( this.useLargePlayBtn() ){ + this.addLargePlayBtn(); + this.pauseInterfaceUpdate(); + } + + // Native player controls: + if( !this.isPersistentNativePlayer() ){ + // Rewrite the html to thumbnail disp + this.showThumbnail(); + this.bufferedPercent = 0; // reset buffer state + this.controlBuilder.setStatus( this.getTimeRange() ); + } + // Reset the playhead + this.updatePlayHead( 0 ); + // update the status: + this.controlBuilder.setStatus( this.getTimeRange() ); + // reset buffer indicator: + this.bufferedPercent = 0; + this.updateBufferStatus(); + }, + + /** + * Base Embed mute + * + * Handles interface updates for toggling mute. Plug-in / player interface + * must handle the actual media player action + */ + toggleMute: function( userAction ) { + mw.log( 'EmbedPlayer::toggleMute> (old state:) ' + this.muted ); + if ( this.muted ) { + this.muted = false; + var percent = this.preMuteVolume; + } else { + this.muted = true; + this.preMuteVolume = this.volume; + var percent = 0; + } + // Change the volume and trigger the volume change so that other plugins can listen. + this.setVolume( percent, true ); + // Update the interface + this.setInterfaceVolume( percent ); + // trigger the onToggleMute event + $( this ).trigger('onToggleMute'); + }, + + /** + * Update volume function ( called from interface updates ) + * + * @param {float} + * percent Percent of full volume + * @param {triggerChange} + * boolean change if the event should be triggered + */ + setVolume: function( percent, triggerChange ) { + var _this = this; + // ignore NaN percent: + if( isNaN( percent ) ){ + return ; + } + // Set the local volume attribute + this.previousVolume = this.volume; + + this.volume = percent; + + // Un-mute if setting positive volume + if( percent != 0 ){ + this.muted = false; + } + + // Update the playerElement volume + this.setPlayerElementVolume( percent ); + //mw.log("EmbedPlayer:: setVolume:: " + percent + ' trigger volumeChanged: ' + triggerChange ); + if( triggerChange ){ + $( _this ).trigger('volumeChanged', percent ); + } + }, + + /** + * Updates the interface volume + * + * TODO should move to controlBuilder + * + * @param {float} + * percent Percentage volume to update interface + */ + setInterfaceVolume: function( percent ) { + if( this.supports[ 'volumeControl' ] && + this.getInterface().find( '.volume-slider' ).length + ) { + this.getInterface().find( '.volume-slider' ).slider( 'value', percent * 100 ); + } + }, + + /** + * Abstract method Update volume Method must be override by plug-in / player interface + * + * @param {float} + * percent Percentage volume to update + */ + setPlayerElementVolume: function( percent ) { + mw.log('Error player does not support volume adjustment' ); + }, + + /** + * Abstract method get volume Method must be override by plug-in / player interface + * (if player does not override we return the abstract player value ) + */ + getPlayerElementVolume: function(){ + // mw.log(' error player does not support getting volume property' ); + return this.volume; + }, + + /** + * Abstract method get volume muted property must be overwritten by plug-in / + * player interface (if player does not override we return the abstract + * player value ) + */ + getPlayerElementMuted: function(){ + // mw.log(' error player does not support getting mute property' ); + return this.muted; + }, + + /** + * Passes a fullscreen request to the controlBuilder interface + */ + fullscreen: function() { + this.controlBuilder.toggleFullscreen(); + }, + + /** + * Abstract method to be run post embedding the player Generally should be + * overwritten by the plug-in / player + */ + postEmbedActions:function() { + return ; + }, + + /** + * Checks the player state based on thumbnail display & paused state + * + * @return {Boolean} true if playing false if not playing + */ + isPlaying : function() { + if ( this.stopped ) { + // in stopped state + return false; + } else if ( this.paused ) { + // paused state + return false; + } else { + return true; + } + }, + + /** + * Get Stopped state + * + * @return {Boolean} true if stopped false if playing + */ + isStopped: function() { + return this.stopped; + }, + /** + * Stop the play state monitor + */ + stopMonitor: function(){ + clearInterval( this.monitorInterval ); + this.monitorInterval = 0; + }, + /** + * Start the play state monitor + */ + startMonitor: function(){ + this.monitor(); + }, + + /** + * Monitor playback and update interface components. underling player classes + * are responsible for updating currentTime + */ + monitor: function() { + var _this = this; + + // Check for current time update outside of embed player + _this.syncCurrentTime(); + + // mw.log( "monitor:: " + this.currentTime + ' propagateEvents: ' + _this._propagateEvents ); + + // update player status + _this.updatePlayheadStatus(); + + // Keep volume proprties set outside of the embed player in sync + _this.syncVolume(); + + // Make sure the monitor continues to run as long as the video is not stoped + _this.syncMonitor() + + if( _this._propagateEvents ){ + + // mw.log('trigger:monitor:: ' + this.currentTime ); + $( _this ).trigger( 'monitorEvent', [ _this.id ] ); + + // Trigger the "progress" event per HTML5 api support + if( _this.progressEventData ) { + $( _this ).trigger( 'progress', _this.progressEventData ); + } + } + }, + /** + * Sync the monitor function + */ + syncMonitor: function(){ + var _this = this; + // Call monitor at this.monitorRate interval. + // ( use setInterval to avoid stacking monitor requests ) + if( ! this.isStopped() ) { + if( !this.monitorInterval ){ + this.monitorInterval = setInterval( function(){ + if( _this.monitor ) + _this.monitor(); + }, this.monitorRate ); + } + } else { + // If stopped "stop" monitor: + this.stopMonitor(); + } + }, + + /** + * Sync the video volume + */ + syncVolume: function(){ + var _this = this; + // Check if volume was set outside of embed player function + // mw.log( ' this.volume: ' + _this.volume + ' prev Volume:: ' + _this.previousVolume ); + if( Math.round( _this.volume * 100 ) != Math.round( _this.previousVolume * 100 ) ) { + _this.setInterfaceVolume( _this.volume ); + } + // Update the previous volume + _this.previousVolume = _this.volume; + + // Update the volume from the player element + _this.volume = this.getPlayerElementVolume(); + + // update the mute state from the player element + if( _this.muted != _this.getPlayerElementMuted() && ! _this.isStopped() ){ + mw.log( "EmbedPlayer::syncVolume: muted does not mach embed player" ); + _this.toggleMute(); + // Make sure they match: + _this.muted = _this.getPlayerElementMuted(); + } + }, + + /** + * Checks if the currentTime was updated outside of the getPlayerElementTime function + */ + syncCurrentTime: function(){ + var _this = this; + + // Hide the spinner once we have time update: + if( _this._checkHideSpinner && _this.currentTime != _this.getPlayerElementTime() ){ + _this._checkHideSpinner = false; + _this.hideSpinnerAndPlayBtn(); + + if( _this.isPersistantPlayBtn() ){ + // add the play button likely iphone or native player that needs the play button on + // non-event "exit native html5 player" + _this.addLargePlayBtn(); + } else{ + // also hide the play button ( in case it was there somehow ) + _this.hideLargePlayBtn(); + } + } + + // Check if a javascript currentTime change based seek has occurred + if( parseInt( _this.previousTime ) != parseInt( _this.currentTime ) && + !this.userSlide && + !this.seeking && + !this.isStopped() + ){ + // If the time has been updated and is in range issue a seek + if( _this.getDuration() && _this.currentTime <= _this.getDuration() ){ + var seekPercent = _this.currentTime / _this.getDuration(); + mw.log("EmbedPlayer::syncCurrentTime::" + _this.previousTime + ' != ' + + _this.currentTime + " javascript based currentTime update to " + + seekPercent + ' == ' + _this.currentTime ); + _this.previousTime = _this.currentTime; + this.seek( seekPercent ); + } + } + + // Update currentTime via embedPlayer + _this.currentTime = _this.getPlayerElementTime(); + + // Update any offsets from server seek + if( _this.serverSeekTime && _this.supportsURLTimeEncoding() ){ + _this.currentTime = parseInt( _this.serverSeekTime ) + parseInt( _this.getPlayerElementTime() ); + } + + // Update the previousTime ( so we can know if the user-javascript changed currentTime ) + _this.previousTime = _this.currentTime; + + // Check for a pauseTime to stop playback in temporal media fragments + if( _this.pauseTime && _this.currentTime > _this.pauseTime ){ + _this.pause(); + _this.pauseTime = null; + } + }, + /** + * Updates the player time and playhead position based on currentTime + */ + updatePlayheadStatus: function(){ + var _this = this; + if ( this.currentTime >= 0 && this.duration ) { + if ( !this.userSlide && !this.seeking ) { + if ( parseInt( this.startOffset ) != 0 ) { + this.updatePlayHead( ( this.currentTime - this.startOffset ) / this.duration ); + var et = ( this.controlBuilder.longTimeDisp ) ? '/' + mw.seconds2npt( parseFloat( this.startOffset ) + parseFloat( this.duration ) ) : ''; + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + et ); + } else { + // use raw currentTIme for playhead updates + var ct = ( this.getPlayerElement() ) ? this.getPlayerElement().currentTime || this.currentTime: this.currentTime; + this.updatePlayHead( ct / this.duration ); + // Only include the end time if longTimeDisp is enabled: + var et = ( this.controlBuilder.longTimeDisp ) ? '/' + mw.seconds2npt( this.duration ) : ''; + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + et ); + } + } + // Check if we are "done" + var endPresentationTime = ( this.startOffset ) ? ( this.startOffset + this.duration ) : this.duration; + if ( this.currentTime >= endPresentationTime && !this.isStopped() ) { + mw.log( "EmbedPlayer::updatePlayheadStatus > should run clip done :: " + this.currentTime + ' > ' + endPresentationTime ); + this.onClipDone(); + } + } else { + // Media lacks duration just show end time + if ( this.isStopped() ) { + this.controlBuilder.setStatus( this.getTimeRange() ); + } else if ( this.paused ) { + this.controlBuilder.setStatus( mw.msg( 'mwe-embedplayer-paused' ) ); + } else if ( this.isPlaying() ) { + if ( this.currentTime && ! this.duration ) + this.controlBuilder.setStatus( mw.seconds2npt( this.currentTime ) + ' /' ); + else + this.controlBuilder.setStatus( " - - - " ); + } else { + this.controlBuilder.setStatus( this.getTimeRange() ); + } + } + }, + + /** + * Abstract getPlayerElementTime function + */ + getPlayerElementTime: function(){ + mw.log("Error: getPlayerElementTime should be implemented by embed library"); + }, + + /** + * Abstract getPlayerElementTime function + */ + getPlayerElement: function(){ + mw.log("Error: getPlayerElement should be implemented by embed library, or you may be calling this event too soon"); + }, + + /** + * Update the Buffer status based on the local bufferedPercent var + */ + updateBufferStatus: function() { + // Get the buffer target based for playlist vs clip + var $buffer = this.getInterface().find( '.mw_buffer' ); + // Update the buffer progress bar (if available ) + if ( this.bufferedPercent != 0 ) { + // mw.log('Update buffer css: ' + ( this.bufferedPercent * 100 ) + + // '% ' + $buffer.length ); + if ( this.bufferedPercent > 1 ){ + this.bufferedPercent = 1; + } + $buffer.css({ + "width" : ( this.bufferedPercent * 100 ) + '%' + }); + $( this ).trigger( 'updateBufferPercent', this.bufferedPercent ); + } else { + $buffer.css( "width", '0px' ); + } + + // if we have not already run the buffer start hook + if( this.bufferedPercent > 0 && !this.bufferStartFlag ) { + this.bufferStartFlag = true; + mw.log("EmbedPlayer::bufferStart"); + $( this ).trigger( 'bufferStartEvent' ); + } + + // if we have not already run the buffer end hook + if( this.bufferedPercent == 1 && !this.bufferEndFlag){ + this.bufferEndFlag = true; + $( this ).trigger( 'bufferEndEvent' ); + } + }, + + /** + * Update the player playhead + * + * @param {Float} + * perc Value between 0 and 1 for position of playhead + */ + updatePlayHead: function( perc ) { + //mw.log( 'EmbedPlayer: updatePlayHead: '+ perc); + if( this.getInterface() ){ + var $playHead = this.getInterface().find( '.play_head' ); + if ( !this.useNativePlayerControls() && $playHead.length != 0 ) { + var val = parseInt( perc * 1000 ); + $playHead.slider( 'value', val ); + } + } + $( this ).trigger('updatePlayHeadPercent', perc); + }, + + + /** + * Helper Functions for selected source + */ + + /** + * Get the current selected media source or first source + * + * @param {Number} + * Requested time in seconds to be passed to the server if the + * server supports supportsURLTimeEncoding + * @return src url + */ + getSrc: function( serverSeekTime ) { + if( serverSeekTime ){ + this.serverSeekTime = serverSeekTime; + } + if( this.currentTime && !this.serverSeekTime){ + this.serverSeekTime = this.currentTime; + } + + // No media element we can't return src + if( !this.mediaElement ){ + return false; + } + + // If no source selected auto select the source: + if( !this.mediaElement.selectedSource ){ + this.mediaElement.autoSelectSource(); + }; + + // Return selected source: + if( this.mediaElement.selectedSource ){ + // See if we should pass the requested time to the source generator: + if( this.supportsURLTimeEncoding() ){ + // get the first source: + return this.mediaElement.selectedSource.getSrc( this.serverSeekTime ); + } else { + return this.mediaElement.selectedSource.getSrc(); + } + } + // No selected source return false: + return false; + }, + /** + * Return the currently selected source + */ + getSource: function(){ + // update the current selected source: + this.mediaElement.autoSelectSource(); + return this.mediaElement.selectedSource; + }, + /** + * Static helper to get media sources from a set of videoFiles + * + * Uses mediaElement select logic to chose a + * video file among a set of sources + * + * @param videoFiles + * @return + */ + getCompatibleSource: function( videoFiles ){ + // Convert videoFiles json into HTML element: + // TODO mediaElement should probably accept JSON + var $media = $('<video />'); + $.each(videoFiles, function( inx, source){ + $media.append( $('<source />').attr({ + 'src' : source.src, + 'type' : source.type + })); + mw.log("EmbedPlayer::getCompatibleSource: add " + source.src + ' of type:' + source.type ); + }); + var myMediaElement = new mw.MediaElement( $media[0] ); + var source = myMediaElement.autoSelectSource(); + if( source ){ + mw.log("EmbedPlayer::getCompatibleSource: " + source.getSrc()); + return source; + } + mw.log("Error:: could not find compatible source"); + return false; + }, + /** + * If the selected src supports URL time encoding + * + * @return {Boolean} true if the src supports url time requests false if the + * src does not support url time requests + */ + supportsURLTimeEncoding: function() { + var timeUrls = mw.config.get('EmbedPlayer.EnableURLTimeEncoding') ; + if( timeUrls == 'none' ){ + return false; + } else if( timeUrls == 'always' ){ + return this.mediaElement.selectedSource.URLTimeEncoding; + } else if( timeUrls == 'flash' ){ + if( this.mediaElement.selectedSource && this.mediaElement.selectedSource.URLTimeEncoding){ + // see if the current selected player is flash: + return ( this.instanceOf == 'Kplayer' ); + } + } else { + mw.log("Error:: invalid config value for EmbedPlayer.EnableURLTimeEncoding:: " + mw.config.get('EmbedPlayer.EnableURLTimeEncoding') ); + } + return false; + } + }; + +})( mw, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerGeneric.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerGeneric.js new file mode 100644 index 00000000..fc8dec55 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerGeneric.js @@ -0,0 +1,36 @@ +/* +* Simple embed object for unknown application/ogg plugin +*/ + +( function( mw, $ ) { "use strict"; + +mw.EmbedPlayerGeneric = { + // List of supported features of the generic plugin + supports: { + 'playHead':false, + 'pause':false, + 'stop':true, + 'fullscreen':false, + 'timeDisplay':false, + 'volumeControl':false + }, + + // Instance name: + instanceOf:'Generic', + + /* + * Generic embed html + * + * @return {String} + * embed code for generic ogg plugin + */ + embedPlayerHTML: function() { + $( this ).html( + '<object type="application/ogg" ' + + 'width="' + this.getWidth() + '" height="' + this.getHeight() + '" ' + + 'data="' + this.getSrc( this.seekTimeSec ) + '"></object>' + ); + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.css b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.css new file mode 100644 index 00000000..9cf7b490 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.css @@ -0,0 +1,18 @@ +.mwEmbedPlayer .iewebm-prompt { + position: absolute; + top: 100px; + bottom: 60px; + left: 20px; + right: 20px; + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.75); + + width: auto !important; + height: auto !important; + + padding: 20px; +} + +.mwEmbedPlayer .iewebm-prompt div { + color: black; +}
\ No newline at end of file diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.js new file mode 100644 index 00000000..00694f81 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.js @@ -0,0 +1,46 @@ +/** + * Show a prompt to install WebM plugin for IE 9+ + */ + +( function( mw, $ ) { "use strict"; + +mw.EmbedPlayerIEWebMPrompt = { + // 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:'IEWebMPrompt', + + /* + * Embed this "fake" player + * + * @return {String} + * embed code to link to WebM plugin download + */ + embedPlayerHTML: function() { + var pluginUrl = 'https://tools.google.com/dlpage/webmmf/', + $link; + + // Overlay the video placeholder with download plugin link. + $link = $( '<a></a>' ) + .attr( 'href', pluginUrl ) + .attr( 'target', '_blank' ) + .text( mw.msg( 'mwe-embedplayer-iewebmprompt-linktext' ) ); + $( this ).append( $( '<div class="iewebm-prompt"></div>' ) + .width( this.getWidth() ) + .height( this.getHeight() ) + .append( $( '<div></div>' ).text( mw.msg( 'mwe-embedplayer-iewebmprompt-intro' ) ) ) + .append( $link ) + .append( $( '<div></div>' ).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 = + $( '<img />' ) + .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="<h2>Flash version "+b.version+ +" or greater is required</h2><h3>"+(g[0]>0?"Your version is "+g:"You have no flash plugin installed")+"</h3>"+(a.tagName=="A"?"<p>Click here to download latest version</p>":"<p>Download latest version from <a href='"+k+"'>here</a></p>");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='<object width="'+a.width+'" height="'+a.height+'" id="'+a.id+'" name="'+a.id+'"';if(a.cachebusting)a.src+=(a.src.indexOf("?")!=-1?"&":"?")+Math.random();c+=a.w3c||!i?' data="'+a.src+'" type="application/x-shockwave-flash"':' classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"';c+=">";if(a.w3c||i)c+='<param name="movie" value="'+a.src+'" />';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+= +'<param name="'+d+'" value="'+a[d]+'" />';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+='<param name="flashvars" value=\''+a+"' />"}c+="</object>";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( + $('<img />').css({ + 'margin' : '0', + 'width': '100%', + 'height': '100%' + }) + .attr( 'src', posterSrc) + .addClass('playerPoster') + ) + } + $( this ).show(); + }, + /** + * Return the embed code + */ + embedPlayerHTML : function () { + var _this = this; + var vid = _this.getPlayerElement(); + this.isFirstEmbedPlay = true; + + // Check if we should have a play button on the native player: + if( this.useLargePlayBtn() ){ + this.addLargePlayBtn(); + } + + if( vid && $( vid ).attr('src') == this.getSrc( this.currentTime ) ){ + _this.postEmbedActions(); + return ; + } + mw.log( "EmbedPlayerNative::embedPlayerHTML > play url:" + this.getSrc( this.currentTime ) + ' startOffset: ' + this.start_ntp + ' end: ' + this.end_ntp ); + + // Check if using native controls and already the "pid" is already in the DOM + if( this.isPersistentNativePlayer() && vid ) { + _this.postEmbedActions(); + return ; + } + // Reset some play state flags: + _this.bufferStartFlag = false; + _this.bufferEndFlag = false; + + $( this ).html( + _this.getNativePlayerHtml() + ); + + // Directly run postEmbedActions ( if playerElement is not available it will retry ) + _this.postEmbedActions(); + }, + + /** + * Get the native player embed code. + * + * @param {object} playerAttributes Attributes to be override in function call + * @return {object} cssSet css to apply to the player + */ + getNativePlayerHtml: function( playerAttributes, cssSet ){ + if( !playerAttributes) { + playerAttributes = {}; + } + // Update required attributes + if( !playerAttributes['id'] ){ + playerAttributes['id'] = this.pid; + } + if( !playerAttributes['src'] ){ + playerAttributes['src'] = this.getSrc( this.currentTime); + } + + // If autoplay pass along to attribute ( needed for iPad / iPod no js autoplay support + if( this.autoplay ) { + playerAttributes['autoplay'] = 'true'; + } + + if( !cssSet ){ + cssSet = {}; + } + + // Set default width height to 100% of parent container + if( !cssSet['width'] ) cssSet['width'] = '100%'; + if( !cssSet['height'] ) cssSet['height'] = '100%'; + + // Also need to set the loop param directly for iPad / iPod + if( this.loop ) { + playerAttributes['loop'] = 'true'; + } + + var tagName = this.isAudio() ? 'audio' : 'video'; + + return $( '<' + tagName + ' />' ) + // Add the special nativeEmbedPlayer to avoid any rewrites of of this video tag. + .addClass( 'nativeEmbedPlayerPid' ) + .attr( playerAttributes ) + .css( cssSet ) + }, + + /** + * Post element javascript, binds event listeners and starts monitor + */ + postEmbedActions: function() { + var _this = this; + // Setup local pointer: + var vid = this.getPlayerElement(); + if( !vid ){ + return ; + } + // Update the player source ( if needed ) + if( $( vid).attr( 'src' ) != this.getSrc( this.currentTime ) ){ + $( vid ).attr( 'src', this.getSrc( this.currentTime ) ); + } + // Update the WebKitPlaysInline value + if( mw.config.get( 'EmbedPlayer.WebKitPlaysInline') ){ + $( vid ).attr( 'webkit-playsinline', 1 ); + } + // Update the EmbedPlayer.WebKitAllowAirplay option: + if( mw.config.get( 'EmbedPlayer.WebKitAllowAirplay' ) ){ + $( vid ).attr( 'x-webkit-airplay', "allow" ); + } + // make sure to display native controls if enabled: + if( this.useNativePlayerControls() ){ + $( vid ).attr( 'controls', "true" ); + } + + // Apply media element bindings: + _this.applyMediaElementBindings(); + + // Make sure we start playing in the correct place: + if( this.currentTime != vid.currentTime ){ + var waitReadyStateCount = 0; + var checkReadyState = function(){ + if( vid.readyState > 0 ){ + vid.currentTime = this.currentTime; + return ; + } + if( waitReadyStateCount > 1000 ){ + mw.log("Error: EmbedPlayerNative: could not run native seek"); + return ; + } + waitReadyStateCount++; + setTimeout( function() { + checkReadyState(); + }, 10 ); + }; + } + // Some mobile devices ( iOS need a load call before play will work ) + if ( !_this.loop ) { + vid.load(); + } + }, + /** + * Apply media element bindings + */ + applyMediaElementBindings: function(){ + var _this = this; + mw.log("EmbedPlayerNative::MediaElementBindings"); + var vid = this.getPlayerElement(); + if( ! vid ){ + mw.log( " Error: applyMediaElementBindings without player elemnet"); + return ; + } + $.each( _this.nativeEvents, function( inx, eventName ){ + $( vid ).unbind( eventName + '.embedPlayerNative').bind( eventName + '.embedPlayerNative', function(){ + if( _this._propagateEvents ){ + var argArray = $.makeArray( arguments ); + // always pass the current ref id as the last argument + // helps check against some event trigger ref issues in jQuery + argArray.push( _this.id ); + // Check if there is local handler: + if( _this[ '_on' + eventName ] ){ + _this[ '_on' + eventName ].apply( _this, argArray); + } else { + // No local handler directly propagate the event to the abstract object: + $( _this ).trigger( eventName, argArray ); + } + } + }); + }); + }, + + // basic monitor function to update buffer + monitor: function(){ + var _this = this; + var vid = _this.getPlayerElement(); + + // Update the bufferedPercent + if( vid && vid.buffered && vid.buffered.end && vid.duration ) { + try{ + this.bufferedPercent = ( vid.buffered.end(0) / vid.duration ); + } catch ( e ){ + // opera does not have buffered.end zero index support ? + } + } + _this.parent_monitor(); + }, + + + /** + * Issue a seeking request. + * + * @param {Float} percent + * @param {bollean} stopAfterSeek if the player should stop after the seek + */ + seek: function( percent, stopAfterSeek ) { + // bounds check + if( percent < 0 ){ + percent = 0; + } + + if( percent > 1 ){ + percent = 1; + } + mw.log( 'EmbedPlayerNative::seek p: ' + percent + ' : ' + this.supportsURLTimeEncoding() + ' dur: ' + this.getDuration() + ' sts:' + this.seekTimeSec ); + + // Trigger preSeek event for plugins that want to store pre seek conditions. + this.triggerHelper( 'preSeek', percent ); + + this.seeking = true; + // Update the current time ( local property ) + this.currentTime = ( percent * this.duration ).toFixed( 2 ) ; + + // trigger the seeking event: + mw.log( 'EmbedPlayerNative::seek:trigger' ); + this.triggerHelper( 'seeking' ); + + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + + // @@todo check if the clip is loaded here (if so we can do a local seek) + if ( this.supportsURLTimeEncoding() ) { + // Make sure we could not do a local seek instead: + if ( percent < this.bufferedPercent && this.playerElement.duration && !this.didSeekJump ) { + mw.log( "EmbedPlayerNative::seek local seek " + percent + ' is already buffered < ' + this.bufferedPercent ); + this.doNativeSeek( percent ); + } else { + // We support URLTimeEncoding call parent seek: + this.parent_seek( percent ); + } + } else { + // Try to do a play then seek: + this.doNativeSeek( percent ); + } + }, + + /** + * Do a native seek by updating the currentTime + * @param {float} percent + * Percent to seek to of full time + */ + doNativeSeek: function( percent, callback ) { + // If player already seeking, exit + var _this = this; + // chrome crashes with multiple seeks: + if( (navigator.userAgent.indexOf('Chrome') === -1) && _this.playerElement.seeking ) { + return ; + } + + mw.log( 'EmbedPlayerNative::doNativeSeek::' + percent ); + this.seeking = true; + + this.seekTimeSec = 0; + + // Hide iPad video off screen ( iOS shows quicktime logo during seek ) + if( mw.isIOS() ){ + this.hidePlayerOffScreen(); + } + + this.setCurrentTime( ( percent * this.duration ) , function(){ + // Update the current time ( so that there is not a monitor delay in reflecting "seeked time" ) + _this.currentTime = _this.getPlayerElement().currentTime; + // Done seeking ( should be a fallback trigger event ) : + if( _this.seeking ){ + _this.seeking = false; + $( _this ).trigger( 'seeked' ); + } + // restore iPad video position: + _this.restorePlayerOnScreen(); + + _this.monitor(); + // issue the callback: + if( callback ){ + callback(); + } + }); + }, + + /** + * Seek in a existing stream, we first play then seek to work around issues with iPad seeking. + * + * @param {Float} percent + * percent of the stream to seek to between 0 and 1 + */ + doPlayThenSeek: function( percent ) { + mw.log( 'EmbedPlayerNative::doPlayThenSeek::' + percent + ' isPaused ' + this.paused); + var _this = this; + var oldPauseState = this.paused; + this.play(); + var retryCount = 0; + var readyForSeek = function() { + _this.getPlayerElement(); + // If we have duration then we are ready to do the seek + if ( _this.playerElement && _this.playerElement.duration ) { + _this.doNativeSeek( percent, function(){ + // restore pause if paused: + if( oldPauseState ){ + _this.pause(); + } + } ); + } else { + // Try to get player for 30 seconds: + // (it would be nice if the onmetadata type callbacks where fired consistently) + if ( retryCount < 800 ) { + setTimeout( readyForSeek, 10 ); + retryCount++; + } else { + mw.log( 'EmbedPlayerNative:: Error: doPlayThenSeek failed :' + _this.playerElement.duration); + } + } + }; + readyForSeek(); + }, + + /** + * Set the current time with a callback + * + * @param {Float} position + * Seconds to set the time to + * @param {Function} callback + * Function called once time has been set. + */ + setCurrentTime: function( seekTime , callback, callbackCount ) { + var _this = this; + if( !callbackCount ){ + callbackCount = 0; + } + mw.log( "EmbedPlayerNative:: setCurrentTime seekTime:" + seekTime + ' count:' + callbackCount ); + + // Make sure all the timeouts don't seek to an expired target: + $( this ).data('currentSeekTarget', seekTime ); + + var vid = this.getPlayerElement(); + // add a callback handler to null out callback: + var callbackHandler = function(){ + if( $.isFunction( callback ) ){ + callback(); + callback = null; + } + } + if( !vid ) { + callbackHandler(); + _this.currentSeekTargetTime = seekTime.toFixed( 2 ); + return; + } + // Check if player is ready for seek: + if( vid.readyState < 1 ){ + // Try to seek for 4 seconds: + if( callbackCount >= 40 ){ + mw.log("Error:: EmbedPlayerNative: with seek request, media never in ready state"); + callbackHandler(); + return ; + } + setTimeout( function(){ + // Check that this seek did not expire: + if( $( _this ).data('currentSeekTarget') != seekTime ){ + mw.log("EmbedPlayerNative:: expired seek target"); + return ; + } + _this.setCurrentTime( seekTime, callback , callbackCount+1); + }, 100 ); + return ; + } + // Check if currentTime is already set to the seek target: + if( vid.currentTime.toFixed(2) == seekTime.toFixed(2) ){ + mw.log("EmbedPlayerNative:: setCurrentTime: current time matches seek target: " + + vid.currentTime.toFixed(2) + ' == ' + seekTime.toFixed(2) ); + callbackHandler(); + return; + } + // setup a namespaced seek bind: + var seekBind = 'seeked.nativeSeekBind'; + + // Remove any old listeners + $( vid ).unbind( seekBind ); + // Bind a seeked listener for the callback + $( vid ).bind( seekBind, function( event ) { + // Remove the listener: + $( vid ).unbind( seekBind ); + + // Check if seeking to zero: + if( seekTime == 0 && vid.currentTime == 0 ){ + callbackHandler(); + return ; + } + + // Check if we got a valid seek: + if( vid.currentTime > 0 ){ + callbackHandler(); + } else { + mw.log( "Error:: EmbedPlayerNative: seek callback without time updatet " + vid.currentTime ); + } + }); + setTimeout(function(){ + // Check that this seek did not expire: + if( $( _this ).data('currentSeekTarget') != seekTime ){ + mw.log("EmbedPlayerNative:: Expired seek target"); + return ; + } + + if( $.isFunction( callback ) ){ + // if seek is within 5 seconds of the target assume success. ( key frame intervals can mess with seek accuracy ) + // this only runs where the seek callback failed ( i.e broken html5 seek ? ) + if( Math.abs( vid.currentTime - seekTime ) < 5 ){ + mw.log( "EmbedPlayerNative:: Seek time is within 5 seconds of target, sucessfull seek"); + callback(); + } else { + mw.log( "Error:: EmbedPlayerNative: Seek still has not made a callback after 5 seconds, retry"); + _this.setCurrentTime( seekTime, callback , callbackCount++ ); + } + } + }, 5000); + + // Try to update the playerElement time: + try { + _this.currentSeekTargetTime = seekTime.toFixed( 2 ); + // use toFixed ( iOS issue with float seek times ) + vid.currentTime = _this.currentSeekTargetTime; + } catch ( e ) { + mw.log("Error:: EmbedPlayerNative: Could not set video tag seekTime"); + callbackHandler(); + return ; + } + + // Check for seeking state ( some player iOS / iPad can only seek while playing ) + if(! vid.seeking ){ + mw.log( "Error:: not entering seek state, play and wait for positive time" ); + vid.play(); + setTimeout(function(){ + _this.waitForPositiveCurrentTime( function(){ + mw.log("EmbedPlayerNative:: Got possitive time:" + vid.currentTime.toFixed(3) + ", trying to seek again"); + _this.setCurrentTime( seekTime , callback, callbackCount+1 ); + }); + }, mw.config.get( 'EmbedPlayer.MonitorRate' ) ); + } + }, + waitForPositiveCurrentTime: function( callback ){ + var _this = this; + var vid = this.getPlayerElement(); + this.waitForPositiveCurrentTimeCount++; + // Wait for playback for 10 seconds + if( vid.currentTime > 0 ){ + mw.log( 'EmbedPlayerNative:: waitForPositiveCurrentTime success' ); + callback(); + } else if( this.waitForPositiveCurrentTimeCount > 200 ){ + mw.log( "Error:: waitForPositiveCurrentTime failed to reach possitve time"); + callback(); + } else { + setTimeout(function(){ _this.waitForPositiveCurrentTime( callback ) }, 50 ) + } + }, + /** + * Get the embed player time + */ + getPlayerElementTime: function() { + var _this = this; + // Make sure we have .vid obj + this.getPlayerElement(); + if ( !this.playerElement ) { + mw.log( 'EmbedPlayerNative::getPlayerElementTime: ' + this.id + ' not in dom ( stop monitor)' ); + this.stop(); + return false; + } + var ct = this.playerElement.currentTime; + // Return 0 or a positive number: + if( ! ct || isNaN( ct ) || ct < 0 || ! isFinite( ct ) ){ + return 0; + } + // Return the playerElement currentTime + return this.playerElement.currentTime; + }, + + // Update the poster src ( updates the native object if in dom ) + updatePosterSrc: function( src ){ + if( this.getPlayerElement() ){ + $( this.getPlayerElement() ).attr('poster', src ); + } + // Also update the embedPlayer poster + this.parent_updatePosterSrc( src ); + }, + /** + * Empty player sources from the active video tag element + */ + emptySources: function(){ + // empty player source: + $( this.getPlayerElement() ).attr( 'src', null ); + // empty out generic sources: + this.parent_emptySources(); + }, + /** + * playerSwitchSource switches the player source working around a few bugs in browsers + * + * @param {Object} + * Source object to switch to. + * @param {function} + * switchCallback Function to call once the source has been switched + * @param {function} + * doneCallback Function to call once the clip has completed playback + */ + playerSwitchSource: function( source, switchCallback, doneCallback ){ + var _this = this; + var src = source.getSrc(); + var vid = this.getPlayerElement(); + var switchBindPostfix = '.playerSwitchSource'; + this.isPauseLoading = false; + // Make sure the switch source is different: + if( !src || src == vid.src ){ + if( $.isFunction( switchCallback ) ){ + switchCallback( vid ); + } + // Delay done callback to allow any non-blocking switch callback code to fully execute + if( $.isFunction( doneCallback ) ){ + doneCallback(); + } + return ; + } + + // only display switch msg if actually switching: + mw.log( 'EmbedPlayerNative:: playerSwitchSource: ' + src + ' native time: ' + vid.currentTime ); + + // Update some parent embedPlayer vars: + this.currentTime = 0; + this.previousTime = 0; + if ( vid ) { + try { + // Remove all switch player bindings + $( vid ).unbind( switchBindPostfix ); + + // pause before switching source + vid.pause(); + + var orginalControlsState = vid.controls; + // Hide controls ( to not display native play button while switching sources ) + vid.removeAttribute('controls'); + + // dissable seeking ( if we were in a seeking state before the switch ) + _this.seeking = false; + + // add a loading indicator: + _this.addPlayerSpinner(); + + // Do the actual source switch: + vid.src = src; + // load the updated src + vid.load(); + + // hide the player offscreen while we switch + _this.hidePlayerOffScreen(); + // restore position once we have metadata + $( vid ).bind( 'loadedmetadata' + switchBindPostfix, function(){ + $( vid ).unbind( 'loadedmetadata' + switchBindPostfix); + mw.log("EmbedPlayerNative:: playerSwitchSource> loadedmetadata callback for:" + src + ' switchCallback: ' + switchCallback ); + // Only update duration if we didn't get it server side + // Some browsers report bad duration (e.g. Android native browser) + // So avoid using the browser detected value if possible. + if ( !_this.duration && isFinite( vid.duration ) ) { + _this.duration = vid.duration; + } + // keep going towards playback! if switchCallback has not been called yet + // we need the "playing" event to trigger the switch callback + if ( $.isFunction( switchCallback ) ){ + vid.play(); + } + }); + + var handleSwitchCallback = function(){ + // restore video position ( now that we are playing with metadata size ) + _this.restorePlayerOnScreen(); + // play hide loading spinner: + _this.hideSpinnerAndPlayBtn(); + // Restore + vid.controls = orginalControlsState; + // check if we have a switch callback and issue it now: + if ( $.isFunction( switchCallback ) ){ + switchCallback( vid ); + switchCallback = null; + } + } + + // once playing issue callbacks: + $( vid ).bind( 'playing' + switchBindPostfix, function(){ + $( vid ).unbind( 'playing' + switchBindPostfix ); + mw.log("EmbedPlayerNative:: playerSwitchSource> playing callback"); + handleSwitchCallback(); + }); + + // Add the end binding if we have a post event: + if( $.isFunction( doneCallback ) ){ + $( vid ).bind( 'ended' + switchBindPostfix , function( event ) { + // remove end binding: + $( vid ).unbind( switchBindPostfix ); + // issue the doneCallback + doneCallback(); + + // Support loop for older iOS + // Temporarly disabled pending more testing or refactor into a better place. + //if ( _this.loop ) { + // vid.play(); + //} + return false; + }); + } + + // issue the play request: + vid.play(); + + // check if ready state is loading or doing anything ( iOS play restriction ) + // give iOS 5 seconds to ~start~ loading media + setTimeout(function(){ + // Check that the player got out of readyState 0 + if( vid.readyState === 0 && $.isFunction( switchCallback ) ){ + mw.log("EmbedPlayerNative:: possible iOS play without gesture failed, issue callback"); + // hand off to the swtich callback method. + handleSwitchCallback(); + // make sure we are in a pause state ( failed to change and play media ); + _this.pause(); + // show the big play button so the user can give us a user gesture: + if( ! _this.useLargePlayBtn() ){ + _this.addLargePlayBtn(); + } + } + }, 5000 ); + + + } catch (e) { + mw.log("Error: EmbedPlayerNative Error in switching source playback"); + } + } + }, + hidePlayerOffScreen:function( vid ){ + var vid = this.getPlayerElement(); + // Move the video offscreen while it switches ( hides quicktime logo only applies to iPad ) + $( vid ).css( { + 'position' : 'absolute', + 'left': '-4048px' + }); + }, + restorePlayerOnScreen: function( vid ){ + var vid = this.getPlayerElement(); + if( this.keepPlayerOffScreenFlag ){ + return ; + } + + // Remove any poster div ( that would overlay the player ) + $( this ).find( '.playerPoster' ).remove(); + // Restore video pos before calling sync syze + $( vid ).css( { + 'left': '0px' + }); + }, + /** + * Pause the video playback + * calls parent_pause to update the interface + */ + pause: function( ) { + this.getPlayerElement(); + this.parent_pause(); // update interface + if ( this.playerElement ) { // update player + this.playerElement.pause(); + } + }, + + /** + * Play back the video stream + * calls parent_play to update the interface + */ + play: function() { + var _this = this; + // if starting playback from stoped state and not in an ad or otherise blocked controls state: + // restore player: + if( this.isStopped() && this._playContorls ){ + this.restorePlayerOnScreen(); + } + // Run parent play: + if( _this.parent_play() ){ + if ( this.getPlayerElement() && this.getPlayerElement().play ) { + mw.log( "EmbedPlayerNative:: issue native play call" ); + // If in pauseloading state make sure the loading spinner is present: + if( this.isPauseLoading ){ + this.hideSpinnerOncePlaying(); + } + // issue a play request + this.getPlayerElement().play(); + // re-start the monitor: + this.monitor(); + } + } else { + mw.log( "EmbedPlayerNative:: parent play returned false, don't issue play on native element"); + } + }, + + /** + * Stop the player ( end all listeners ) + */ + stop: function(){ + var _this = this; + if( this.playerElement && this.playerElement.currentTime){ + this.playerElement.currentTime = 0; + this.playerElement.pause(); + } + this.parent_stop(); + }, + + /** + * Toggle the Mute + * calls parent_toggleMute to update the interface + */ + toggleMute: function() { + this.parent_toggleMute(); + this.getPlayerElement(); + if ( this.playerElement ) + this.playerElement.muted = this.muted; + }, + + /** + * Update Volume + * + * @param {Float} percent Value between 0 and 1 to set audio volume + */ + setPlayerElementVolume : function( percent ) { + if ( this.getPlayerElement() ) { + // Disable mute if positive volume + if( percent != 0 ) { + this.playerElement.muted = false; + } + this.playerElement.volume = percent; + } + }, + + /** + * get Volume + * + * @return {Float} + * Audio volume between 0 and 1. + */ + getPlayerElementVolume: function() { + if ( this.getPlayerElement() ) { + return this.playerElement.volume; + } + }, + /** + * get the native muted state + */ + getPlayerElementMuted: function(){ + if ( this.getPlayerElement() ) { + return this.playerElement.muted; + } + }, + + /** + * Get the native media duration + */ + getNativeDuration: function() { + if ( this.playerElement ) { + return this.playerElement.duration; + } + }, + + /** + * Load the video stream with a callback fired once the video is "loaded" + * + * @parma {Function} callbcak Function called once video is loaded + */ + load: function( callback ) { + this.getPlayerElement(); + if ( !this.playerElement ) { + // No vid loaded + mw.log( 'EmbedPlayerNative::load() ... doEmbed' ); + this.onlyLoadFlag = true; + this.embedPlayerHTML(); + this.onLoadedCallback = callback; + } else { + // Should not happen offten + this.playerElement.load(); + if( callback ){ + callback(); + } + } + }, + + /** + * Get /update the playerElement value + */ + getPlayerElement: function () { + this.playerElement = $( '#' + this.pid ).get( 0 ); + return this.playerElement; + }, + + /** + * Bindings for the Video Element Events + */ + + /** + * Local method for seeking event + * fired when "seeking" + */ + _onseeking: function() { + mw.log( "EmbedPlayerNative::onSeeking " + this.seeking + ' new time: ' + this.getPlayerElement().currentTime ); + if( this.seeking && Math.round( this.getPlayerElement().currentTime - this.currentSeekTargetTime ) > 2 ){ + mw.log( "Error:: EmbedPlayerNative Seek time missmatch: target:" + this.getPlayerElement().currentTime + + ' actual ' + this.currentSeekTargetTime + ', note apple HLS can only seek to 10 second targets'); + } + // Trigger the html5 seeking event + //( if not already set from interface ) + if( !this.seeking ) { + this.currentSeekTargetTime = this.getPlayerElement().currentTime; + this.seeking = true; + // Run the onSeeking interface update + this.controlBuilder.onSeek(); + + // Trigger the html5 "seeking" trigger + mw.log("EmbedPlayerNative::seeking:trigger:: " + this.seeking); + if( this._propagateEvents ){ + this.triggerHelper( 'seeking' ); + } + } + }, + + /** + * Local method for seeked event + * fired when done seeking + */ + _onseeked: function() { + mw.log("EmbedPlayerNative::onSeeked " + this.seeking + ' ct:' + this.playerElement.currentTime ); + // sync the seek checks so that we don't re-issue the seek request + this.previousTime = this.currentTime = this.playerElement.currentTime; + + // Trigger the html5 action on the parent + if( this.seeking ){ + + // HLS safari triggers onseek when its not even close to the target time, + // we don't want to trigger the seek event for these "fake" onseeked triggers + if( Math.abs( this.currentSeekTargetTime - this.getPlayerElement().currentTime ) > 2 ){ + mw.log( "Error:: EmbedPlayerNative:seeked triggred with time mismatch: target:" + + this.currentSeekTargetTime + + ' actual:' + this.getPlayerElement().currentTime ); + return ; + } + this.seeking = false; + if( this._propagateEvents ){ + mw.log( "EmbedPlayerNative:: trigger: seeked" ); + this.triggerHelper( 'seeked' ); + } + } + this.hideSpinner(); + // update the playhead status + if( this.isStopped() ){ + this.addLargePlayBtn(); + } + this.monitor(); + }, + + /** + * Handle the native paused event + */ + _onpause: function(){ + var _this = this; + var timeSincePlay = Math.abs( this.absoluteStartPlayTime - new Date().getTime() ); + mw.log( "EmbedPlayerNative:: OnPaused:: propagate:" + this._propagateEvents + ' time since play: ' + timeSincePlay + ' isNative=true' ); + // Only trigger parent pause if more than MonitorRate time has gone by. + // Some browsers trigger native pause events when they "play" or after a src switch + if( timeSincePlay > mw.config.get( 'EmbedPlayer.MonitorRate' ) ){ + _this.parent_pause(); + } else { + // continue playback: + this.getPlayerElement().play(); + } + }, + + /** + * Handle the native play event + */ + _onplay: function(){ + mw.log("EmbedPlayerNative:: OnPlay:: propogate:" + this._propagateEvents + ' paused: ' + this.paused); + // if using native controls make sure the inteface does not block the native controls interface: + if( this.useNativePlayerControls() ){ + this.$interface.css('pointer-events', 'none'); + } + + // Update the interface ( if paused ) + if( ! this.isFirstEmbedPlay && this._propagateEvents && this.paused ){ + this.parent_play(); + } else { + // make sure the interface reflects the current play state if not calling parent_play() + this.playInterfaceUpdate(); + } + // Set firstEmbedPlay state to false to avoid initial play invocation : + this.isFirstEmbedPlay = false; + }, + + /** + * Local method for metadata ready + * fired when metadata becomes available + * + * Used to update the media duration to + * accurately reflect the src duration + */ + _onloadedmetadata: function() { + this.getPlayerElement(); + + if ( this.playerElement && !isNaN( this.playerElement.duration ) && isFinite( this.playerElement.duration) ) { + mw.log( 'EmbedPlayerNative :onloadedmetadata metadata ready Update duration:' + this.playerElement.duration + ' old dur: ' + this.getDuration() ); + // Only update duration if we didn't get it server side + // Some browsers report bad duration (e.g. Android native browser) + // So avoid using the browser detected value if possible. + if( !this.duration && this.playerElement && isFinite( this.playerElement.duration ) ) { + this.duration = this.playerElement.duration; + } + } + + // Check if in "playing" state and we are _propagateEvents events and continue to playback: + if( !this.paused && this._propagateEvents ){ + this.getPlayerElement().play(); + } + + //Fire "onLoaded" flags if set + if( typeof this.onLoadedCallback == 'function' ) { + this.onLoadedCallback(); + } + + // Trigger "media loaded" + if( ! this.mediaLoadedFlag ){ + $( this ).trigger( 'mediaLoaded' ); + this.mediaLoadedFlag = true; + } + }, + + /** + * Local method for end of media event + */ + _onended: function( event ) { + var _this = this; + if( this.getPlayerElement() ){ + mw.log( 'EmbedPlayer:native: onended:' + this.playerElement.currentTime + ' real dur:' + this.getDuration() + ' ended ' + this._propagateEvents ); + if( this._propagateEvents ){ + this.onClipDone(); + } + } + }, + /** + * Local onClip done function for native player. + */ + onClipDone: function(){ + var _this = this; + // add clip done binding ( will only run on sequence complete ) + $(this).unbind('onEndedDone.onClipDone').bind( 'onEndedDone.onClipDone', function(){ + _this.addPlayScreenWithNativeOffScreen(); + // if not a legitmate play screen don't keep the player offscreen when playback starts: + if( !_this.isImagePlayScreen() ){ + _this.keepPlayerOffScreenFlag =false; + } + }); + this.parent_onClipDone(); + } +}; + +} )( mediaWiki, jQuery ); 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_<number>'; 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 <span> ends up being not used as we get the html via .html() + $link = $( '<span></span>' ).append( $( '<a></a>' ).attr( 'href', appStoreUrl ).append( + mw.html.escape( mw.msg( 'mwe-embedplayer-vlcapp-vlcapplinktext' ) ) + ) ); + $( this ).html( $( '<div class="vlcapp-player"></div>' ) + .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( $( '<ul></ul>' ) + .append( $( '<li></li>' ).append( $( '<a></a>' ).attr( 'href', appStoreUrl ) + .text( mw.msg( 'mwe-embedplayer-vlcapp-downloadapp' ) ) ) + ).append( $( '<li></li>' ).append( $( '<a></a>' ).attr( 'href', vlcUrl ) + .text( mw.msg( 'mwe-embedplayer-vlcapp-openvideo' ) ) ) + ).append( $( '<li></li>' ).append( $( '<a></a>' ).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( + '<object classid="clsid:9BE31822-FDAD-461B-AD51-BE1D1C159921" ' + + 'codebase="http://downloads.videolan.org/pub/videolan/vlc/latest/win32/axvlc.cab#Version=0,8,6,0" ' + + 'id="' + this.pid + '" events="True" height="' + this.getPlayerHeight() + '" width="' + this.getPlayerWidth() + '"' + + '>' + + '<param name="MRL" value="">' + + '<param name="ShowDisplay" value="True">' + + '<param name="AutoLoop" value="False">' + + '<param name="AutoPlay" value="False">' + + '<param name="Volume" value="50">' + + '<param name="StartTime" value="0">' + + '<embed pluginspage="http://www.videolan.org" type="application/x-vlc-plugin" ' + + 'progid="VideoLAN.VLCPlugin.2" name="' + this.pid + '" ' + + 'height="' + this.getHeight() + '" width="' + this.getWidth() + '" ' + + // Set the style for IE layout issues ) + 'style="width:' + this.getWidth() + 'px;height:' + this.getHeight() + 'px;" ' + + '>' + + '</object>' + ) + // 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); + } + // <video> element + if ( ! mw.config.get('EmbedPlayer.DisableVideoTagSupport' ) // to support testing limited / old browsers + && + ( + typeof HTMLVideoElement == 'object' // Firefox, Safari + || + typeof HTMLVideoElement == 'function' // Opera + ) + ){ + // Test what codecs the native player supports: + try { + var dummyvid = document.createElement( "video" ); + if( dummyvid.canPlayType ) { + // Add the webm player + if( dummyvid.canPlayType('video/webm; codecs="vp8, vorbis"') ){ + this.mediaPlayers.addPlayer( webmNativePlayer ); + } + if( dummyvid.canPlayType('video/webm; codecs="vp9, opus"') ){ + this.mediaPlayers.addPlayer( vp9NativePlayer ); + } + + // Test for MP3: + if ( this.supportedMimeType('audio/mpeg') ) { + this.mediaPlayers.addPlayer( mp3NativePlayer ); + } + + // Test for AAC: + if ( dummyvid.canPlayType( 'audio/mp4; codecs="mp4a.40.5"' ) ) { + this.mediaPlayers.addPlayer( aacNativePlayer ); + } + + // Test for h264: + if ( dummyvid.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"' ) ) { + this.mediaPlayers.addPlayer( h264NativePlayer ); + // Check for iOS for vdn player support ( apple adaptive ) or vdn canPlayType != '' ( ie maybe/probably ) + if( mw.isIOS() || dummyvid.canPlayType('application/vnd.apple.mpegurl; codecs="avc1.42E01E"' ) ){ + // Android 3x lies about HLS support ( only add if not Android 3.x ) + if( navigator.userAgent.indexOf( 'Android 3.') == -1 ){ + this.mediaPlayers.addPlayer( appleVdnPlayer ); + } + } + + } + // For now if Android assume we support h264Native (FIXME + // test on real devices ) + if ( mw.isAndroid2() ){ + this.mediaPlayers.addPlayer( h264NativePlayer ); + } + + // Test for ogg + if ( dummyvid.canPlayType( 'video/ogg; codecs="theora, vorbis"' ) || + dummyvid.canPlayType( 'audio/ogg; codecs="vorbis"' ) + ) { + this.mediaPlayers.addPlayer( oggNativePlayer ); + } + + // Test for opus + if ( dummyvid.canPlayType( 'audio/ogg; codecs="opus"' ).replace(/maybe/, '') ) { + this.mediaPlayers.addPlayer( opusNativePlayer ); + } + } + } catch ( e ) { + mw.log( 'could not run canPlayType ' + e ); + } + } + + // "navigator" plugins + if ( navigator.mimeTypes && navigator.mimeTypes.length > 0 ) { + for ( var i = 0; i < navigator.mimeTypes.length; i++ ) { + var type = navigator.mimeTypes[i].type; + var semicolonPos = type.indexOf( ';' ); + if ( semicolonPos > -1 ) { + type = type.substr( 0, semicolonPos ); + } + // mw.log( 'on type: ' + type ); + var pluginName = navigator.mimeTypes[i].enabledPlugin ? navigator.mimeTypes[i].enabledPlugin.name : ''; + if ( !pluginName ) { + // In case it is null or undefined + pluginName = ''; + } + //if ( pluginName.toLowerCase() == 'vlc multimedia plugin' || pluginName.toLowerCase() == 'vlc multimedia plug-in' ) { + // this.mediaPlayers.addPlayer( vlcPlayer ); + // continue; + //} + + if ( (type == 'video/mpeg' || type == 'video/x-msvideo') ){ + //pluginName.toLowerCase() == 'vlc multimedia plugin' ) { + //this.mediaPlayers.addPlayer( vlcMozillaPlayer ); + } + + if ( type == 'application/ogg' ) { + //if ( pluginName.toLowerCase() == 'vlc multimedia plugin' ) { + //this.mediaPlayers.addPlayer( vlcMozillaPlayer ); + //else if ( pluginName.indexOf( 'QuickTime' ) > -1 ) + //this.mediaPlayers.addPlayer(quicktimeMozillaPlayer); + //} else { + //this.mediaPlayers.addPlayer( oggPluginPlayer ); + //} + continue; + } else if ( uniqueMimesOnly ) { + if ( type == 'application/x-vlc-player' ) { + // this.mediaPlayers.addPlayer( vlcMozillaPlayer ); + continue; + } else if ( type == 'video/quicktime' ) { + // this.mediaPlayers.addPlayer(quicktimeMozillaPlayer); + continue; + } + } + } + } + + if ( mw.isIOS() ) { + this.mediaPlayers.addPlayer( vlcAppPlayer ); + } + + // Note IE 11 doesn't identify itself as 'MSIE'. + // For simplicity just check for the rendering engine codename 'Trident'. + if ( navigator.userAgent.indexOf( 'Trident' ) != -1 ) { + if ( this.mediaPlayers.isSupportedPlayer( 'webmNative' ) ) { + // IE has the WebM components already, leave it be! + } else if ( navigator.userAgent.indexOf( 'ARM' ) != -1 ) { + // Windows RT doesn't allow installation of the WebM components. + // Don't tease the poor user. + } else { + // Prompt user to install the WebM media components for IE 9+ + this.mediaPlayers.addPlayer( IEWebMPrompt ); + } + } + + // ogv.js compatibility detection... + if ( OGVCompat.supported( 'OGVPlayer' ) ) { + // ogv.js emscripten version + // + // Works in: + // * Safari 6.1+ on Mac OS X + // * Safari on iOS 8+ (best on 64-bit devices) + // * IE 10/11 on Windows 7/8/8.1 (requires Flash for audio) + // * Edge on Windows 10 (no plugins needed) + // + // Current Firefox, Chrome, Opera all work great too, but use + // native playback by default of course! + // + this.mediaPlayers.addPlayer( ogvJsPlayer ); + } + + // Allow extensions to detect and add their own "players" + mw.log("EmbedPlayer::trigger:embedPlayerUpdateMediaPlayersEvent"); + $( mw ).trigger( 'embedPlayerUpdateMediaPlayersEvent' , this.mediaPlayers ); + + }, + + /** + * Test IE for activeX by name + * + * @param {String} + * name Name of ActiveXObject to look for + */ + testActiveX : function ( name ) { + mw.log("EmbedPlayer::detect: test testActiveX: " + name); + var hasObj = true; + try { + // No IE, not a class called "name", it's a variable + var obj = new ActiveXObject( '' + name ); + } catch ( e ) { + hasObj = false; + } + return hasObj; + } +}; + + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaElement.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaElement.js new file mode 100644 index 00000000..9af48998 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaElement.js @@ -0,0 +1,491 @@ +/** + * A media element corresponding to a <video> element. + * + * It is implemented as a collection of mediaSource objects. The media sources + * will be initialized from the <video> element, its child <source> elements, + * and/or the ROE file referenced by the <video> element. + * + * @param {element} + * videoElement <video> element used for initialization. + * @constructor + */ +( function( mw, $ ) { "use strict"; + +mw.MediaElement = function( element ) { + this.init( element ); +}; + +mw.MediaElement.prototype = { + + // The array of mediaSource elements. + sources: null, + + // flag for ROE data being added. + addedROEData: false, + + // Selected mediaSource element. + selectedSource: null, + + /** + * Media Element constructor + * + * Sets up a mediaElement from a provided top level "video" element adds any + * child sources that are found + * + * @param {Element} + * videoElement Element that has src attribute or has children + * source elements + */ + init: function( videoElement ) { + var _this = this; + mw.log( "EmbedPlayer::mediaElement:init:" + videoElement.id ); + this.parentEmbedId = videoElement.id; + this.sources = new Array(); + + // Process the videoElement as a source element: + if( videoElement ){ + if ( $( videoElement ).attr( "src" ) ) { + _this.tryAddSource( videoElement ); + } + // Process elements source children + $( videoElement ).find( 'source,track' ).each( function( ) { + _this.tryAddSource( this ); + } ); + } + }, + + /** + * Updates the time request for all sources that have a standard time + * request argument (ie &t=start_time/end_time) + * + * @param {String} + * startNpt Start time in npt format + * @param {String} + * endNpt End time in npt format + */ + updateSourceTimes: function( startNpt, endNpt ) { + var _this = this; + $.each( this.sources, function( inx, mediaSource ) { + mediaSource.updateSrcTime( startNpt, endNpt ); + } ); + }, + + /** + * Get Text tracks + */ + getTextTracks: function(){ + var textTracks = []; + $.each( this.sources, function(inx, source ){ + if ( source.nodeName == 'track' || ( source.mimeType && source.mimeType.indexOf('text/') !== -1 )){ + textTracks.push( source ); + } + }); + return textTracks; + }, + /** + * Returns the array of mediaSources of this element. + * + * @param {String} + * [mimeFilter] Filter criteria for set of mediaSources to return + * @return {Array} mediaSource elements. + */ + getSources: function( mimeFilter ) { + if ( !mimeFilter ) { + return this.sources; + } + // Apply mime filter: + var source_set = new Array(); + for ( var i = 0; i < this.sources.length ; i++ ) { + if ( this.sources[i].mimeType && + this.sources[i].mimeType.indexOf( mimeFilter ) != -1 ) + { + source_set.push( this.sources[i] ); + } + } + return source_set; + }, + + /** + * Selects a source by id + * + * @param {String} + * sourceId Id of the source to select. + * @return {MediaSource} The selected mediaSource or null if not found + */ + getSourceById:function( sourceId ) { + for ( var i = 0; i < this.sources.length ; i++ ) { + if ( this.sources[i].id == sourceId ) { + return this.sources[i]; + } + } + return null; + }, + + /** + * Selects a particular source for playback updating the "selectedSource" + * + * @param {Number} + * index Index of source element to set as selectedSource + */ + setSourceByIndex: function( index ) { + mw.log( 'EmbedPlayer::mediaElement:selectSource: ' + index ); + var oldSrc = this.selectedSource.getSrc(); + var playableSources = this.getPlayableSources(); + for ( var i = 0; i < playableSources.length; i++ ) { + if ( i == index ) { + this.selectedSource = playableSources[i]; + break; + } + } + if( oldSrc != this.selectedSource.getSrc() ){ + $( '#' + this.parentEmbedId ).trigger( 'SourceChange'); + } + }, + /** + * Sets a the selected source to passed in source object + * @param {Object} Source + */ + setSource: function( source ){ + var oldSrc = this.selectedSource.getSrc(); + this.selectedSource = source; + if( oldSrc != this.selectedSource.getSrc() ){ + $( '#' + this.parentEmbedId ).trigger( 'SourceChange'); + } + }, + + + /** + * Selects the default source via cookie preference, default marked, or by + * id order + */ + autoSelectSource: function() { + mw.log( 'EmbedPlayer::mediaElement::autoSelectSource' ); + var _this = this; + // Select the default source + var playableSources = this.getPlayableSources(); + var flash_flag = false, ogg_flag = false; + // Check if there are any playableSources + if( playableSources.length == 0 ){ + return false; + } + var setSelectedSource = function( source ){ + _this.selectedSource = source; + return _this.selectedSource; + }; + + // Set via module driven preference: + $( this ).trigger( 'onSelectSource', playableSources ); + + if( _this.selectedSource ){ + mw.log('MediaElement::autoSelectSource: Set via trigger::' + _this.selectedSource.getTitle() ); + return _this.selectedSource; + } + + // Set via marked default: + $.each( playableSources, function( inx, source ){ + if ( source.markedDefault ) { + mw.log( 'MediaElement::autoSelectSource: Set via marked default: ' + source.markedDefault ); + return setSelectedSource( source );; + } + }); + + // Set apple adaptive ( if available ) + var vndSources = this.getPlayableSources('application/vnd.apple.mpegurl') + if( vndSources.length && mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'application/vnd.apple.mpegurl' ).length ){ + // Check for device flags: + var desktopVdn, mobileVdn; + $.each( vndSources, function( inx, source) { + // Kaltura tags vdn sources with iphonenew + if( source.getFlavorId() && source.getFlavorId().toLowerCase() == 'iphonenew' ){ + mobileVdn = source; + } else { + desktopVdn = source; + } + }) + // NOTE: We really should not have two VDN sources the point of vdn is to be a set of adaptive streams. + // This work around is a result of Kaltura HLS stream tagging + if( mw.isIphone() && mobileVdn ){ + setSelectedSource( mobileVdn ); + } else if( desktopVdn ){ + setSelectedSource( desktopVdn ); + } + } + if ( this.selectedSource ) { + mw.log('MediaElement::autoSelectSource: Set via Adaptive HLS: source flavor id:' + _this.selectedSource.getFlavorId() + ' src: ' + _this.selectedSource.getSrc() ); + return this.selectedSource; + } + + // Set via user bandwidth pref will always set source to closest bandwidth allocation while not going over EmbedPlayer.UserBandwidth + if( $.cookie('EmbedPlayer.UserBandwidth') ){ + var bandwidthDelta = 999999999; + var bandwidthTarget = $.cookie('EmbedPlayer.UserBandwidth'); + $.each( playableSources, function(inx, source ){ + if( source.bandwidth ){ + // Check if a native source ( takes president over bandwidth selection ) + var player = mw.EmbedTypes.getMediaPlayers().defaultPlayer( source.mimeType ); + if ( !player || player.library != 'Native' ) { + // continue + return true; + } + + if( Math.abs( source.bandwidth - bandwidthTarget ) < bandwidthDelta ){ + bandwidthDelta = Math.abs( source.bandwidth - bandwidthTarget ); + setSelectedSource( source ); + } + } + }); + } + + if ( this.selectedSource ) { + mw.log('MediaElement::autoSelectSource: Set via bandwidth prefrence: source ' + this.selectedSource.bandwidth + ' user: ' + $.cookie('EmbedPlayer.UserBandwidth') ); + return this.selectedSource; + } + + + // If we have at least one native source, throw out non-native sources + // for size based source selection: + var nativePlayableSources = []; + $.each( playableSources, function(inx, source ){ + var mimeType = source.mimeType; + var player = mw.EmbedTypes.getMediaPlayers().defaultPlayer( mimeType ); + if ( player && player.library == 'Native' ) { + nativePlayableSources.push( source ); + } + }); + + // Prefer native playback ( and prefer WebM over ogg and h.264 ) + var namedSourceSet = {}; + var useBogoSlow = false; // use benchmark only for ogv.js + $.each( playableSources, function(inx, source ){ + var mimeType = source.mimeType; + var player = mw.EmbedTypes.getMediaPlayers().defaultPlayer( mimeType ); + if ( player && ( player.library == 'Native' || player.library == 'OgvJs' ) ) { + switch( player.id ){ + case 'mp3Native': + var shortName = 'mp3'; + break; + case 'aacNative': + var shortName = 'aac'; + break; + case 'oggNative': + var shortName = 'ogg'; + break; + case 'ogvJsPlayer': + useBogoSlow = true; + var shortName = 'ogg'; + break; + case 'webmNative': + var shortName = 'webm'; + break; + case 'vp9Native': + var shortName = 'vp9'; + break; + case 'h264Native': + var shortName = 'h264'; + break; + case 'appleVdn': + var shortName = 'appleVdn'; + break; + } + if( !namedSourceSet[ shortName ] ){ + namedSourceSet[ shortName ] = []; + } + namedSourceSet[ shortName ].push( source ); + } + }); + + var codecPref = mw.config.get( 'EmbedPlayer.CodecPreference'); + + // if on android 4 use mp4 over webm + if( mw.isAndroid40() ){ + if( codecPref && codecPref[0] == 'webm' ){ + codecPref[0] = 'h264'; + codecPref[1] = 'webm'; + } + } + + if( codecPref ){ + for(var i =0; i < codecPref.length; i++){ + var codec = codecPref[ i ]; + if( ! namedSourceSet[ codec ] ){ + continue; + } + if( namedSourceSet[ codec ].length == 1 ){ + mw.log('MediaElement::autoSelectSource: Set 1 source via EmbedPlayer.CodecPreference: ' + namedSourceSet[ codec ][0].getTitle() ); + return setSelectedSource( namedSourceSet[ codec ][0] ); + } else if( namedSourceSet[ codec ].length > 1 ) { + // select based on size: + // Set via embed resolution closest to relative to display size + var minSizeDelta = null; + + // unless we're really slow... + var isBogoSlow = useBogoSlow && OGVCompat.isSlow(); + + if( this.parentEmbedId ){ + var displayWidth = $('#' + this.parentEmbedId).width(); + $.each( namedSourceSet[ codec ], function(inx, source ){ + if ( ( isBogoSlow && source.height > 240 ) + || (useBogoSlow && source.height > 360 ) ) { + // On iOS or slow Windows devices, large videos decoded in JavaScript are a bad idea! + // continue + return true; + } + if( source.width && displayWidth ){ + var sizeDelta = Math.abs( source.width - displayWidth ); + mw.log('MediaElement::autoSelectSource: size delta : ' + sizeDelta + ' for s:' + source.width ); + if( minSizeDelta == null || sizeDelta < minSizeDelta){ + minSizeDelta = sizeDelta; + setSelectedSource( source ); + } + } + }); + } + // If we found a source via display size return: + if ( this.selectedSource ) { + mw.log('MediaElement::autoSelectSource: from ' + this.selectedSource.mimeType + ' because of resolution:' + this.selectedSource.width + ' close to: ' + displayWidth ); + return this.selectedSource; + } + // if no size info is set just select the first source: + if( namedSourceSet[ codec ][0] ){ + return setSelectedSource( namedSourceSet[ codec ][0] ); + } + } + }; + } + + // Set h264 via native or flash fallback + $.each( playableSources, function(inx, source ){ + var mimeType = source.mimeType; + var player = mw.EmbedTypes.getMediaPlayers().defaultPlayer( mimeType ); + if ( mimeType == 'video/h264' + && player + && ( + player.library == 'Native' + || + player.library == 'Kplayer' + ) + ) { + if( source ){ + mw.log('MediaElement::autoSelectSource: Set h264 via native or flash fallback:' + source.getTitle() ); + return setSelectedSource( source ); + } + } + }); + + // Else just select the first playable source + if ( !this.selectedSource && playableSources[0] ) { + mw.log( 'MediaElement::autoSelectSource: Set via first source: ' + playableSources[0].getTitle() + ' mime: ' + playableSources[0].getMIMEType() ); + return setSelectedSource( playableSources[0] ); + } + // No Source found so no source selected + return false; + }, + + /** + * check if the mime is ogg + */ + isOgg: function( mimeType ){ + if ( mimeType == 'video/ogg' + || mimeType == 'ogg/video' + || mimeType == 'video/annodex' + || mimeType == 'application/ogg' + ) { + return true; + } + return false; + }, + + /** + * Returns the thumbnail URL for the media element. + * + * @returns {String} thumbnail URL + */ + getPosterSrc: function( ) { + return this.poster; + }, + + /** + * Checks whether there is a stream of a specified MIME type. + * + * @param {String} + * mimeType MIME type to check. + * @return {Boolean} true if sources include MIME false if not. + */ + hasStreamOfMIMEType: function( mimeType ) + { + for ( var i = 0; i < this.sources.length; i++ ) + { + if ( this.sources[i].getMIMEType() == mimeType ){ + return true; + } + } + return false; + }, + + /** + * Checks if media is a playable type + */ + isPlayableType: function( mimeType ) { + // mw.log("isPlayableType:: " + mimeType); + if ( mw.EmbedTypes.getMediaPlayers().defaultPlayer( mimeType ) ) { + mw.log("isPlayableType:: " + mimeType); + return true; + } else { + return false; + } + }, + + /** + * Adds a single mediaSource using the provided element if the element has a + * 'src' attribute. + * + * @param {Element} + * element <video>, <source> or <mediaSource> <text> element. + */ + tryAddSource: function( element ) { + //mw.log( 'mw.MediaElement::tryAddSource:' + $( element ).attr( "src" ) ); + var newSrc = $( element ).attr( 'src' ); + if ( newSrc ) { + // Make sure an existing element with the same src does not already exist: + for ( var i = 0; i < this.sources.length; i++ ) { + if ( this.sources[i].src == newSrc ) { + // Source already exists update any new attr: + this.sources[i].updateSource( element ); + return this.sources[i]; + } + } + } + // Create a new source + var source = new mw.MediaSource( element ); + + this.sources.push( source ); + //mw.log( 'tryAddSource: added source ::' + source + 'sl:' + this.sources.length ); + return source; + }, + + /** + * Get playable sources + * + *@pram mimeFilter {=string} (optional) Filter the playable sources set by mime filter + * + * @returns {Array} of playable media sources + */ + getPlayableSources: function( mimeFilter ) { + var playableSources = []; + for ( var i = 0; i < this.sources.length; i++ ) { + if ( this.isPlayableType( this.sources[i].mimeType ) + && + ( !mimeFilter || this.sources[i].mimeType.indexOf( mimeFilter) != -1 ) + ){ + playableSources.push( this.sources[i] ); + } + }; + mw.log( "MediaElement::GetPlayableSources mimeFilter:" + mimeFilter + " " + + playableSources.length + ' sources playable out of ' + this.sources.length ); + + return playableSources; + } +}; + +} )( mediaWiki, jQuery ); + diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayer.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayer.js new file mode 100644 index 00000000..94ef9708 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayer.js @@ -0,0 +1,85 @@ +/** + * mediaPlayer represents a media player plugin. + * + * @param {String} + * id id used for the plugin. + * @param {Array} + * supportedTypes an array of supported MIME types. + * @param {String} + * library external script containing the plugin interface code. + * @constructor + */ +( function( mw, $ ) { "use strict"; + +mw.MediaPlayer = function( id, supportedTypes, library ) +{ + this.id = id; + this.supportedTypes = supportedTypes; + this.library = library; + this.loaded = false; + this.loading_callbacks = new Array(); + return this; +}; +mw.MediaPlayer.prototype = { + // Id of the mediaPlayer + id:null, + + // Mime types supported by this player + supportedTypes:null, + + // Player library ie: native, vlc etc. + library:null, + + // Flag stores the mediaPlayer load state + loaded:false, + + /** + * Checks support for a given MIME type + * + * @param {String} + * type Mime type to check against supportedTypes + * @return {Boolean} true if mime type is supported false if mime type is + * unsupported + */ + supportsMIMEType: function( type ) { + for ( var i = 0; i < this.supportedTypes.length; i++ ) { + if ( this.supportedTypes[i] == type ) + return true; + } + return false; + }, + + /** + * Get the "name" of the player from a predictable msg key + */ + getName: function() { + // Give grep a chance to find the usages: + // mwe-embedplayer-ogg-player-vlc-player, mwe-embedplayer-ogg-player-oggNative, mwe-embedplayer-ogg-player-mp3Native, + // mwe-embedplayer-ogg-player-aacNative, mwe-embedplayer-ogg-player-h264Native, mwe-embedplayer-ogg-player-webmNative, + // mwe-embedplayer-ogg-player-oggPlugin, mwe-embedplayer-ogg-player-quicktime-mozilla, + // mwe-embedplayer-ogg-player-quicktime-activex, mwe-embedplayer-ogg-player-cortado, + // mwe-embedplayer-ogg-player-flowplayer, mwe-embedplayer-ogg-player-kplayer, mwe-embedplayer-ogg-player-selected, + // mwe-embedplayer-ogg-player-omtkplayer + return mw.msg( 'mwe-embedplayer-ogg-player-' + this.id ); + }, + + /** + * Loads the player library & player skin config ( if needed ) and then + * calls the callback. + * + * @param {Function} + * callback Function to be called once player library is loaded. + */ + load: function( callback ) { + // Load player library ( upper case the first letter of the library ) + mw.load( [ + 'mw.EmbedPlayer' + this.library.substr(0,1).toUpperCase() + this.library.substr(1) + ], function() { + if( callback ){ + callback(); + } + } ); + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayers.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayers.js new file mode 100644 index 00000000..9d1e34f4 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayers.js @@ -0,0 +1,195 @@ +/** + * mediaPlayers is a collection of mediaPlayer objects supported by the client. + * + * @constructor + */ +( function( mw, $ ) { "use strict"; + +mw.MediaPlayers = function(){ + this.init(); +}; + +mw.MediaPlayers.prototype = { + // The list of players supported + players : null, + + // Store per mime-type preferences for players + preference : { }, + + // Stores the default set of players for a given mime type + defaultPlayers : { }, + + /** + * Initializartion function sets the default order for players for a given + * mime type + */ + init: function() { + this.players = new Array(); + this.loadPreferences(); + + // Set up default players order for each library type + this.defaultPlayers['video/x-flv'] = ['Kplayer', 'Vlc']; + this.defaultPlayers['video/h264'] = ['Native', 'Kplayer', 'Vlc']; + + this.defaultPlayers['application/vnd.apple.mpegurl'] = ['Native']; + + this.defaultPlayers['video/ogg'] = ['Native', 'Vlc', 'OgvJs', 'Generic', 'VLCApp']; + this.defaultPlayers['audio/webm'] = ['Native', 'Vlc', 'VLCApp', 'IEWebMPrompt']; + this.defaultPlayers['video/webm'] = ['Native', 'Vlc', 'VLCApp', 'IEWebMPrompt']; + this.defaultPlayers['application/ogg'] = ['Native', 'Vlc', 'OgvJs', 'Generic', 'VLCApp']; + this.defaultPlayers['audio/ogg'] = ['Native', 'Vlc', 'OgvJs', 'VLCApp']; + this.defaultPlayers['audio/mpeg']= ['Native', 'Kplayer']; + this.defaultPlayers['audio/mp3']= ['Native', 'Kplayer']; + this.defaultPlayers['audio/mp4']= ['Native']; + this.defaultPlayers['video/mp4'] = ['Native', 'Vlc']; + this.defaultPlayers['video/mpeg'] = ['Vlc']; + this.defaultPlayers['video/x-msvideo'] = ['Vlc']; + + // this.defaultPlayers['text/html'] = ['Html']; + //this.defaultPlayers['image/svg'] = ['ImageOverlay']; + + this.defaultPlayers['image/jpeg'] = ['ImageOverlay']; + this.defaultPlayers['image/png'] = ['ImageOverlay']; + + }, + + /** + * Adds a Player to the player list + * + * @param {Object} + * player Player object to be added + */ + addPlayer: function( player ) { + for ( var i = 0; i < this.players.length; i++ ) { + if ( this.players[i].id == player.id ) { + // Player already found + return ; + } + } + // Add the player: + this.players.push( player ); + }, + + /** + * Checks if a player is supported by id + */ + isSupportedPlayer: function( playerId ){ + for( var i=0; i < this.players.length; i++ ){ + if( this.players[i].id == playerId ){ + return true; + } + } + return false; + }, + + /** + * get players that support a given mimeType + * + * @param {String} + * mimeType Mime type of player set + * @return {Array} Array of players that support a the requested mime type + */ + getMIMETypePlayers: function( mimeType ) { + var mimePlayers = new Array(); + var _this = this; + var baseMimeType = mimeType.split( ';' )[0]; + if ( this.defaultPlayers[ baseMimeType ] ) { + $.each( this.defaultPlayers[ baseMimeType ], function( d, lib ) { + var library = _this.defaultPlayers[ baseMimeType ][ d ]; + for ( var i = 0; i < _this.players.length; i++ ) { + if ( _this.players[i].library == library && _this.players[i].supportsMIMEType( mimeType ) ) { + mimePlayers.push( _this.players[i] ); + } + } + } ); + } + return mimePlayers; + }, + + /** + * Default player for a given mime type + * + * @param {String} + * mimeType Mime type of the requested player + * @return Player for mime type null if no player found + */ + defaultPlayer : function( mimeType ) { + // mw.log( "get defaultPlayer for " + mimeType ); + var mimePlayers = this.getMIMETypePlayers( mimeType ); + if ( mimePlayers.length > 0 ) + { + // Select the default player: + for ( var i = 0; i < mimePlayers.length; i++ ) { + // Check for native: + if( mimePlayers[i].librayr == 'Native' ){ + return mimePlayers[i]; + } + // else check for preference + if ( mimePlayers[i].id == this.preference[mimeType] ){ + return mimePlayers[i]; + } + } + // Otherwise just return the first compatible player + // (it will be chosen according to the defaultPlayers list + return mimePlayers[0]; + } + // mw.log( 'No default player found for ' + mimeType ); + return null; + }, + + /** + * Sets the format preference. + * + * @param {String} + * mimeFormat Prefered format + */ + setFormatPreference : function ( mimeFormat ) { + this.preference['formatPreference'] = mimeFormat; + $.cookie( 'EmbedPlayer.Preference', JSON.stringify( this.preference) ); + }, + + /** + * Loads the user preference settings from a cookie + */ + loadPreferences : function ( ) { + this.preference = { }; + // See if we have a cookie set to a clientSupported type: + if( $.cookie( 'EmbedPlayer.Preference' ) ) { + this.preference = JSON.parse( $.cookie( 'EmbedPlayer.Preference' ) ); + } + }, + + /** + * Sets the player preference + * + * @param {String} + * playerId Preferred player id + * @param {String} + * mimeType Mime type for the associated player stream + */ + setPlayerPreference : function( playerId, mimeType ) { + var selectedPlayer = null; + for ( var i = 0; i < this.players.length; i++ ) { + if ( this.players[i].id == playerId ) { + selectedPlayer = this.players[i]; + mw.log( 'EmbedPlayer::setPlayerPreference: choosing ' + playerId + ' for ' + mimeType ); + this.preference[ mimeType ] = playerId; + $.cookie( 'EmbedPlayer.Preference', JSON.stringify( this.preference ) ); + break; + } + } + // Update All the player instances on the page + if ( selectedPlayer ) { + $('.mwEmbedPlayer').each(function(inx, playerTarget ){ + var embedPlayer = $( playerTarget ).get( 0 ); + if ( embedPlayer.mediaElement.selectedSource + && ( embedPlayer.mediaElement.selectedSource.mimeType == mimeType ) ) + { + embedPlayer.selectPlayer( selectedPlayer ); + } + }); + } + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaSource.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaSource.js new file mode 100644 index 00000000..9449a5d4 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaSource.js @@ -0,0 +1,490 @@ +/** + * mediaSource class represents a source for a media element. + * + * @param {Element} + * element: MIME type of the source. + * @constructor + */ + +/** + * The base source attribute checks also see: + * http://dev.w3.org/html5/spec/Overview.html#the-source-element + */ + +( function( mw, $ ) { "use strict"; + +mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [ + // source id + 'id', + + // media url + 'src', + + // Title string for the source asset + 'title', + + // boolean if we support temporal url requests on the source media + 'URLTimeEncoding', + + // Store the node name for type identification + 'nodeName', + + /** + * data- attributes ( not yet standards ) + */ + + // Media has a startOffset ( used for plugins that + // display ogg page time rather than presentation time + 'data-startoffset', + + // 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', + + // Source stream qualities + // NOTE data- is striped from the attribute as we build out the "mediaSource" object + 'data-shorttitle', // short title for stream ( useful for stream switching control bar widget) + 'data-width', // the width of the stream + 'data-height', // the height of the stream + 'data-bandwidth', // the overall bitrate of the stream in bytes + 'data-sizebytes', // the size of the stream in bytes + 'data-framerate', // the framereate of the stream + 'data-flavorid', // a source flavor id ( useful for targeting devices ) + 'data-aspect', // the aspect ratio, useful for adaptive protocal urls that don't have a strict height / width + + // Used as title in download panel + 'data-title', + + // Used for download attribute on mediawiki + 'data-mwtitle', + // used for setting the api provider for mediawiki + 'data-mwprovider', + + // to disable menu or timedText for a given embed + 'data-disablecontrols', + + // used for language direction of subtitles + 'data-dir', + + // Media start time + 'start', + + // Media end time + 'end', + + // If the source is the default source + 'default' +] ); + +mw.MediaSource = function( element ) { + this.init( element ); +}; + +mw.MediaSource.prototype = { + // MIME type of the source. + mimeType:null, + + // URI of the source. + uri:null, + + // Title of the source. + title: null, + + // True if the source has been marked as the default. + markedDefault: false, + + // True if the source supports url specification of offset and duration + URLTimeEncoding:false, + + // Start offset of the requested segment + startOffset: 0, + + // Duration of the requested segment (0 if not known) + duration:0, + + // source id + id: null, + + // Start time in npt format + startNpt: null, + + // End time in npt format + endNpt: null, + + // Language of the file + srclang: null, + /** + * MediaSource constructor: + */ + init : function( element ) { + var _this = this; + // mw.log('EmbedPlayer::adding mediaSource: ' + element); + this.src = $( element ).attr( 'src' ); + + // Set default URLTimeEncoding if we have a time url: + // not ideal way to discover if content is on an oggz_chop server. + // should check some other way. + var pUrl = new mw.Uri ( this.src ); + if ( typeof pUrl.query[ 't' ] != 'undefined' ) { + this.URLTimeEncoding = true; + } + + var sourceAttr = mw.config.get( 'EmbedPlayer.SourceAttributes' ); + $.each( sourceAttr, function( inx, attr ){ + if ( $( element ).attr( attr ) ) { + // strip data- from the attribute name + var attrName = ( attr.indexOf('data-') === 0) ? attr.substr(5) : attr + _this[ attrName ] = $( element ).attr( attr ); + } + }); + + // Normalize "label" to "title" ( label is the actual spec so use that over title ) + if( this.label ){ + this.title = this.label; + } + + // Set the content type: + if ( $( element ).attr( 'type' ) ) { + this.mimeType = $( element ).attr( 'type' ); + }else if ( $( element ).attr( 'content-type' ) ) { + this.mimeType = $( element ).attr( 'content-type' ); + }else if( $( element )[0].tagName.toLowerCase() == 'audio' ){ + // If the element is an "audio" tag set audio format + this.mimeType = 'audio/ogg'; + } else { + this.mimeType = this.detectType( this.src ); + } + + // Conform the mime type to ogg + if( this.mimeType == 'video/theora') { + this.mimeType = 'video/ogg'; + } + + if( this.mimeType == 'audio/vorbis') { + this.mimeType = 'audio/ogg'; + } + + // Check for parent elements ( supplies categories in "track" ) + if( $( element ).parent().attr('category') ) { + this.category = $( element ).parent().attr('category'); + } + + if( $( element ).attr( 'default' ) ){ + this.markedDefault = true; + } + + // Get the url duration ( if applicable ) + this.getURLDuration(); + }, + + /** + * Update Source title via Element + * + * @param {Element} + * element Source element to update attributes from + */ + updateSource: function( element ) { + // for now just update the title: + if ( $( element ).attr( "title" ) ) { + this.title = $( element ).attr( "title" ); + } + }, + + /** + * Updates the src time and start & end + * + * @param {String} + * start_time: in NPT format + * @param {String} + * end_time: in NPT format + */ + updateSrcTime: function ( startNpt, endNpt ) { + // mw.log("f:updateSrcTime: "+ startNpt+'/'+ endNpt + ' from org: ' + + // this.startNpt+ '/'+this.endNpt); + // mw.log("pre uri:" + this.src); + // if we have time we can use: + if ( this.URLTimeEncoding ) { + // make sure its a valid start time / end time (else set default) + if ( !mw.npt2seconds( startNpt ) ) { + startNpt = this.startNpt; + } + + if ( !mw.npt2seconds( endNpt ) ) { + endNpt = this.endNpt; + } + + this.src = mw.replaceUrlParams( this.src, { + 't': startNpt + '/' + endNpt + }); + + // update the duration + this.getURLDuration(); + } + }, + + /** + * Sets the duration and sets the end time if unset + * + * @param {Float} + * duration: in seconds + */ + setDuration: function ( duration ) { + this.duration = duration; + if ( !this.endNpt ) { + this.endNpt = mw.seconds2npt( this.startOffset + duration ); + } + }, + + /** + * MIME type accessor function. + * + * @return {String} the MIME type of the source. + */ + getMIMEType: function() { + if( this.mimeType ) { + return this.mimeType; + } + this.mimeType = this.detectType( this.src ); + return this.mimeType; + }, + /** + * Update the local src + * @param {String} + * src The URL to the media asset + */ + setSrc: function( src ){ + this.src = src; + }, + + /** + * URI function. + * + * @param {Number} + * serverSeekTime Int: Used to adjust the URI for url based + * seeks) + * @return {String} the URI of the source. + */ + getSrc: function( serverSeekTime ) { + if ( !serverSeekTime || !this.URLTimeEncoding ) { + return this.src; + } + var endvar = ''; + if ( this.endNpt ) { + endvar = '/' + this.endNpt; + } + return mw.replaceUrlParams( this.src, + { + 't': mw.seconds2npt( serverSeekTime ) + endvar + } + ); + }, + /** + * Title accessor function. + * + * @return {String} Title of the source. + */ + getTitle : function() { + if( this.title ){ + return this.title; + } + // Text tracks use "label" instead of "title" + if( this.label ){ + return this.label; + } + + // Return a Title based on mime type: + var mimeType = this.getMIMEType().split( ';' )[0]; + switch( mimeType ) { + case 'video/h264' : + case 'video/mp4' : + return mw.msg( 'mwe-embedplayer-video-h264' ); + break; + case 'video/x-flv' : + return mw.msg( 'mwe-embedplayer-video-flv' ); + break; + case 'video/webm' : + return mw.msg( 'mwe-embedplayer-video-webm'); + break; + case 'video/ogg' : + return mw.msg( 'mwe-embedplayer-video-ogg' ); + break; + case 'audio/ogg' : + return mw.msg( 'mwe-embedplayer-video-audio' ); + break; + case 'audio/mpeg' : + return mw.msg('mwe-embedplayer-audio-mpeg'); + break; + case 'video/3gp' : + return mw.msg('mwe-embedplayer-video-3gp'); + break; + case 'video/mpeg' : + return mw.msg('mwe-embedplayer-video-mpeg'); + break; + case 'video/x-msvideo' : + return mw.msg('mwe-embedplayer-video-msvideo' ); + break; + } + + // Return title based on file name: + try{ + var fileName = new mw.Uri( mw.absoluteUrl( this.getSrc() ) ).path.split('/').pop(); + if( fileName ){ + return fileName; + } + } catch(e){} + + // Return the mime type string if not known type. + return this.mimeType; + }, + /** + * Get a short title for the stream + */ + getShortTitle: function(){ + var _this =this; + if( this.shorttitle ){ + return this.shorttitle; + } + // Just use a short "long title" + var longTitle = this.getTitle(); + if(longTitle.length > 20) { + longTitle = longTitle.substring(0,17)+"..."; + } + return longTitle + }, + /** + * + * Get Duration of the media in milliseconds from the source url. + * + * Supports media_url?t=ntp_start/ntp_end url request format + */ + getURLDuration : function() { + // check if we have a URLTimeEncoding: + if ( this.URLTimeEncoding ) { + var annoURL = new mw.Uri( this.src ); + if ( annoURL.query.t ) { + var times = annoURL.query.t.split( '/' ); + this.startNpt = times[0]; + this.endNpt = times[1]; + this.startOffset = mw.npt2seconds( this.startNpt ); + this.duration = mw.npt2seconds( this.endNpt ) - this.startOffset; + } else { + // look for this info as attributes + if ( this.startOffset ) { + this.startNpt = mw.seconds2npt( this.startOffset ); + } + if ( this.duration ) { + this.endNpt = mw.seconds2npt( parseInt( this.duration ) + parseInt( this.startOffset ) ); + } + } + } + }, + /** + * Get the extension of a url + * @param String uri + */ + getExt : function( uri ){ + var urlParts = new mw.Uri( uri ); + // Get the extension from the url or from the relative name: + var ext = ( urlParts.file ) ? /[^.]+$/.exec( urlParts.file ) : /[^.]+$/.exec( uri ); + // remove the hash string if present + ext = /[^#]*/g.exec( ext.toString() ); + ext = ext || ''; + return ext.toString().toLowerCase(); + }, + /** + * Get the flavorId if available. + */ + getFlavorId: function(){ + if( this.flavorid ){ + return this.flavorid; + } + return ; + }, + + /** + * Attempts to detect the type of a media file based on the URI. + * + * @param {String} + * uri URI of the media file. + * @return {String} The guessed MIME type of the file. + */ + detectType: function( uri ) { + // NOTE: if media is on the same server as the javascript + // we can issue a HEAD request and read the mime type of the media... + // ( this will detect media mime type independently of the url name ) + // http://www.jibbering.com/2002/4/httprequest.html + switch( this.getExt( uri ) ) { + case 'smil': + case 'sml': + return 'application/smil'; + break; + case 'm4v': + case 'mp4': + return 'video/h264'; + break; + case 'm3u8': + return 'application/vnd.apple.mpegurl'; + break; + case 'webm': + return 'video/webm'; + break; + case '3gp': + return 'video/3gp'; + break; + case 'srt': + return 'text/x-srt'; + break; + case 'flv': + return 'video/x-flv'; + break; + case 'ogg': + case 'ogv': + return 'video/ogg'; + break; + case 'oga': + return 'audio/ogg'; + break; + case 'mp3': + return 'audio/mpeg'; + case 'm4a': + return 'audio/mp4'; + break; + case 'anx': + return 'video/ogg'; + break; + case 'xml': + return 'text/xml'; + break; + case 'avi': + return 'video/x-msvideo'; + break; + case 'mpg': + return 'video/mpeg'; + break; + case 'mpeg': + return 'video/mpeg'; + break; + } + mw.log( "Error: could not detect type of media src: " + uri ); + }, + /** + * bitrate is mesured in kbs rather than bandwith bytes per second + */ + getBitrate: function() { + if( this.bandwidth ){ + return this.bandwidth / 1024; + } + return 0; + }, + /** + * Get the size of the stream in bytes + */ + getSize: function(){ + if( this.sizebytes ){ + return this.sizebytes; + } + return 0; + } +}; + +} )( mediaWiki, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.processEmbedPlayers.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.processEmbedPlayers.js new file mode 100644 index 00000000..0748ccd0 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.processEmbedPlayers.js @@ -0,0 +1,353 @@ +/** + * Selector based embedPlayer processing + * + * @param {Function=} + * callback Optional Function to be called once video interfaces + * are ready + * + */ + +( function( mw, $ ) { "use strict"; + +mw.processEmbedPlayers = function( playerSet, callback ) { + mw.log( 'processEmbedPlayers:: playerSet: ', playerSet); + // The player id list container + var playerIdList = []; + + // Check if the selected player set is ready if ready issue the parent callback + var areSelectedPlayersReady = function(){ + var playersLoaded = true; + $.each( playerIdList, function(inx, playerId){ + if( ! $( '#' + playerId )[0].playerReadyFlag ){ + playersLoaded = false; + return false; + } + }) + if( playersLoaded ){ + if( callback ){ + callback(); + } + } + } + + /** + * Adds a player element for the embedPlayer to rewrite + * + * uses embedPlayer interface on audio / video elements uses mvPlayList + * interface on playlist elements + * + * Once a player interface is established the following chain of functions + * are called; + * + * _this.checkPlayerSources() + * _this.setupSourcePlayer() + * _this.updatePlaybackInterface() + * _this.selectedPlayer.load() + * _this.showPlayer() + * + * @param {Element} + * playerElement DOM element to be swapped + */ + var addPlayerElement = function( playerElement ) { + var _this = this; + mw.log('EmbedPlayer:: addElement:: ' + playerElement.id ); + + // Be sure to "stop" the target ( Firefox 3x keeps playing + // the video even though its been removed from the DOM ) + if( playerElement.pause ){ + playerElement.pause(); + } + + // Allow modules to override the wait for metadata flag: + $( mw ).trigger( 'EmbedPlayerWaitForMetaCheck', playerElement ); + + // DOM *could* load height, width and duration eventually, in some browsers + // By default, don't bother waiting for this. + var waitForMeta = false; + + // if a plugin has told us not to waitForMeta, don't + if ( playerElement.waitForMeta !== false ) { + // Check if we should wait for metadata, after all + waitForMeta = waitForMetaCheck( playerElement ); + } + + var ranPlayerSwapFlag = false; + + // Local callback to runPlayer swap once playerElement has metadata + var runPlayerSwap = function () { + // Don't run player swap twice + if( ranPlayerSwapFlag ){ + return ; + } + ranPlayerSwapFlag = true; + mw.log( "processEmbedPlayers::runPlayerSwap::" + $( playerElement ).attr('id') ); + + var playerInterface = new mw.EmbedPlayer( playerElement ); + var inDomPlayer = swapEmbedPlayerElement( playerElement, playerInterface ); + + // IE/Edge with WebM components re-triggers autoplay after removal as well. + if( playerElement.pause ){ + playerElement.pause(); + } + + // Trigger the EmbedPlayerNewPlayer for embedPlayer interface + mw.log("processEmbedPlayers::trigger:: EmbedPlayerNewPlayer " + inDomPlayer.id ); + + // Allow plugins to add bindings to the inDomPlayer + $( mw ).trigger ( 'EmbedPlayerNewPlayer', inDomPlayer ); + + // Add a player ready binding: + $( inDomPlayer ).bind( 'playerReady.swap', function(event, id){ + $( inDomPlayer ).unbind( 'playerReady.swap' ); + areSelectedPlayersReady(); + }); + + // + // Allow modules to block player build out + // + // this is needed in cases where you need to do an asynchronous + // player interface setup. like iframes asynchronous announcing its ready for + // bindings that can affect player setup. + mw.log("EmbedPlayer::addPlayerElement :trigger startPlayerBuildOut:" + inDomPlayer.id ); + $( '#' + inDomPlayer.id ).triggerQueueCallback( 'startPlayerBuildOut', function(){ + // Issue the checkPlayerSources call to the new player + // interface: make sure to use the element that is in the DOM: + inDomPlayer.checkPlayerSources(); + }); + }; + + if( waitForMeta && mw.config.get('EmbedPlayer.WaitForMeta' ) ) { + mw.log('processEmbedPlayers::WaitForMeta ( video missing height (' + + $( playerElement ).attr('height') + '), width (' + + $( playerElement ).attr('width') + ') or duration: ' + + $( playerElement ).attr('duration') + ); + $( playerElement ).bind( "loadedmetadata", runPlayerSwap ); + + // Time-out of 5 seconds ( maybe still playable but no timely metadata ) + setTimeout( runPlayerSwap, 5000 ); + return ; + } else { + runPlayerSwap(); + return ; + } + }; + + /** + * Check if we should wait for metadata. + * + * @return true if the size is "likely" to be updated by waiting for metadata + * false if the size has been set via an attribute or is already loaded + */ + var waitForMetaCheck = function( playerElement ){ + var waitForMeta = false; + + // Don't wait for metadata for non html5 media elements + if( !playerElement ){ + return false; + } + if( !playerElement.tagName || ( playerElement.tagName.toLowerCase() != 'audio' && playerElement.tagName.toLowerCase() != 'video' ) ){ + return false; + } + // If we don't have a native player don't wait for metadata + if( !mw.EmbedTypes.getMediaPlayers().isSupportedPlayer( 'oggNative') && + !mw.EmbedTypes.getMediaPlayers().isSupportedPlayer( 'webmNative') && + !mw.EmbedTypes.getMediaPlayers().isSupportedPlayer( 'h264Native' ) && + !mw.EmbedTypes.getMediaPlayers().isSupportedPlayer( 'appleVdnPlayer' ) ) + { + return false; + } + + + var width = $( playerElement ).css( 'width' ); + var height = $( playerElement ).css( 'height' ); + // Css video defaults ( firefox ) + if( $( playerElement ).css( 'width' ) == '300px' && + $( playerElement ).css( 'height' ) == '150px' + ){ + waitForMeta = true; + } else { + // Check if we should wait for duration: + if( $( playerElement ).attr( 'duration') || + $( playerElement ).attr( 'durationHint') || + $( playerElement ).attr('data-durationhint') + ){ + // height, width and duration set; do not wait for meta data: + return false; + } else { + waitForMeta = true; + } + } + + // Firefox ~ sometimes~ gives -1 for unloaded media + if ( $(playerElement).attr( 'width' ) == -1 || $(playerElement).attr( 'height' ) == -1 ) { + waitForMeta = true; + } + + // Google Chrome / safari gives 0 width height for unloaded media + if( $(playerElement).attr( 'width' ) === 0 || + $(playerElement).attr( 'height' ) === 0 + ) { + waitForMeta = true; + } + + // Firefox default width height is ~sometimes~ 150 / 300 + if( playerElement.height == 150 && playerElement.width == 300 ){ + waitForMeta = true; + } + + // Make sure we have a src attribute or source child + // ( i.e not a video tag to be dynamically populated or looked up from + // xml resource description ) + if( waitForMeta && + ( + $( playerElement ).attr('src') || + $( playerElement ).find("source[src]").length !== 0 + ) + ) { + // Detect src type ( if no type set ) + return true; + } else { + // playerElement is not likely to update its meta data ( no src ) + return false; + } + }; + + /** + * swapEmbedPlayerElement + * + * Takes a video element as input and swaps it out with an embed player interface + * + * @param {Element} + * targetElement Element to be swapped + * @param {Object} + * playerInterface Interface to swap into the target element + */ + var swapEmbedPlayerElement = function( targetElement, playerInterface ) { + mw.log( 'processEmbedPlayers::swapEmbedPlayerElement: ' + targetElement.id ); + // Create a new element to swap the player interface into + var swapPlayerElement = document.createElement('div'); + + // Add a class that identifies all embedPlayers: + $( swapPlayerElement ).addClass( 'mwEmbedPlayer' ); + + // Get properties / methods from playerInterface: + for ( var method in playerInterface ) { + if ( method != 'readyState' ) { // readyState crashes IE ( don't include ) + swapPlayerElement[ method ] = playerInterface[ method ]; + } + } + // copy over css text: + swapPlayerElement.style.cssText = targetElement.style.cssText; + // player element must always be relative to host video and image layout + swapPlayerElement.style.position = 'relative'; + + // Copy any data attributes from the target player element over to the swapPlayerElement + var dataAttributes = mw.config.get("EmbedPlayer.DataAttributes"); + if( dataAttributes ){ + $.each( dataAttributes, function( attrName, na ){ + if( $( targetElement ).data( attrName ) ){ + $( swapPlayerElement ).data( attrName, $( targetElement ).data( attrName ) ); + } + }); + } + // Check for Persistent native player ( should keep the video embed around ) + if( playerInterface.isPersistentNativePlayer() + || + // Also check for native controls on a video or audio tag + ( playerInterface.useNativePlayerControls() + && + ( targetElement.nodeName == 'video' || targetElement.nodeName == 'audio' ) + ) + ) { + + $( targetElement ) + .attr( 'id', playerInterface.pid ) + .addClass( 'nativeEmbedPlayerPid' ) + .show() + .after( + $( swapPlayerElement ).css( 'display', 'none' ) + ); + + } else { + $( targetElement ).replaceWith( swapPlayerElement ); + } + + // If we don't already have a loadSpiner add one: + if( $('#loadingSpinner_' + playerInterface.id ).length == 0 && $.client.profile().name !== 'firefox' ){ + if( playerInterface.useNativePlayerControls() || playerInterface.isPersistentNativePlayer() ) { + var $spinner = $( targetElement ) + .getAbsoluteOverlaySpinner(); + }else{ + var $spinner = $( swapPlayerElement ).getAbsoluteOverlaySpinner(); + } + $spinner.attr('id', 'loadingSpinner_' + playerInterface.id ); + } + return swapPlayerElement; + }; + + // Add a loader for <div /> embed player rewrites: + $( playerSet ).each( function( index, playerElement) { + + // Make sure the playerElement has an id: + if( !$( playerElement ).attr('id') ){ + $( playerElement ).attr( "id", 'mwe_vid' + ( index ) ); + } + // Add the player Id to the playerIdList + playerIdList.push( $( playerElement ).attr( "id") ); + + // If we are dynamically embedding on a "div" check if we can + // add a poster image behind the loader: + if( playerElement.nodeName.toLowerCase() == 'div' + && + $(playerElement).attr( 'poster' ) ) + { + var posterSrc = $(playerElement).attr( 'poster' ); + + // Set image size: + var width = $( playerElement ).width(); + var height = $( playerElement ).height(); + if( !width ){ + var width = '100%'; + } + if( !height ){ + var height = '100%'; + } + + mw.log('EmbedPlayer:: set loading background: ' + posterSrc); + $( playerElement ).append( + $( '<img />' ) + .attr( 'src', posterSrc) + .css({ + 'position' : 'absolute', + 'width' : width, + 'height' : height + }) + ); + } + }); + + // Make sure we have user preference setup for setting preferences on video selection + var addedPlayersFlag = false; + mw.log("processEmbedPlayers:: Do: " + $( playerSet ).length + ' players '); + // Add each selected element to the player manager: + $( playerSet ).each( function( index, playerElement) { + // Make sure the video tag was not generated by our library: + if( $( playerElement ).hasClass( 'nativeEmbedPlayerPid' ) ){ + $( '#loadingSpinner_' + $( playerElement ).attr('id') ).remove(); + mw.log( 'processEmbedPlayers::$.embedPlayer skip embedPlayer gennerated video: ' + playerElement ); + } else { + addedPlayersFlag = true; + // Add the player + addPlayerElement( playerElement ); + } + }); + if( !addedPlayersFlag ){ + // Run the callback directly if no players were added + if( callback ){ + callback(); + } + } +}; + +})( mw, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/EmbedPlayer.css b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/EmbedPlayer.css new file mode 100644 index 00000000..9699c2e9 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/EmbedPlayer.css @@ -0,0 +1,166 @@ +.ui-dialog-content .mediaContainer { margin: 0 auto; } +.mwPlayerContainer, video { width: 100%; height: 100%; } +.mwPlayerContainer { position: relative; height: 100%; background: #000; } +.videoHolder { + position: absolute; + top: 0px; + left:0px; + right:0px; + bottom:0px; + overflow: hidden; +} +.mwPlayerContainer.fullscreen { + position: absolute !important; + width: 100% !important; + height: 100%! important; + z-index: 9999; + min-height: 100%; + top: 0; + left: 0; + margin: 0; +} + +.mwEmbedPlayer { width: 100%; height: 100%; overflow: hidden; position: absolute; top: 0; left: 0; } + + +.player_select_list { + color:white; + font-size:10pt; +/* display:none;*/ +} +.player_select_list a:visited { + color:white; +} +.mv_playhead { + position:absolute; + top:0; + left:0; + width:17px; + height:21px; + /*http://art.gnome.org/themes/gtk2*/ +} + +.mv_status { + font-family:"Times New Roman", Times, serif; + font-size:14px; + float:left; +} +.set_ogg_player_pref{ + text-align:left; +} + +.large_play_button { + display:block; + width: 130px; + height: 96px; + margin: auto; +/* margin: -202px 0 0 154px;*/ + position: absolute; + z-index: 3; + cursor: pointer; +} + +/* jquery.ui overrides */ + +.ui-icon_link { + padding: .4em 1em .4em 20px; + text-decoration: none; + position: relative; +} +.ui-icon_link span.ui-icon { + margin: 0 5px 0 0; + position: absolute; + left: 0.2em; + right: auto; + top: 50%; + margin-top: -8px; + zoom: 1; +} +.ui-icon_link span.ui-text { + position: absolute; + left: 0.2em; + right: auto; + margin-top: -3px; + zoom: 1; +} + +.ui-progressbar-value{ + background-image: none; +} + +.kplayer .ui-widget-overlay { + background: black; opacity: .40; filter: Alpha(Opacity=40); +} + +.kplayer .ui-widget-content input { + padding: 5px; +} +.kplayer .ui-widget-content a { + color: #222; +} + +ul.ui-provider-selection { + list-style-type: none; + margin: 0 0 0.6em 0; + overflow: hidden; + padding: 0; + text-align: center; +} + +ul.ui-provider-selection li { + border-left: 1px solid black; + float: left; + line-height: 1.1em; + margin: 0 0.5em 0 -0.5em; + padding: 0 0.5em; + color: blue; + list-style-image:none; + cursor:pointer; +} + +ul.ui-provider-selection li .ui-selected { + color: black; + font-weight: bold; +} + +ul.ui-provider-selection li a.ui-active { + color: black; + font-weight: bold; +} + +ul.ui-provider-selection li a { + color: blue; + text-decoration: none; +} +.fg-menu .ui-icon{ + position:relative; + top:-1px; +} + +.ui-dialog-buttonpane a{ + float: right; + margin-right: 10px; +} + + +/* Custom */ +.mv-player .overlay-win { background: transparent; border: 0; } /* Custom */ +.mv-player .overlay-content { padding: 10px; } +.mv-player .overlay-content h3 { display: block; font-size: 16px; font-weight: bold; color: #fff; font-family: arial; } +.mv-player .overlay-win h2 { font-size: 18px; margin-top: 0; } +.mv-player .overlay-content div { font-size: 12px; color: #fff; font-weight: bold; } +.mv-player .overlay-content div a { color: #00a8ff } +.mv-player .overlay-content div a:hover { color: #3abcff } +.mv-player .overlay-content ul { list-style: none; margin: 0 0 10px 0; padding: 0; } +.mv-player .vol_container { + background: #272727; + opacity: .80; + filter:Alpha(Opacity=80); + position:absolute; + left:0px; +} +.mv-player .ui-icon ui-icon-closethick { border: 1px solid #606060; background: #222;font-weight: normal; color: #EEE; } +.mv-player .overlay-win textarea { background: #e4e4e4; height: 35px; padding: 6px; color: #666; border: 0; } +.mv-player .overlay-content .copycode { padding: 8px 12px; font-weight: bold; float: right; cursor: pointer; } +.control-bar .ui-icon_link { border: 0; } +.control-bar .ui-state-hover { border: 0; } diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/PlayerSkinKskin.css b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/PlayerSkinKskin.css new file mode 100644 index 00000000..f6c675d7 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/PlayerSkinKskin.css @@ -0,0 +1,484 @@ +/* +* K-skin player +*/ +.k-player { + color: #FFF; + background-color: #000; +} +.k-player .videoHolder a { + color: #0645AD; +} +.k-player .videoHolder a:visited { + color: #0B0080; +} +.k-player .ui-widget-content { + color: #555; + z-index: 1503; +} +.k-player .ui-widget-content a{ + color: #555; +} +/* large play button */ +.k-player .play-btn-large { + width: 70px; + height: 55px; + background: url(images/ksprite.png) no-repeat 0px -433px; + position: absolute; + cursor: pointer; + border: none; +} +@print { + .k-player .play-btn-large { + display: none; + } +} +/*.ui-state-default */ +.k-player .play-btn-large:hover { + background: url(images/ksprite.png) no-repeat 0px -377px; +} + +/* control icons: */ +.k-player .control-bar .ui-icon,.k-player .control-bar .ui-icon{ + background: transparent url(images/ksprite.png) no-repeat scroll 0 -48px; +} + +.k-player .ui-state-default .ui-icon-arrow-4-diag { + background-position: 0 -32px; +} +/* fullscreen */ +.k-player .ui-state-hover .ui-icon-arrow-4-diag { + background-position: -16px -32px; +} + +.k-player .ui-state-hover .ui-icon-volume-on{ + background-position: -16px -48px; +} + +/* cc icon */ +.k-player .ui-state-default .ui-icon-comment { + background-position: 0px -65px; +} + +.k-player .ui-state-default .ui-icon-play { + background: url(images/ksprite.png) no-repeat 0 0; +} + +.k-player .ui-state-hover .ui-icon-play { + background-position: -16px 0; +} + +.k-player .ui-state-default .ui-icon-pause { + background: url(images/ksprite.png) no-repeat 0 -17px; +} + +.k-player .ui-state-hover .ui-icon-pause { + background-position: -16px -17px; +} + +.k-player .control-bar { + border:1px solid #c8c8c8; + border-top: 0px; + border-right: 0px; + height: 21px; + padding: 2px 0 0 6px; + margin-top: 0px; + background: url(images/ksprite.png) repeat-x 0 -81px; + font: normal 11px arial, sans-serif; + color: #555; + + position:absolute; + bottom:0px; + left:0px; + right:0px; + + z-index: 2; +} + +.k-player .play_head { + background: url("images/ksprite.png") repeat-x scroll 0 -350px + transparent; + display: inline; + /* @noflip */ + float: left; + margin-left: 10px; + border: 1px solid #EEEEEE; + height: 8px; + margin: 5px 2px 0 0px; + position: relative; + /* @noflip */ + direction: ltr; +} + +.k-player .play_head .ui-slider-handle { + background: url("images/ksprite.png") no-repeat scroll -67px -341px + transparent !important; + border: 1px solid #888888; + display: block; + height: 8px; + margin: -1px 0 0 -5px; + position: absolute; + top: 0; + width: 8px; + cursor: pointer; + -moz-border-radius:5px 5px 5px 5px; + border-radius:5px 5px 5px 5px; + -webkit-border-radius:5px 5px 5px 5px; +} + +.k-player .ui-corner-all { + border-radius:5px 5px 5px 5px !important; + -webkit-border-radius:5px 5px 5px 5px !important; + -moz-border-radius:5px 5px 5px 5px !important; +} +.k-player ul.fg-menu{ + margin: 0.3em 0 0 .3em; + font-size: 1.2em; + padding: 0px; +} +.k-player .fg-menu-container{ + padding: 0px; + /* @noflip */ + right: 16px; + /* Add scroll bar to list */ + overflow: auto; +} + +.ui-dialog-content .k-player ul.fg-menu{ + font-size: 1.2em; +} + +.k-player .time-disp { + border: medium none; + display: inline; + color: #555555; + font: 11px arial, sans-serif; + line-height: 20px; + overflow: hidden; + width: 39px; + /* @noflip */ + float: right; +} + +.k-player .source-switch { + border: medium none; + display: inline; + color: #555; + font: 11px arial, sans-serif; + line-height: 20px; + overflow: hidden; + width: 70px; + cursor: pointer; + /* @noflip */ + float: right; + text-align: center; +} + +.k-player .lButton { + cursor: pointer; + /* @noflip */ + float: left; + list-style: none outside none; + margin: 2px; + padding: 0px 0; + width: 19px; + height: 16px; + position: relative; + background: none repeat scroll 0 0 transparent !important; + border: medium none; +} + +.k-player .rButton { + cursor: pointer; + /* @noflip */ + float: right; + list-style: none outside none; + margin-top: 2px; + padding: 0px 0; + width: 22px; + height: 16px; + position: relative; + background: none repeat scroll 0 0 transparent !important; + border: medium none; +} + +.k-player .k-options { + border: 1px solid #AAAAAA !important; + color: #555555 !important; + /* @noflip */ + float: right; + height: 21px; + margin-top: -2px; + margin-right: 0px; + width: 50px; + background: none repeat scroll 0 0 transparent !important; + font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; + font-size: 11px; + text-transform: uppercase; + text-align: center; +} + +.k-player .k-options span { + position: relative; + top: 4px; +} + +.k-player .k-menu-screens { + /* @noflip */ + float: left; + font-size: 14px; + text-align: left; + padding: 5px 5px 10px 5px; +} + +.k-player ul.k-menu-bar { + background: url("images/ksprite.png") no-repeat scroll -99px -104px + transparent; + bottom: 5px; + height: 128px; + list-style: none outside none; + padding: 0 0 5px; + position: absolute; + /* @noflip */ + right: 0; + margin-left: 0; +} + +.k-player .k-menu { + background: none repeat scroll 0 0 #181818; + border: medium none; + display: none; + left: 0; + position: absolute; + top: 0; +} + +.k-player .k-menu-bar li a { + background: url("images/ksprite.png") no-repeat scroll -51px -110px + transparent; + display: block; + height: 32px; + margin-left: 1px; + overflow: hidden; + text-indent: 99999px; + width: 49px; +} + +.k-menu-bar li a:hover { + background-position: -1px -110px; +} + +.k-menu-bar li.k-download-btn a { + background-position: -51px -203px; +} + +.k-menu-bar li.k-download-btn a:hover { + background-position: -1px -203px; +} + +.k-menu-bar li.k-share-btn a { + background-position: -51px -172px; +} + +.k-menu-bar li.k-share-btn a:hover { + background-position: -1px -172px; +} + +.k-menu-bar li.k-credits-btn a { + background-position: -51px -141px; +} + +.k-menu-bar li.k-credits-btn a:hover { + background-position: -1px -141px; +} + + + +.k-menu-screens p { + margin: 6px 0; +} + +.k-menu-screens a img { + border: none; +} + +.k-menu-screens ul { + padding: 0; + margin: 6px 0 0; + list-style: none outside none; +} + +.k-edit-screen { + width: 370px; + height: 223px; + padding-top: 77px; + text-align: center; + background: #181818; + color: #fff; +} + +.k-edit-screen div { + +} + +.k-edit-screen a { + color: #7BB8FC; +} + +.k-edit-screen a img { + border: none; +} + + +.k-menu-screens h2, .k-menu-screens h3 { + padding: 0 0 5px 15px; + clear: both; + font-size: 12px; + color: #999; + border-bottom: 0; +} + +.k-menu-screens p { + margin: 6px 0; +} + +.k-menu-screens a img { + border: none; +} + +.k-menu-screens ul { + padding: 0; + margin: 6px 0 0; + list-style: none outside none; +} + +.k-menu-screens li { + margin-bottom: 6px; +} + +.k-menu-screens li a { + padding-left: 22px; + padding-right: 22px; + background: url(images/ksprite.png) no-repeat -85px -274px; + text-decoration: none; + color: #BBB; +} + +.k-menu-screens li a.active,.k-menu-screens li a:hover .active { + background-position: -85px -245px; +} + +.k-menu-screens li a:hover { + background-position: -85px -259px; +} + +.k-menu textarea { + background: none repeat scroll 0 0 transparent; + border-color: #000000 -moz-use-text-color -moz-use-text-color #000000; + border-style: solid none none solid; + border-width: 2px medium medium 2px; + color: #CCCCCC; + font: 11px arial, sans-serif; + overflow: hidden; + padding-left: 2px; + width: 95%; +} + +.menu-screen.menu-share button { + background: url("images/ksprite.png") no-repeat scroll 0 -81px #D4D4D4; + border: 1px solid #000000; + color: #000000; + float: right; + height: 34px; + padding: 0 5px 3px; + font-size: 1em; +} + +.k-player .menu-screen { + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + + +.k-player .menu-screen.menu-share div.ui-state-highlight { + background: none repeat scroll 0 0 transparent; + border-color: #554926; + color: #FFE96E; + float: left; + padding: 2px 5px; +} + +.k-player .menu-screen.menu-share div.ui-state-highlight a { + color: #FFE96E; + font-weight: bold; +} + +.k-player .volume_control { + /* @noflip */ + margin-right: 2px; + width: 16px; +} + +.k-player .volume_control span { + margin-right: 0px; +} + +.k-player .volume-slider { + width: 20px; + /* @noflip */ + direction: ltr; +} + +.k-player .volume-slider .ui-slider-range { + -moz-border-radius: 0 0 0 0; + background: url("images/ksprite.png") repeat-x scroll -66px -306px transparent !important; + height: 17px; + position: absolute; +} + +.k-player .volume-slider a.ui-slider-handle { + background: none repeat scroll 0 0 transparent; + border: medium none; + display: block; + height: 18px; + margin: -3px 5px 0 -1px; + position: absolute; + width: 8px; +} + +.k-player .ui-slider-horizontal .ui-slider-range-min { + /* @noflip */ + left: 0; +} + +.k-player .credits_box { + background-attachment:scroll; + background-color:white; + background-image:none; + background-position:0 0; + bottom: 20px; + left: 20px; + position:absolute; + right: 20px; + top: 30px; + overflow:hidden; +} +.k-player .credits_box a{ + color:#666; + text-decoration: underline; +} +.k-player .creditline img { + float: left; + width: 90px; + margin: 4px; +} + +.k-player .k-attribution{ + position:absolute; + bottom: 5px; + right : 20px; + background: url("images/kaltura_open_source_video_platform.png"); + width : 51px; + height : 12px; + cursor: pointer; +} diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gif b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gif Binary files differnew file mode 100644 index 00000000..cf05af25 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gif diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.png b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.png Binary files differnew file mode 100644 index 00000000..77f2a32a --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.png diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png Binary files differnew file mode 100644 index 00000000..53c772eb --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/mw.PlayerSkinKskin.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/mw.PlayerSkinKskin.js new file mode 100644 index 00000000..39b43834 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/mw.PlayerSkinKskin.js @@ -0,0 +1,394 @@ +/** +* Skin js allows you to override contrlBuilder html/class output +*/ + +( function( mw, $ ) {"use strict"; + +mw.PlayerSkinKskin = { + + // The parent class for all kskin css: + playerClass: 'k-player', + + // Display time string length + longTimeDisp: false, + + // Default control bar height + height: 20, + + // Volume control layout is horizontal + volumeLayout: 'horizontal', + + // Skin "kskin" is specific for wikimedia we have an + // api Title key so the "credits" menu item can be showed. + supportedMenuItems: { + 'credits': true + }, + // Stores the current menu item id + currentMenuItem: null, + + // Extends base components with kskin specific options: + components: { + 'pause': { + 'w': 28 + }, + 'volumeControl': { + 'w': 40 + }, + 'playButtonLarge' : { + 'h' : 55 + }, + 'options': { + 'w': 52, + 'o': function( ctrlObj ) { + return $( '<div />' ) + .attr( 'title', mw.msg( 'mwe-embedplayer-player_options' ) ) + .addClass( "ui-state-default ui-corner-bl rButton k-options" ) + .append( + $( '<span />' ) + .text( mw.msg( 'mwe-embedplayer-menu_btn' ) ) + ); + } + }, + // No attributionButton component for kSkin ( its integrated into the credits screen ) + 'attributionButton' : false, + + // Time display: + 'timeDisplay': { + 'w': 52 + }, + 'optionsMenu': { + 'w' : 0, + 'o' : function( ctrlObj ) { + var embedPlayer = ctrlObj.embedPlayer; + var $menuOverlay = $( '<div />') + .addClass( 'overlay-win k-menu ui-widget-content' ) + .css( { + 'width' : '100%', + 'position': 'absolute', + 'top' : '0px', + 'bottom' : ( ctrlObj.getHeight() + 2 ) + 'px' + } ); + + // Note safari can't display video overlays with text: + // see bug https://bugs.webkit.org/show_bug.cgi?id=48379 + + var userAgent = navigator.userAgent.toLowerCase(); + if( userAgent.indexOf('safari') != -1 ){ + $menuOverlay.css('opacity', '0.9'); + } + // Setup menu offset ( if player height < getOverlayHeight ) + // This displays the menu outside of the player on small embeds + if ( embedPlayer.getPlayerHeight() < ctrlObj.getOverlayHeight() ) { + var topPos = ( ctrlObj.isOverlayControls() ) + ? embedPlayer.getPlayerHeight() + : embedPlayer.getPlayerHeight() + ctrlObj.getHeight(); + + if( embedPlayer.isAudio() ){ + topPos = ctrlObj.embedPlayer.getInterface().height(); + } + + $menuOverlay.css( { + 'top' : topPos + 'px', + 'bottom' : null, + 'width' : ctrlObj.getOverlayWidth(), + 'height' : ctrlObj.getOverlayHeight() + 'px' + } ); + // Special common overflow hack for thumbnail display of player + $( embedPlayer ).parents( '.thumbinner' ).css( 'overflow', 'visible' ); + } + + var $menuBar = $( '<ul />' ) + .addClass( 'k-menu-bar' ); + + // Don't include about player menu item ( FIXME should be moved to a init function ) + delete ctrlObj.supportedMenuItems['aboutPlayerLibrary']; + + // Output menu item containers: + for ( var menuItem in ctrlObj.supportedMenuItems ) { + // Give grep a chance to find the usages: + // mwe-embedplayer-playerSelect, mwe-embedplayer-download, + // mwe-embedplayer-share, mwe-embedplayer-credits + $menuBar.append( + $( '<li />') + // Add the menu item class: + .addClass( 'k-' + menuItem + '-btn' ) + .attr( 'rel', menuItem ) + .append( + $( '<a />' ) + .attr( { + 'title' : mw.msg( 'mwe-embedplayer-' + menuItem ), + 'href' : '#' + }) + ) + ); + } + + // Add the menuBar to the menuOverlay + $menuOverlay.append( $menuBar ); + + var $menuScreens = $( '<div />' ) + .addClass( 'k-menu-screens' ) + .css( { + 'position' : 'absolute', + 'top' : '0px', + 'left' : '0px', + 'bottom' : '0px', + 'right' : '45px', + 'overflow' : 'hidden' + } ); + for ( var menuItem in ctrlObj.supportedMenuItems ) { + $menuScreens.append( + $( '<div />' ) + .addClass( 'menu-screen menu-' + menuItem ) + ); + } + + // Add the menuScreens to the menuOverlay + $menuOverlay.append( $menuScreens ); + + return $menuOverlay; + + } + } + }, + + /** + * Get minimal width for interface overlay + */ + getOverlayWidth: function(){ + return ( this.embedPlayer.getPlayerWidth() < 220 )? 220 : this.embedPlayer.getPlayerWidth(); + }, + + /** + * Get minimal height for interface overlay + */ + getOverlayHeight: function(){ + return ( this.embedPlayer.getPlayerHeight() < 160 )? 160 : this.embedPlayer.getPlayerHeight(); + }, + + /** + * Adds the skin Control Bindings + */ + addSkinControlBindings: function() { + var embedPlayer = this.embedPlayer; + var _this = this; + + // Set up control bar pointer + this.$playerTarget = embedPlayer.$interface; + // Set the menu target: + + + // Options menu display: + this.$playerTarget.find( '.k-options' ) + .unbind() + .click( function() { + _this.checkMenuOverlay(); + var $kmenu = _this.$playerTarget.find( '.k-menu' ); + if ( $kmenu.is( ':visible' ) ) { + _this.closeMenuOverlay( ); + } else { + _this.showMenuOverlay(); + // no other item is selected by default show the media credits: + if ( !_this.currentMenuItem ){ + _this.showMenuItem('credits'); + // Hide the others + _this.$playerTarget.find( '.menu-screen' ).hide(); + // Show credits + _this.$playerTarget.find( '.menu-credits' ).fadeIn( "fast" ); + } + } + } ); + + }, + + /** + * checks for menu overlay and runs menu bindings if unset + */ + checkMenuOverlay: function(){ + var _this = this; + var embedPlayer = this.embedPlayer; + if ( _this.$playerTarget.find( '.k-menu' ).length == 0 ) { + // Stop the player if it does not support overlays: + if ( !embedPlayer.supports['overlays'] ) { + embedPlayer.stop(); + } + + // Add the menu binding + _this.addMenuBinding(); + } + }, + + /** + * Close the menu overlay + */ + closeMenuOverlay: function() { + mw.log("PlayerSkinKskin:: close menu overlay" ); + var embedPlayer = this.embedPlayer; + var $optionsMenu = embedPlayer.getInterface().find( '.k-options' ); + var $kmenu = embedPlayer.getInterface().find( '.k-menu' ); + $kmenu.fadeOut( "fast", function() { + $optionsMenu.find( 'span' ) + .text ( mw.msg( 'mwe-embedplayer-menu_btn' ) ); + } ); + // show the play button if not playing + if( !embedPlayer.isPlaying() ){ + embedPlayer.getInterface().find( '.play-btn-large' ).fadeIn( 'fast' ); + } + + // re-display the control bar if hidden: + this.showControlBar(); + + // Set close overlay menu flag: + this.displayOptionsMenuFlag = false; + }, + + /** + * Show the menu overlay + */ + showMenuOverlay: function( $ktxt ) { + var $optionsMenu = this.$playerTarget.find( '.k-options' ); + var $kmenu = this.$playerTarget.find( '.k-menu' ); + + $kmenu.fadeIn( "fast", function() { + $optionsMenu.find( 'span' ) + .text ( mw.msg( 'mwe-embedplayer-close_btn' ) ); + } ); + this.$playerTarget.find( '.play-btn-large' ).fadeOut( 'fast' ); + + $(this.embedPlayer).trigger( 'displayMenuOverlay' ); + + // Set the Options Menu display flag to true: + this.displayOptionsMenuFlag = true; + }, + + /** + * Adds binding for the options menu + * + * @param {Object} $tp Target video container for + */ + addMenuBinding: function() { + var _this = this; + var embedPlayer = this.embedPlayer; + // Set local player target pointer: + var $playerTarget = embedPlayer.$interface; + + // Check if k-menu already exists: + if ( $playerTarget.find( '.k-menu' ).length != 0 ) + return false; + + // Add options menu to top of player target children: + $playerTarget.append( + _this.getComponent( 'optionsMenu' ) + ); + + // By default its hidden: + $playerTarget.find( '.k-menu' ).hide(); + + // Add menu-items bindings: + for ( var menuItem in _this.supportedMenuItems ) { + $playerTarget.find( '.k-' + menuItem + '-btn' ).click( function( ) { + + // Grab the context from the "clicked" menu item + var mk = $( this ).attr( 'rel' ); + + // hide all menu items + var $targetItem = $playerTarget.find( '.menu-' + mk ); + + // call the function showMenuItem + _this.showMenuItem( mk ); + + // Hide the others + $playerTarget.find( '.menu-screen' ).hide(); + + // Show the target menu item: + $targetItem.fadeIn( "fast" ); + + // Don't follow the # link + return false; + } ); + } + }, + + /** + * Shows a selected menu_item + * + * NOTE: this should be merged with parent mw.PlayerControlBuilder optionMenuItems + * binding mode + * + * @param {String} menu_itme Menu item key to display + */ + showMenuItem:function( menuItem ) { + var embedPlayer = this.embedPlayer; + this.currentMenuItem = menuItem; + //handle special k-skin specific display; + switch( menuItem ){ + case 'credits': + this.showCredits(); + break; + case 'playerSelect': + embedPlayer.$interface.find( '.menu-playerSelect').html( + this.getPlayerSelect() + ); + break; + case 'download' : + embedPlayer.$interface.find( '.menu-download').text( + mw.msg('mwe-loading_txt' ) + ); + // Call show download with the target to be populated + this.showDownload( + embedPlayer.$interface.find( '.menu-download') + ); + break; + case 'share': + embedPlayer.$interface.find( '.menu-share' ).html( + this.getShare() + ); + break; + } + }, + + /** + * Show the credit screen ( presently specific to kaltura skin ) + */ + showCredits: function() { + // Set up the shortcuts: + var embedPlayer = this.embedPlayer; + var _this = this; + var $target = embedPlayer.$interface.find( '.menu-credits' ); + + $target.empty().append( + $('<h2 />') + .text( mw.msg( 'mwe-embedplayer-credits' ) ), + $('<div />') + .addClass( "credits_box ui-corner-all" ) + .append( + $('<div/>') + .loadingSpinner() + .css({'position':'absolute','top':'50%','left':'50%'}) + ) + ); + + if( mw.config.get( 'EmbedPlayer.KalturaAttribution' ) == true ){ + $target.append( + $( '<div />' ) + .addClass( 'k-attribution' ) + .attr({ + 'title': mw.msg('mwe-embedplayer-kaltura-platform-title') + }) + .click( function( ) { + window.location = 'http://html5video.org'; + }) + ); + } + var $creditBox = $target.find('.credits_box'); + $creditBox.data( 'playerId', embedPlayer.id ); + $( embedPlayer ).triggerQueueCallback('showCredits', $creditBox, function( addedCredits ){ + if( !addedCredits ){ + $creditBox.find('.credits_box').text( mw.msg( 'mwe-embedplayer-nocredits') ) + } + }); + } + +}; + +} )( mw, jQuery ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/PlayerSkinMvpcf.css b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/PlayerSkinMvpcf.css new file mode 100644 index 00000000..a9664ec5 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/PlayerSkinMvpcf.css @@ -0,0 +1,194 @@ +/** + * reference player skin + */ + + +/*.ui-state-default */ +.mv-player a:link {color: #2060c1; text-decoration: underline;} +.mv-player a:visited {color: #2060c1; text-decoration: underline;} +/*a:visited {color: #75a5e4; text-decoration: underline;}*/ /*Not sure if you want this*/ +.mv-player a:hover {color: #75a5e4; text-decoration: underline;} +.mv-player img, .mv-player img a, .mv-player img a:hover {border: 0;} + + +.mv-player .video { + display: block; + position: relative; + font-size: 1px; + height: 305px; +} +.mv-player .control-bar { + overflow: hidden; + height: 29px; + margin: 0; + padding: 0; + border: 0; + z-index: 2; +} +.mv-player .controlInnerSmall { +/* width: 430px;*/ + height: 29px; + float: left; + display: inline; +} + +.mv-player .lButton { + cursor:pointer; + float:left; + list-style:none outside none; + margin:2px; + padding:4px 0; + width: 24px; + height:16px; + position:relative; +} +.mv-player .rButton { + cursor:pointer; + float:right; + list-style:none outside none; + margin:2px; + padding:4px 0; + width: 23px; + height:16px; + position:relative; +} + +.mv-player .volume_icon { + float: right; + display: inline; + width: 22px; + height: 29px; + padding: 0 0 0 0; + +} + +.mv-player .vol_container{ + z-index:99; + width:23px; + height:75px; + width:23px; + position:absolute; + left:0px; + background: #CCC; +} +.mv-player .vol_container_below{ + top:30px; +} +.mv-player .vol_container_top{ + top:-77px; +} +.mv-player .vol_container .volume-slider{ + margin-top:5px; + height:65px; + width:10px; + margin-left: auto ; + margin-right: auto ; +} +.mv-player .vol_container .ui-slider-handle{ + cursor : pointer; + width:10px; + height:10px; + position:absolute; + left:-1px; +} + +.mv-player .time-disp { + line-height: 32px; + height: 29px; + overflow: visible; + font-size: 10.2px; + float: right; + display: inline; + border:none; + padding-right:4px; +} + +.mv-player .source-switch { + border: medium none; + display: inline; + color: #eee; + font: 11px arial, sans-serif; + line-height: 20px; + overflow: hidden; + width: 70px; + cursor: pointer; + float: right; + text-align: center; + padding-top:6px; +} + + +.mv-player .play_head{ + float: left; + display: inline; + height: 10px; + margin-left:8px; + margin-top:10px; + margin-right: 8px; + position:relative; +} + +.mv-player .play_head .ui-slider-handle{ + width:10px; + height:15px; + margin-left:-5px; + margin-top: -0px; + z-index: 2; +} + +.mv-player .inOutSlider .ui-slider-handle{ + width:8px; + cusror: move; +} + +.mv-player .overlay-win textarea { + background:none repeat scroll 0 0 transparent; + border: 2px solid #333; + color: #fff; + font: 11px arial,sans-serif; + height:15px; + overflow:hidden; + padding-left:2px; + width:97%; +} + +.mv-player .overlay-win div.ui-state-highlight { + background:none repeat scroll 0 0 transparent; + border-color:#554926; + color:#FFE96E; + float:left; + padding:2px 5px; +} + +.mv-player .videoOptionsComplete div.ui-state-highlight a { + color:#eee; + font-weight:bold; +} + +.mv-player .overlay-win h2{ + font-size: 115%; +} + +.mv-player .overlay-win{ + font-family : arial,sans-serif; + font-size : 85%; +} +.mv-player .overlay-win a{ + text-decoration: none; +} + +.mv-player .overlay-win ul{ + padding-left: 15px; +} + +.mv-player a:hover {} + +.mv-player .overlay-win ul li span { font-weight:bold; color:#fff;} + +.mv-player .overlay-win h2 { font-size:16px;} +.mv-player .overlay-win h3 { font-size:14px;} + +.active { font-size: 12px; } + +.ui-slider-horizontal.volume-slider { width: 44px; height: 2px; top: 7px; } +.ui-slider-horizontal.volume-slider .ui-slider-handle { border-width: 1px; } diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.png b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.png Binary files differnew file mode 100644 index 00000000..155f15e1 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.png diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/mw.PlayerSkinMvpcf.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/mw.PlayerSkinMvpcf.js new file mode 100644 index 00000000..4b270418 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/mw.PlayerSkinMvpcf.js @@ -0,0 +1,7 @@ +/* +mvpcf skin config +*/ + +mw.PlayerSkinMvpcf = { + playerClass : 'mv-player' +};
\ No newline at end of file diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mw.PlayerControlBuilder.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mw.PlayerControlBuilder.js new file mode 100644 index 00000000..5ef54d4f --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mw.PlayerControlBuilder.js @@ -0,0 +1,2721 @@ +/** +* Msg text is inherited from embedPlayer +*/ + +( function( mw, $ ) { "use strict"; +/** +* mw.PlayerControlBuilder object +* @param the embedPlayer element we are targeting +*/ +mw.PlayerControlBuilder = function( embedPlayer, options ) { + return this.init( embedPlayer, options ); +}; + +/** + * ControlsBuilder prototype: + */ +mw.PlayerControlBuilder.prototype = { + //Default Local values: + + // Parent css Class name + playerClass : 'mv-player', + + // Long string display of time value + longTimeDisp: true, + + // Default volume layout is "vertical" + volumeLayout : 'vertical', + + // Default control bar height + height: mw.config.get( 'EmbedPlayer.ControlsHeight' ), + + // Default supported components is merged with embedPlayer set of supported types + supportedComponents: { + // All playback types support options + 'options': true + }, + + // Default supported menu items is merged with skin menu items + supportedMenuItems: { + // Player Select + 'playerSelect' : true, + + // Download the file menu + 'download' : true, + + // Share the video menu + 'share' : true, + + // Player library link + 'aboutPlayerLibrary': true + }, + + // Flag to store the current fullscreen mode + inFullScreen: false, + + // Flag to store if a warning binding has been added + addWarningFlag: false, + + // Flag to store state of overlay on player + displayOptionsMenuFlag: false, + + // Local storage of ControlBar Callback + hideControlBarCallback: false, + + // Flag to store controls status (disabled/enabled) + controlsDisabled: false, + + // binding postfix + bindPostfix: '.controlBuilder', + + /** + * Initialization Object for the control builder + * + * @param {Object} embedPlayer EmbedPlayer interface + */ + init: function( embedPlayer ) { + var _this = this; + this.embedPlayer = embedPlayer; + // Check for skin overrides for controlBuilder + var skinClass = embedPlayer.skinName.substr(0,1).toUpperCase() + embedPlayer.skinName.substr( 1 ); + if ( mw['PlayerSkin' + skinClass ] ) { + // Clone as to not override prototype with the skin config + _this = $.extend( true, { }, this, mw['PlayerSkin' + skinClass ] ); + } + if ( _this.embedPlayer.mediaElement.getPlayableSources().length <= 1 + && _this.supportedMenuItems.playerSelect ) { + delete _this.supportedMenuItems.playerSelect; + } + // Return the controlBuilder Object: + return _this; + }, + + /** + * Get the control bar height + * @return {Number} control bar height + */ + getHeight: function(){ + return this.height; + }, + + + /** + * Add the controls html to player interface + */ + addControls: function() { + // Set up local pointer to the embedPlayer + var embedPlayer = this.embedPlayer, + profile = $.client.profile(); + + // Set up local controlBuilder + var _this = this; + + // Remove any old controls & old overlays: + embedPlayer.getInterface().find( '.control-bar,.overlay-win' ).remove(); + + // Reset flags: + _this.displayOptionsMenuFlag = false; + + // Setup the controlBar container ( starts hidden ) + var $controlBar = $('<div />') + .addClass( 'ui-state-default ui-widget-header ui-helper-clearfix control-bar' ) + .css( 'height', this.height ); + + // Controls are hidden by default if overlaying controls: + if( _this.isOverlayControls() ){ + $controlBar.hide(); + } else { + // Include the control bar height when calculating the layout + $controlBar.addClass('block'); + } + + // Make room for audio controls in the interface: + if( embedPlayer.isAudio() && embedPlayer.getInterface().height() == 0 ){ + embedPlayer.getInterface().css( { + 'height' : this.height + } ); + } + + // Add the controls to the interface + embedPlayer.getInterface().append( $controlBar ); + + if ( profile.name === 'firefox' && profile.versionNumber < 2 ) { + embedPlayer.triggerHelper( 'resizeIframeContainer', [ {'height' : embedPlayer.height + $controlBar.height() - 1} ] ); + } + + // Add the Controls Component + this.addControlComponents(); + + // Add top level Controls bindings + this.addControlBindings(); + }, + + /** + * Add control components as defined per this.components + */ + addControlComponents: function( ) { + var _this = this; + + // Set up local pointer to the embedPlayer + var embedPlayer = this.embedPlayer; + + //Set up local var to control container: + var $controlBar = embedPlayer.getInterface().find( '.control-bar' ); + + this.availableWidth = embedPlayer.getPlayerWidth(); + + mw.log( 'PlayerControlsBuilder:: addControlComponents into:' + this.availableWidth ); + // Build the supportedComponents list + this.supportedComponents = $.extend( this.supportedComponents, embedPlayer.supports ); + + // Check for Attribution button + if( mw.config.get( 'EmbedPlayer.AttributionButton' ) && embedPlayer.attributionbutton ){ + this.supportedComponents[ 'attributionButton' ] = true; + } + // Check global fullscreen enabled flag + if( mw.config.get( 'EmbedPlayer.EnableFullscreen' ) === false ){ + this.supportedComponents[ 'fullscreen'] = false; + } + // Check if the options item is available + if( mw.config.get( 'EmbedPlayer.EnableOptionsMenu' ) === false ){ + this.supportedComponents[ 'options'] = false; + } + // Check for volume control + if( mw.config.get( 'EmbedPlayer.EnableVolumeControl') === false ){ + this.supportedComponents[ 'volumeControl'] = false; + } + + // Check if we have multiple playable sources ( if only one source don't display source switch ) + + if( embedPlayer.mediaElement.getPlayableSources().length == 1 ){ + this.supportedComponents[ 'sourceSwitch' ] = false; + + } + + // Give embeds option to explicitly disable components via flag + var source = embedPlayer.mediaElement.getPlayableSources()[0]; + if ( !embedPlayer.disablecontrols && source ) { + embedPlayer.disablecontrols = source.disablecontrols; + } + if ( embedPlayer.disablecontrols ) { + embedPlayer.disablecontrols.split(',').forEach(function( key ) { + mw.log( 'PlayerControlBuilder:: disabled component via flag:' + key ); + _this.supportedComponents[ key ] = false; + }); + } + + $( embedPlayer ).trigger( 'addControlBarComponent', this ); + + var components = []; + var largestPos = 0; + var addComponent = function( componentId ){ + if ( _this.supportedComponents[ componentId ] ) { + if ( _this.availableWidth >= _this.components[ componentId ].w ) { + _this.availableWidth -= _this.components[ componentId ].w; + // Check if position is defined, if not, place at end of known positions + var position = _this.components[ componentId ].position ? + _this.components[ componentId ].position: + largestPos+1 + if( position > largestPos ){ + largestPos = position; + } + components.push({ + 'id': componentId, + 'position': position + }); + //mw.log(" availableWidth:" + _this.availableWidth + ' ' + componentId + ' took: ' + _this.components[ componentId ].w ) + } else { + mw.log( 'PlayerControlBuilder:: Not enough space for control component:' + componentId ); + } + } + }; + + var addComponents = function() { + components.sort(function(a, b) { + return b.position - a.position; + }); + for(var i=0;i<components.length;i++) { + $controlBar.append( + _this.getComponent( components[ i ]['id'] ) + ); + } + } + + // Output components + for ( var componentId in this.components ) { + // Check for (component === false ) and skip + if( this.components[ componentId ] === false ){ + continue; + } + + // Special case with playhead and time ( to make sure they are to the left of everything else ) + if ( componentId == 'playHead' ){ + continue; + } + if( componentId == 'timeDisplay' && !mw.config.get( 'EmbedPlayer.EnableTimeDisplay' ) ){ + continue; + } + + // Skip "fullscreen" button for assets or where height is 0px ( audio ) + if( componentId == 'fullscreen' && this.embedPlayer.isAudio() ){ + continue; + } + // Skip sourceSwitch if width < smalles derivative + if ( componentId == 'sourceSwitch' && this.availableWidth < 320) { + continue; + } + addComponent( componentId ); + } + if( this.availableWidth > 30 ){ + addComponent( 'playHead' ); + } + addComponents(); + $(embedPlayer).trigger( 'controlBarBuildDone' ); + }, + + /** + * Get a window size for the player while preserving aspect ratio: + * + * @@TODO This has similar logic to mw.embedPlayerNative applyIntrinsicAspect we should look + * at merging their functionality. + * + * @param {object} windowSize + * object that set { 'width': {width}, 'height':{height} } of target window + * @return {object} + * css settings for fullscreen player + */ + getAspectPlayerWindowCss: function( windowSize ) { + var embedPlayer = this.embedPlayer; + var _this = this; + // Setup target height width based on max window size + if( !windowSize ){ + var windowSize = { + 'width' : $( window ).width(), + 'height' : $( window ).height() + }; + } + windowSize.width = parseInt( windowSize.width ); + windowSize.height = parseInt( windowSize.height ); + // See if we need to leave space for control bar + if( !_this.isOverlayControls() ){ + //targetHeight = targetHeight - this.height; + windowSize.height = windowSize.height - this.height; + } + + // Set target width + var targetWidth = windowSize.width; + var targetHeight = targetWidth * ( 1 / _this.getIntrinsicAspect() ); + // Check if it exceeds the height constraint: + if( targetHeight > windowSize.height ){ + targetHeight = windowSize.height; + targetWidth = parseInt( targetHeight * _this.getIntrinsicAspect() ); + } + var offsetTop = 0; + // Move the video down 1/2 of the difference of window height + offsetTop+= ( targetHeight < windowSize.height )? ( windowSize.height- targetHeight ) / 2 : 0; + // if the video is very tall in a short window adjust the size: + var offsetLeft = ( targetWidth < windowSize.width )? parseInt( windowSize.width- targetWidth ) / 2 : 0; + + var position = (mw.isIOS4() && mw.isIphone()) ? 'static' : 'absolute'; + mw.log( 'PlayerControlBuilder::getAspectPlayerWindowCss: ' + ' h:' + targetHeight + ' w:' + targetWidth + ' t:' + offsetTop + ' l:' + offsetLeft ); + return { + 'position' : position, + 'height': parseInt( targetHeight ), + 'width' : parseInt( targetWidth ), + 'top' : parseInt( offsetTop ), + 'left': parseInt( offsetLeft) + }; + }, + + /** + * Get the intrinsic aspect ratio of media ( width / height ) + * @return {float} + * size object with width and height + */ + getIntrinsicAspect: function(){ + var vid = this.embedPlayer.getPlayerElement(); + // Check for raw intrinsic media size: + if( vid && vid.videoWidth && vid.videoHeight ){ + return vid.videoWidth / vid.videoHeight; + } + + // See if we have source data attributes available: + if( this.embedPlayer.mediaElement && + this.embedPlayer.mediaElement.selectedSource ) + { + var ss = this.embedPlayer.mediaElement.selectedSource; + // See if we have a hardcoded aspect to the source ( Adaptive streams don't have width / height ) + if( ss.aspect ){ + return ss.aspect; + } + + if( ss.width && ss.height ){ + return ss.width / ss.height + } + } + + // check for posterImage size: ( should have Intrinsic aspect size as well ) + var img = this.embedPlayer.getInterface().find('.playerPoster')[0]; + if( img && img.naturalWidth && img.naturalHeight){ + return img.naturalWidth / img.naturalHeight + } + + // if all else fails use embedPlayer.getWidth() + return this.embedPlayer.getWidth() / this.embedPlayer.getHeight() + }, + + /** + * Get the play button css + */ + getPlayButtonPosition: function() { + var _this = this; + return { + 'position' : 'absolute', + 'left' : '50%', + 'top' : '50%', + 'margin-left' : - .5 * this.getComponentWidth( 'playButtonLarge' ), + 'margin-top' : - .5 * this.getComponentHeight( 'playButtonLarge' ) + }; + }, + + /** + * Check if we're in Fullscreen + * @return {boolean) + */ + isInFullScreen: function() { + return this.inFullScreen; + }, + + /** + * Toggles full screen by calling + * doFullScreenPlayer to enable fullscreen mode + * restoreWindowPlayer to restore window mode + */ + toggleFullscreen: function( forceClose ) { + var _this = this; + // Do normal in-page fullscreen handling: + if( this.isInFullScreen() ){ + this.restoreWindowPlayer(); + }else { + this.doFullScreenPlayer(); + } + // Don't follow the # link: + return false; + }, + + /** + * Do full-screen mode + */ + doFullScreenPlayer: function( callback ) { + mw.log("PlayerControlBuilder:: doFullScreenPlayer" ); + // Setup pointer to control builder : + var _this = this, + profile = $.client.profile(); + + // Store the page vertical scroll + var doc = window.document; + var context = window; + this.verticalScrollPosition = doc.all ? doc.scrollTop : context.pageYOffset; + + // Setup local reference to embed player: + var embedPlayer = this.embedPlayer; + + // Setup a local reference to the player interface: + var $interface = embedPlayer.getInterface(); + // Check fullscreen state ( if already true do nothing ) + if( this.isInFullScreen() == true ){ + return ; + } + this.inFullScreen = true; + + // Add fullscreen class to interface: + $interface.addClass( 'fullscreen' ); + + // if overlaying controls add hide show player binding. + if( _this.isOverlayControls() && !embedPlayer.isTouchDevice() ){ + _this.addFullscreenMouseMoveHideShowControls(); + } + + // Store the current scroll location on the iframe: + $( embedPlayer ).trigger( 'fullScreenStoreVerticalScroll' ); + + if( window.fullScreenApi.supportsFullScreen ) { + _this.preFullscreenPlayerSize = this.getPlayerSize(); + var fullscreenHeight = null; + var fsTarget = this.getFsTarget(); + + var escapeFullscreen = function( event ) { + // grab the correct document target to check for fullscreen + if ( ! window.fullScreenApi.isFullScreen( window.document ) ) { + _this.restoreWindowPlayer(); + } + } + // remove any old binding: + fsTarget.removeEventListener( fullScreenApi.fullScreenEventName, escapeFullscreen ); + // Add a binding to catch "escape" fullscreen + fsTarget.addEventListener( fullScreenApi.fullScreenEventName, escapeFullscreen ); + // Make the iframe fullscreen: + window.fullScreenApi.requestFullScreen( fsTarget ); + + // There is a bug with mozfullscreenchange event in all versions of firefox with supportsFullScreen + // https://bugzilla.mozilla.org/show_bug.cgi?id=724816 + // so we have to have an extra binding to check for size change and then restore. + if( profile.name === 'firefox' ){ + _this.fullscreenRestoreCheck = setInterval( function(){ + if( fullscreenHeight && $(window).height() < fullscreenHeight ){ + // Mozilla triggered size change: + clearInterval ( _this.fullscreenRestoreCheck ); + _this.restoreWindowPlayer(); + } + // set fullscreen height: + if( ! fullscreenHeight && _this.preFullscreenPlayerSize.height != $(window).height() ){ + fullscreenHeight = $(window).height(); + } + }, 250 ); + } + } else { + // Check for hybrid html controls / native fullscreen support: + var vid = this.embedPlayer.getPlayerElement(); + if( mw.config.get('EmbedPlayer.EnableIpadNativeFullscreen') + && + vid && vid.webkitSupportsFullscreen + ){ + this.doHybridNativeFullscreen(); + return ; + } else { + // make the player traget or iframe fullscreen + this.doContextTargetFullscreen(); + } + } + + // Bind escape to restore in page clip ( IE9 needs a secondary escape binding ) + $( window ).keyup( function( event ) { + // Escape check + if( event.keyCode == 27 ){ + _this.restoreWindowPlayer(); + } + } ); + + // trigger the open fullscreen event: + $( embedPlayer ).trigger( 'onOpenFullScreen' ); + + // re draw the controls after a timeout ( to allow the screen dom to update ) + setTimeout( function(){ + _this.addControls(); + },100) + }, + + /** + * Make the target player interface or iframe fullscreen + */ + doContextTargetFullscreen: function() { + var + _this = this, + doc = window.document, + $doc = $( doc ), + $target = $( this.getFsTarget() ), + context = window; + + // update / reset local restore properties + this.parentsAbsoluteList = []; + this.parentsRelativeList = []; + + // Set the original parent page scale if possible: + this.orginalParnetViewPortContent = $doc.find( 'meta[name="viewport"]' ).attr( 'content' ); + this.orginalTargetElementLayout = { + 'style' : $target[0].style.cssText, + 'width' : $target.width(), + 'height' : $target.height() + }; + + mw.log("PlayerControls:: doParentIframeFullscreen> verticalScrollPosition:" + this.verticalScrollPosition); + context.scroll(0, 0); + + // Make sure the parent page page has a zoom of 1: + if( ! $doc.find('meta[name="viewport"]').length ){ + $doc.find('head').append( $( '<meta />' ).attr('name', 'viewport') ); + } + $doc.find('meta[name="viewport"]').attr('content', 'initial-scale=1; maximum-scale=1; minimum-scale=1;' ); + + // iPad 5 supports fixed position in a bad way, use absolute pos for iOS + var playerCssPosition = ( mw.isIOS() ) ? 'absolute': 'fixed'; + + // Remove absolute css of the $target's parents + $target.parents().each( function() { + var $parent = $( this ); + if( $parent.css( 'position' ) == 'absolute' ) { + _this.parentsAbsoluteList.push( $parent ); + $parent.css( 'position', 'static' ); + } + if( $parent.css( 'position' ) == 'relative' ) { + _this.parentsRelativeList.push( $parent ); + $parent.css( 'position', 'static' ); + } + }); + + // Make the $target fullscreen + $target + .css({ + 'z-index': mw.config.get( 'EmbedPlayer.FullScreenZIndex' ), + 'position': playerCssPosition, + 'top' : '0px', + 'left' : '0px', + 'margin': 0 + }) + .data( + 'isFullscreen', true + ); + + var updateTargetSize = function() { + context.scroll(0, 0); + $target.css({ + 'width' : context.innerWidth, + 'height' : context.innerHeight + }); + // update player size if needed: + _this.embedPlayer.applyIntrinsicAspect(); + }; + + updateTargetSize(); + + // Bind orientation change to resize player ( if fullscreen ) + $( context ).bind( 'orientationchange', function(e){ + if( _this.isInFullScreen() ){ + updateTargetSize(); + } + }); + + // prevent scrolling when in fullscreen: ( both iframe and dom target use document ) + document.ontouchmove = function( e ){ + if( _this.isInFullScreen() ){ + e.preventDefault(); + } + }; + }, + /** + * Restore the player interface or iframe to a window player + */ + restoreContextPlayer: function(){ + var + _this = this, + doc = window.document, + $doc = $( doc ), + $target = $( this.getFsTarget() ), + context = window; + + mw.log("PlayerControlsBuilder:: restoreContextPlayer> verticalScrollPosition:" + this.verticalScrollPosition ); + + // Restore document zoom: + if( this.orginalParnetViewPortContent ){ + $doc.find('meta[name="viewport"]').attr('content', this.orginalParnetViewPortContent ); + } else { + // Restore user zoom: ( NOTE, there does not appear to be a way to know the + // initial scale, so we just restore to 1 in the absence of explicit viewport tag ) + // In order to restore zoom, we must set maximum-scale to a valid value + $doc.find('meta[name="viewport"]').attr('content', 'initial-scale=1; maximum-scale=8; minimum-scale=1;' ); + } + if( this.orginalTargetElementLayout ) { + $target[0].style.cssText = this.orginalTargetElementLayout.style; + $target.attr({ + 'width': this.orginalTargetElementLayout.width, + 'height': this.orginalTargetElementLayout.height + }); + // update player size if needed: + _this.embedPlayer.applyIntrinsicAspect(); + } + // Restore any parent absolute pos: + $doc.find( _this.parentsAbsoluteList ).each( function() { + $( this ).css( 'position', 'absolute' ); + } ); + $doc.find( _this.parentsRelativeList ).each( function() { + $( this ).css( 'position', 'relative' ); + } ); + }, + + /** + * Supports hybrid native fullscreen, player html controls, and fullscreen is native + */ + doHybridNativeFullscreen: function(){ + var vid = this.embedPlayer.getPlayerElement(); + var _this = this; + vid.webkitEnterFullscreen(); + // start to pull for exit fullscreen: + this.fsIntervalID = setInterval( function(){ + var currentFS = vid.webkitDisplayingFullscreen; + // Check if we have entered fullscreen but the player + // has exited fullscreen with native controls click + if( _this.isInFullScreen() && !currentFS ){ + // restore non-fullscreen player state + _this.inFullScreen = false; + // Trigger the onCloseFullscreen event: + $( _this.embedPlayer ).trigger( 'onCloseFullScreen' ); + // stop polling for state change. + clearInterval( _this.fsIntervalID ); + } + }, 250 ); + }, + getWindowSize: function(){ + return { + 'width' : $(window).width(), + 'height' : $(window).height() + }; + }, + doDomFullscreen: function(){ + var _this = this; + var embedPlayer = this.embedPlayer; + var $interface = embedPlayer.getInterface(); + // Remove any old mw-fullscreen-overlay + $( '.mw-fullscreen-overlay' ).remove(); + + _this.preFullscreenPlayerSize = this.getPlayerSize(); + + // Add the css fixed fullscreen black overlay as a sibling to the video element + // iOS4 does not respect z-index + $interface.after( + $( '<div />' ) + .addClass( 'mw-fullscreen-overlay' ) + // Set some arbitrary high z-index + .css('z-index', mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) ) + .hide() + .fadeIn("slow") + ); + + // get the original interface to absolute positioned: + if( ! this.windowPositionStyle ){ + this.windowPositionStyle = $interface.css( 'position' ); + } + if( !this.windowZindex ){ + this.windowZindex = $interface.css( 'z-index' ); + } + // Get the base offset: + this.windowOffset = this.getWindowOffset(); + + // Change the z-index of the interface + $interface.css( { + 'position' : 'fixed', + 'z-index' : mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 1, + 'top' : this.windowOffset.top, + 'left' : this.windowOffset.left + } ); + + // If native persistent native player update z-index: + if( embedPlayer.isPersistentNativePlayer() ){ + $( embedPlayer.getPlayerElement() ).css( { + 'z-index': mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 1, + 'position': 'absolute' + }); + } + + // Empty out the parent absolute index + _this.parentsAbsolute = []; + + // Hide the body scroll bar + $('body').css( 'overflow', 'hidden' ); + + var topOffset = '0px'; + var leftOffset = '0px'; + + // Check if we have an offsetParent + if( $interface.offsetParent()[0].tagName + && + $interface.offsetParent()[0].tagName.toLowerCase() != 'body' ) + { + topOffset = -this.windowOffset.top + 'px'; + leftOffset = -this.windowOffset.left + 'px'; + } + + // Overflow hidden in fullscreen: + $interface.css( 'overlow', 'hidden' ); + + // Remove absolute css of the interface parents + $interface.parents().each( function() { + //mw.log(' parent : ' + $( this ).attr('id' ) + ' class: ' + $( this ).attr('class') + ' pos: ' + $( this ).css( 'position' ) ); + if( $( this ).css( 'position' ) == 'absolute' ) { + _this.parentsAbsolute.push( $( this ) ); + $( this ).css( 'position', null ); + mw.log( 'PlayerControlBuilder:: should update position: ' + $( this ).css( 'position' ) ); + } + }); + + // Bind escape to restore in page clip + $( window ).keyup( function( event ) { + // Escape check + if( event.keyCode == 27 ){ + _this.restoreWindowPlayer(); + } + } ); + }, + addFullscreenMouseMoveHideShowControls:function(){ + var _this = this; + // Bind mouse move in interface to hide control bar + _this.mouseMovedFlag = false; + _this.embedPlayer.getInterface().mousemove( function(e){ + _this.mouseMovedFlag = true; + }); + + // Check every 2 seconds reset flag status if controls are overlay + var checkMovedMouse = function(){ + if( _this.isInFullScreen() ){ + if( _this.mouseMovedFlag ){ + _this.mouseMovedFlag = false; + _this.showControlBar(); + // Once we move the mouse keep displayed for 3 seconds + setTimeout(checkMovedMouse, 3000); + } else { + // Check for mouse movement every 250ms + _this.hideControlBar(); + setTimeout(checkMovedMouse, 250 ); + } + return; + } + }; + // always initially show the control bar: + _this.showControlBar(); + // start monitoring for moving mouse + checkMovedMouse(); + }, + getWindowOffset: function(){ + var windowOffset = this.embedPlayer.getInterface().offset(); + windowOffset.top = windowOffset.top - $(document).scrollTop(); + windowOffset.left = windowOffset.left - $(document).scrollLeft(); + this.windowOffset = windowOffset; + return this.windowOffset; + }, + // Display a fullscreen tip if configured to do and the browser supports it. + displayFullscreenTip: function(){ + var _this = this; + // Mobile devices don't have f11 key + if( mw.isMobileDevice() ){ + return ; + } + // Safari does not have a DOM fullscreen ( no subtitles, no controls ) + if ( $.client.profile().name === 'safari' ) { + return; + } + + // OSX has a different short cut than windows and liux + var toolTipMsg = ( navigator.userAgent.indexOf('Mac OS X') != -1 )? + mw.msg( 'mwe-embedplayer-fullscreen-tip-osx') : + mw.msg( 'mwe-embedplayer-fullscreen-tip'); + + var $targetTip = this.addWarningBinding( 'EmbedPlayer.FullscreenTip', + $('<h3/>').html( + toolTipMsg + ) + ); + + // Display the target warning: + $targetTip.show(); + + var hideTip = function(){ + mw.setConfig('EmbedPlayer.FullscreenTip', false ); + $targetTip.fadeOut('fast'); + }; + + // Hide fullscreen tip if: + // We leave fullscreen, + $( this.embedPlayer ).bind( 'onCloseFullScreen', hideTip ); + // After 5 seconds, + setTimeout( hideTip, 5000 ); + // Or if we catch an f11 button press + $( document ).keyup( function( event ){ + if( event.keyCode == 122 ){ + hideTip(); + } + return true; + }); + }, + // TOOD fullscreen iframe vs inpage object abstraction + //( avoid repatiave conditionals in getters ) + getPlayerSize: function(){ + var height = $(window).height() - this.getHeight(); + if( mw.config.get('EmbedPlayer.IsIframeServer' ) ){ + return { + 'height' : height, + 'width' : $(window).width() + } + } else { + return { + 'height' : this.embedPlayer.getInterface().height(), + 'width' : this.embedPlayer.getInterface().width() + } + } + }, + getFsTarget: function(){ + var $interface = this.embedPlayer.getInterface(); + return $interface[0]; + }, + /** + * Restore the window player + */ + restoreWindowPlayer: function() { + var _this = this; + mw.log("PlayerControlBuilder :: restoreWindowPlayer" ); + var embedPlayer = this.embedPlayer; + + // Check if fullscreen mode is already restored: + if( this.isInFullScreen() === false ){ + return ; + } + // Set fullscreen mode to false + this.inFullScreen = false; + + // remove the fullscreen interface + embedPlayer.getInterface().removeClass( 'fullscreen' ); + + // Check for native support for fullscreen and support native fullscreen restore + if ( window.fullScreenApi.supportsFullScreen ) { + var fsTarget = this.getFsTarget(); + window.fullScreenApi.cancelFullScreen( fsTarget ); + } + + // Restore the iFrame context player + this.restoreContextPlayer(); + + // Restore scrolling on iPad + $( document ).unbind( 'touchend.fullscreen' ); + + // Trigger the onCloseFullscreen event: + $( embedPlayer ).trigger( 'onCloseFullScreen' ); + + // Scroll back to the previews position ( do in async call to allow dom fullscreen restore ) + setTimeout( function(){ + window.scroll( 0, _this.verticalScrollPosition ); + }, 100 ); + + // re draw the controls after a timeout ( to allow the screen dom to update ) + setTimeout( function(){ + _this.addControls(); + },100) + }, + restoreDomPlayer: function(){ + var _this = this; + // local ref to embedPlayer: + var embedPlayer = this.embedPlayer; + + var $interface = embedPlayer.$interface; + var interfaceHeight = ( _this.isOverlayControls() ) + ? embedPlayer.getHeight() + : embedPlayer.getHeight() + _this.getHeight(); + + mw.log( 'restoreWindowPlayer:: h:' + interfaceHeight + ' w:' + embedPlayer.getWidth()); + $('.mw-fullscreen-overlay').remove( 'slow' ); + + mw.log( 'restore embedPlayer:: ' + embedPlayer.getWidth() + ' h: ' + embedPlayer.getHeight() ); + + // Restore the player: + embedPlayer.getInterface().css( { + 'width' : _this.preFullscreenPlayerSize.width, + 'height' : _this.preFullscreenPlayerSize.height + }); + var topPos = { + 'position' : _this.windowPositionStyle, + 'z-index' : _this.windowZindex, + 'overlow' : 'visible', + 'top' : '0px', + 'left' : '0px' + }; + // Restore non-absolute layout: + $( [ $interface, $interface.find('.playerPoster'), embedPlayer ] ).css( topPos ); + if( embedPlayer.getPlayerElement() ){ + $( embedPlayer.getPlayerElement() ) + .css( topPos ) + } + // Restore the body scroll bar + $('body').css( 'overflow', 'auto' ); + + // If native player restore z-index: + if( embedPlayer.isPersistentNativePlayer() ){ + $( embedPlayer.getPlayerElement() ).css( { + 'z-index': 'auto' + }); + } + }, + /** + * Get minimal width for interface overlay + */ + getOverlayWidth: function( ) { + return ( this.embedPlayer.getPlayerWidth() < 300 )? 300 : this.embedPlayer.getPlayerWidth(); + }, + + /** + * Get minimal height for interface overlay + */ + getOverlayHeight: function( ) { + return ( this.embedPlayer.getPlayerHeight() < 200 )? 200 : this.embedPlayer.getPlayerHeight(); + }, + + /** + * addControlBindings + * Adds control hooks once controls are in the DOM + */ + addControlBindings: function( ) { + // Set up local pointer to the embedPlayer + var embedPlayer = this.embedPlayer, + _this = this, + $interface = embedPlayer.getInterface(), + profile = $.client.profile(); + + _this.onControlBar = false; + + // Remove any old interface bindings + $( embedPlayer ).unbind( this.bindPostfix ); + + var bindFirstPlay = false; + _this.addRightClickBinding(); + + // add the player click bindings + _this.addPlayerClickBindings(); + + // Bind into play.ctrl namespace ( so we can unbind without affecting other play bindings ) + $( embedPlayer ).bind( 'onplay' + this.bindPostfix, function() { //Only bind once played + // add right click binding again ( in case the player got swaped ) + embedPlayer.controlBuilder.addRightClickBinding(); + }); + + $( embedPlayer ).bind( 'timeupdate' + this.bindPostfix, function(){ + embedPlayer.updatePlayheadStatus() + }); + + // Update buffer information + $( embedPlayer ).bind( 'progress' + this.bindPostfix, function( event, jEvent, id){ + // regain scope + var embedPlayer = $( '#' + id )[0]; + embedPlayer.updateBufferStatus(); + }); + + // Bind to EnableInterfaceComponents + $( embedPlayer ).bind( 'onEnableInterfaceComponents' + this.bindPostfix, function() { + embedPlayer.controlBuilder.controlsDisabled = false; + embedPlayer.controlBuilder.addPlayerClickBindings(); + }); + + // Bind to DisableInterfaceComponents + $( embedPlayer ).bind( 'onDisableInterfaceComponents' + this.bindPostfix, function() { + embedPlayer.controlBuilder.controlsDisabled = true; + embedPlayer.controlBuilder.removePlayerClickBindings(); + }); + + + // TODO select a player on the page + var bindSpaceUp = function(){ + $(window).bind('keyup' + _this.bindPostfix, function(e) { + if( e.keyCode == 32 ) { + if(embedPlayer.paused) { + embedPlayer.play(); + } else { + embedPlayer.pause(); + } + return false; + } + }); + }; + + var bindSpaceDown = function() { + $(window).unbind( 'keyup' + _this.bindPostfix ); + }; + + // Bind to resize event + /* + var triggerUpdate; + $( window ).resize(function() { + // We use setTimeout because of iOS 4.2 issues + clearTimeout(triggerUpdate); + triggerUpdate = setTimeout(function() { + //embedPlayer.triggerHelper('updateLayout'); + }, 100); + }); + */ + + $(window).on("debouncedresize", function() { + embedPlayer.triggerHelper('updateLayout'); + }); + + // Add hide show bindings for control overlay (if overlay is enabled ) + if( ! _this.isOverlayControls() ) { + $interface + .show() + .hover( bindSpaceUp, bindSpaceDown ); + + // include touch start pause binding + $( embedPlayer).bind( 'touchstart' + this.bindPostfix, function() { + embedPlayer._playContorls = true; + mw.log( "PlayerControlBuilder:: touchstart:" + ' isPause:' + embedPlayer.paused); + if( embedPlayer.paused ) { + embedPlayer.play(); + } else { + embedPlayer.pause(); + } + }); + } else { // hide show controls: + // Bind a startTouch to show controls + $( embedPlayer).bind( 'touchstart' + this.bindPostfix, function() { + if ( embedPlayer.getInterface().find( '.control-bar' ).is( ':visible' ) ) { + if( embedPlayer.paused ) { + embedPlayer.play(); + } else { + embedPlayer.pause(); + } + } else { + _this.showControlBar(); + } + clearTimeout( _this.hideControlBarCallback ); + _this.hideControlBarCallback = setTimeout( function() { + _this.hideControlBar(); + }, 60000 ); + // ( Once the user touched the video "don't hide" ) + return true; + } ); + + var hoverIntentConfig = { + 'sensitivity': 100, + 'timeout' : 1000, + 'over' : function(e){ + // Clear timeout on IE9 + if( mw.isIE9() ) { + clearTimeout(_this.hideControlBarCallback); + _this.hideControlBarCallback = false; + } + // Show controls with a set timeout ( avoid fade in fade out on short mouse over ) + _this.showControlBar(); + bindSpaceUp(); + }, + 'out' : function(e){ + _this.hideControlBar(); + bindSpaceDown(); + } + }; + + // Check if we should display the interface: + // special check for IE9 ( does not count hover on non-visiable inerface div + if( mw.isIE9() ){ + $( embedPlayer.getPlayerElement() ).hoverIntent( hoverIntentConfig ); + + // Add hover binding to control bar + embedPlayer.getInterface().find( '.control-bar' ).hover( function(e) { + _this.onControlBar = true; + embedPlayer.getInterface().find( '.control-bar' ).show(); + }, function( e ) { + if (!_this.hideControlBarCallback) { + _this.hideControlBarCallback = setTimeout(function(){ + _this.hideControlBar(); + },1000); + } + _this.onControlBar = false; + }); + + } else { + if ( !mw.isIpad() ) { + $interface.hoverIntent( hoverIntentConfig ); + } + } + + } + + // Add recommend firefox if we have non-native playback: + if ( _this.checkNativeWarning( ) ) { + _this.addWarningBinding( + 'EmbedPlayer.ShowNativeWarning', + mw.msg( 'mwe-embedplayer-for_best_experience', + $('<div>').append( + $('<a />') + .attr({ + 'href': 'http://www.mediawiki.org/wiki/Extension:TimedMediaHandler/Client_download', + 'target' : '_new' + }) + )[0].innerHTML + ) + ); + } + + // Do png fix for ie6 + if ( profile.name === 'msie' && profile.versionNumber <= 6 ) { + $( '#' + embedPlayer.id + ' .play-btn-large' ).pngFix(); + } + + this.doVolumeBinding(); + + // Check if we have any custom skin Bindings to run + if ( this.addSkinControlBindings && typeof( this.addSkinControlBindings ) == 'function' ){ + this.addSkinControlBindings(); + } + + mw.log( 'trigger::addControlBindingsEvent' ); + $( embedPlayer ).trigger( 'addControlBindingsEvent' ); + }, + removePlayerClickBindings: function(){ + $( this.embedPlayer ) + .unbind( "click" + this.bindPostfix ) + .unbind( "dblclick" + this.bindPostfix ); + }, + addPlayerClickBindings: function(){ + + var _this = this; + var embedPlayer = this.embedPlayer; + + // prevent scrolling when in fullscreen: + document.ontouchmove = function( e ){ + if( _this.isInFullScreen() ){ + e.preventDefault(); + } + }; + // Remove old click bindings before adding: + this.removePlayerClickBindings(); + + // Setup "dobuleclick" fullscreen binding to embedPlayer ( if enabled ) + if ( this.supportedComponents['fullscreen'] ){ + $( embedPlayer ).bind( "dblclick" + _this.bindPostfix, function(){ + embedPlayer.fullscreen(); + }); + } + + var dblClickTime = 300; + var lastClickTime = 0; + var didDblClick = false; + + var playerClickCb = function( event ) { + // make sure the event matches: + if( event.currentTarget.id != embedPlayer.id ){ + embedPlayer = $( '#' + event.currentTarget.id )[0]; + } + mw.log( "PlayerControlBuilder:: click:" + embedPlayer.id + ' isPause:' + embedPlayer.paused); + // Don't do anything if touch interface or native controls are shown + if( embedPlayer.useNativePlayerControls() + || + _this.isControlsDisabled() + || + embedPlayer.isTouchDevice() + ) { + return true; + } + var clickTime = new Date().getTime(); + if( clickTime -lastClickTime < dblClickTime ) { + didDblClick = true; + setTimeout( function(){ + didDblClick = false; + }, dblClickTime + 10 ); + } + lastClickTime = clickTime; + setTimeout( function(){ + // check if no click has since the time we called the setTimeout + if( !didDblClick ){ + if( embedPlayer.paused ) { + embedPlayer.play(); + } else { + embedPlayer.pause(); + } + } + }, dblClickTime ); + return true; + }; + // Add click binding: ( $(embedPlayer).click ) has scope issues ) + if ( embedPlayer.attachEvent ) { + embedPlayer.attachEvent("onclick", playerClickCb); + } else{ + // Firefox 3.5 requires third argument to addEventListener + embedPlayer.addEventListener('click', playerClickCb, false ); + } + + }, + addRightClickBinding: function(){ + var embedPlayer = this.embedPlayer; + // check config: + if( mw.config.get( 'EmbedPlayer.EnableRightClick') === false ){ + document.oncontextmenu= function(e){return false;}; + $(embedPlayer).mousedown(function(e){ + if( e.button == 2 ) { + return false; + } + }); + } + }, + /** + * Hide the control bar. + */ + hideControlBar : function(){ + var animateDuration = 'fast'; + var _this = this; + + // Do not hide control bar if overlay menu item is being displayed: + if( _this.displayOptionsMenuFlag || _this.keepControlBarOnScreen ) { + setTimeout( function(){ + _this.hideControlBar(); + }, 200 ); + return ; + } + + // IE9: If the user mouse is on the control bar, don't hide it + if( this.onControlBar === true ) { + return ; + } + + // Hide the control bar + this.embedPlayer.getInterface().find( '.control-bar') + .fadeOut( animateDuration ); + //mw.log('about to trigger hide control bar') + // Allow interface items to update: + $( this.embedPlayer ).trigger('onHideControlBar', [ {'bottom' : 15}, this.embedPlayer.id ] ); + + }, + restoreControlsHover:function(){ + if( this.isOverlayControls() ){ + this.keepControlBarOnScreen = false; + } + }, + /** + * Show the control bar + */ + showControlBar: function( keepOnScreen ){ + var animateDuration = 'fast'; + if(! this.embedPlayer ) + return ; + + if( this.embedPlayer.getPlayerElement && ! this.embedPlayer.isPersistentNativePlayer() ){ + $( this.embedPlayer.getPlayerElement() ).css( 'z-index', '1' ); + } + mw.log( 'PlayerControlBuilder:: ShowControlBar, keep on screen: ' + keepOnScreen ); + + // Show interface controls + this.embedPlayer.getInterface().find( '.control-bar' ) + .fadeIn( animateDuration ); + + if( keepOnScreen ){ + this.keepControlBarOnScreen = true; + } + + // Trigger the screen overlay with layout info: + $( this.embedPlayer ).trigger( 'onShowControlBar', [{ + 'bottom' : this.getHeight() + 15 + }, this.embedPlayer.id ] ); + }, + + /** + * Checks if the browser supports overlays and the controlsOverlay is + * set to true for the player or via config + */ + isOverlayControls: function(){ + //if the player "supports" overlays: + if( ! this.embedPlayer.supports['overlays'] ){ + return false; + } + + // If disabled via the player + if( this.embedPlayer.overlaycontrols === false ){ + return false; + } + + // Don't overlay controls if in audio mode: + if( this.embedPlayer.isAudio() ){ + return false; + } + + + // If the config is false + if( mw.config.get( 'EmbedPlayer.OverlayControls' ) === false){ + return false; + } + + if( this.embedPlayer.controls === false ){ + return false; + } + + // Past all tests OverlayControls is true: + return true; + }, + + /* Check if the controls are disabled */ + + isControlsDisabled: function() { + return this.controlsDisabled; + }, + + /** + * Check if a warning should be issued to non-native playback systems + * + * dependent on mediaElement being setup + */ + checkNativeWarning: function( ) { + if( mw.config.get( 'EmbedPlayer.ShowNativeWarning' ) === false ){ + return false; + } + + // Don't show for imageOverlay player: + if( this.embedPlayer.instanceOf == 'ImageOverlay' ){ + return false; + } + + // If the resolution is too small don't display the warning + if( parseInt( this.embedPlayer.getPlayerHeight() ) < 199 ){ + return false; + } + + // See if we have we have native support + if( this.embedPlayer.instanceOf == 'Native' ){ + return false; + } + + // Not a lot of good options for an iPhone + if( this.embedPlayer.instanceOf == 'VLCApp' ){ + return false; + } + if( this.embedPlayer.instanceOf == 'OgvJs' ){ + return false; + } + + // Chrome's webM support is oky though: + if( /chrome/.test(navigator.userAgent.toLowerCase() ) && + mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'video/webm' ).length ){ + return false; + } + + + // Check for h264 and or flash/flv source and playback support and don't show warning + if( + ( mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'video/h264' ).length + && this.embedPlayer.mediaElement.getSources( 'video/h264' ).length ) + || + ( mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'video/x-flv' ).length + && this.embedPlayer.mediaElement.getSources( 'video/x-flv' ).length ) + || + ( mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'application/vnd.apple.mpegurl' ).length + && this.embedPlayer.mediaElement.getSources( 'application/vnd.apple.mpegurl' ).length ) + || + ( mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( 'audio/mpeg' ).length + && this.embedPlayer.mediaElement.getSources( 'audio/mpeg' ).length ) + ){ + // No firefox link if a h.264 or flash/flv stream is present + return false; + } + + // Should issue the native warning + return true; + }, + + /** + * Does a native warning check binding to the player on mouse over. + * @param {string} preferenceId The preference Id + * @param {object} warningMsg The jQuery object warning message to be displayed. + * + */ + /** + * Display a warning message on the player + * checks a preference Id to enable or disable it. + * @param {string} preferenceId The preference Id + * @param {object} warningMsg The jQuery object warning message to be displayed. + * @param {boolean} if the hide ui should be exposed + * + */ + addWarningBinding: function( preferenceId, warningMsg, hideDisableUi ) { + mw.log( 'mw.PlayerControlBuilder: addWarningBinding: ' + preferenceId + ' wm: ' + warningMsg); + // Set up local pointer to the embedPlayer + var embedPlayer = this.embedPlayer; + var _this = this; + // make sure the player is large enough + if( embedPlayer.getWidth() < 200 ){ + return false; + } + + // Can be uncommented to reset hide prefrence + //$.cookie( preferenceId, '' ); + + // Check if a cookie has been set to hide the warning: + if ( mw.config.get( preferenceId ) === true && $.cookie( preferenceId ) == 'hidewarning' ){ + return ; + } + + var warnId = "warningOverlay_" + embedPlayer.id; + $( '#' + warnId ).remove(); + + // Add the targetWarning: + var $targetWarning = $('<div />') + .attr( { + 'id': warnId + } ) + .addClass( 'ui-corner-all' ) + .css({ + 'position' : 'absolute', + 'background' : '#FFF', + 'color' : '#111', + 'top' : '10px', + 'left' : '10px', + 'right' : '10px', + 'padding' : '4px', + // z-index should be > than play button, as well as greater + // than the dialog box (in pop up video), or link won't work. + 'z-index' : '1502', + }) + .html( warningMsg ); + + embedPlayer.getInterface().append( + $targetWarning + ); + + $targetWarning.append( + $('<br />') + ); + // check if we should show the checkbox + if( !hideDisableUi ){ + + $targetWarning.append( + $( '<input type="checkbox" />' ) + .attr({ + 'id' : 'ffwarn_' + embedPlayer.id, + 'name' : 'ffwarn_' + embedPlayer.id + }) + .click( function() { + mw.log("WarningBindinng:: set " + preferenceId + ' to hidewarning ' ); + // Set up a cookie for 30 days: + $.cookie( preferenceId, 'hidewarning', {expires: 30} ); + // Set the current instance + mw.setConfig( preferenceId, false ); + $( '#warningOverlay_' + embedPlayer.id ).fadeOut( 'slow' ); + // set the local preference to false + _this.addWarningFlag = false; + } ) + ); + $targetWarning.append( + $('<label />') + .text( mw.msg( 'mwe-embedplayer-do_not_warn_again' ) ) + .attr( 'for', 'ffwarn_' + embedPlayer.id ) + ); + } + + return $targetWarning; + }, + + /** + * Binds the volume controls + */ + doVolumeBinding: function( ) { + var embedPlayer = this.embedPlayer; + var _this = this; + embedPlayer.getInterface().find( '.volume_control' ).unbind().buttonHover().click( function() { + mw.log( 'Volume control toggle' ); + embedPlayer.toggleMute(); + } ); + + // Add vertical volume display hover + if ( this.volumeLayout == 'vertical' ) { + // Default volume binding: + var hoverOverDelay = false; + var $targetvol = embedPlayer.getInterface().find( '.vol_container' ).hide(); + embedPlayer.getInterface().find( '.volume_control' ).hover( + function() { + $targetvol.addClass( 'vol_container_top' ); + // Set to "below" if playing and embedType != native + if ( embedPlayer && embedPlayer.isPlaying && embedPlayer.isPlaying() && !embedPlayer.supports['overlays'] ) { + $targetvol.removeClass( 'vol_container_top' ).addClass( 'vol_container_below' ); + } + $targetvol.fadeIn( 'fast' ); + hoverOverDelay = true; + }, + function() { + hoverOverDelay = false; + setTimeout( function() { + if ( !hoverOverDelay ) { + $targetvol.fadeOut( 'fast' ); + } + }, 500 ); + } + ); + } + var userSlide=false; + // Setup volume slider: + var sliderConf = { + range: "min", + value: 80, + min: 0, + max: 100, + slide: function( event, ui ) { + var percent = ui.value / 100; + mw.log('PlayerControlBuilder::slide:update volume:' + percent); + embedPlayer.setVolume( percent ); + userSlide = true; + }, + change: function( event, ui ) { + var percent = ui.value / 100; + if ( percent == 0 ) { + embedPlayer.getInterface().find( '.volume_control span' ).removeClass( 'ui-icon-volume-on' ).addClass( 'ui-icon-volume-off' ); + } else { + embedPlayer.getInterface().find( '.volume_control span' ).removeClass( 'ui-icon-volume-off' ).addClass( 'ui-icon-volume-on' ); + } + mw.log('PlayerControlBuilder::change:update volume:' + percent); + embedPlayer.setVolume( percent, userSlide ); + userSlide = false; + } + }; + + if ( this.volumeLayout == 'vertical' ) { + sliderConf[ 'orientation' ] = "vertical"; + } + + embedPlayer.getInterface().find( '.volume-slider' ).slider( sliderConf ); + }, + + /** + * Get the options menu ul with li menu items + */ + getOptionsMenu: function( ) { + var $optionsMenu = $( '<ul />' ); + for( var menuItemKey in this.optionMenuItems ){ + + // Make sure its supported in the current controlBuilder config: + if( $.inArray( menuItemKey, mw.config.get( 'EmbedPlayer.EnabledOptionsMenuItems' ) ) === -1 ) { + continue; + } + + $optionsMenu.append( + this.optionMenuItems[ menuItemKey ]( this ) + ); + } + return $optionsMenu; + }, + + /** + * Allow the controlBuilder to do interface actions onDone + */ + onClipDone: function(){ + // Related videos could be shown here + }, + + /** + * The ctrl builder updates the interface on seeking + */ + onSeek: function(){ + //mw.log( "controlBuilder:: onSeek" ); + // Update the interface: + this.setStatus( mw.msg( 'mwe-embedplayer-seeking' ) ); + // add a loading spinner: + this.embedPlayer.addPlayerSpinner(); + // hide once playing again: + this.embedPlayer.hideSpinnerOncePlaying(); + }, + + /** + * Updates the player status that displays short text msgs and the play clock + * @param {String} value Status string value to update + */ + setStatus: function( value ) { + // update status: + if( this.embedPlayer.getInterface() ){ + this.embedPlayer.getInterface().find( '.time-disp' ).text( value ); + } + }, + + /** + * Option menu items + * + * @return + * 'li' a li line item with click action for that menu item + */ + optionMenuItems: { + // Share the video menu + 'share': function( ctrlObj ) { + return $.getLineItem( + mw.msg( 'mwe-embedplayer-share' ), + 'mail-closed', + function( ) { + ctrlObj.displayMenuOverlay( + ctrlObj.getShare() + ); + $( ctrlObj.embedPlayer ).trigger( 'showShareEvent' ); + } + ); + }, + + 'aboutPlayerLibrary' : function( ctrlObj ){ + return $.getLineItem( + mw.msg( 'mwe-embedplayer-about-library' ), + 'info', + function( ) { + ctrlObj.displayMenuOverlay( + ctrlObj.aboutPlayerLibrary() + ); + $( ctrlObj.embedPlayer ).trigger( 'aboutPlayerLibrary' ); + } + ); + } + }, + + /** + * Close a menu overlay + */ + closeMenuOverlay: function(){ + var _this = this; + var embedPlayer = this.embedPlayer; + var $overlay = embedPlayer.getInterface().find( '.overlay-win,.ui-widget-overlay,.ui-widget-shadow' ); + + this.displayOptionsMenuFlag = false; + //mw.log(' closeMenuOverlay: ' + this.displayOptionsMenuFlag); + + $overlay.fadeOut( "slow", function() { + $overlay.remove(); + } ); + + // Show the big play button: ( if not in an ad .. TODO clean up ) + if( embedPlayer.isStopped() && + ( + embedPlayer.sequenceProxy && + embedPlayer.sequenceProxy.isInSequence == false + ) + ){ + embedPlayer.getInterface().find( '.play-btn-large' ).fadeIn( 'slow' ); + } + + $(embedPlayer).trigger( 'closeMenuOverlay' ); + + return false; // onclick action return false + }, + + /** + * Generic function to display custom HTML overlay on video. + * + * @param {String} overlayContent content to be displayed + */ + displayMenuOverlay: function( overlayContent, closeCallback, hideCloseButton ) { + var _this = this; + var embedPlayer = this.embedPlayer; + mw.log( 'PlayerControlBuilder:: displayMenuOverlay' ); + // set the overlay display flag to true: + this.displayOptionsMenuFlag = true; + + if ( !this.supportedComponents[ 'overlays' ] ) { + embedPlayer.stop(); + } + + + // Hide the big play button: + embedPlayer.hideLargePlayBtn(); + + // Check if overlay window is already present: + if ( embedPlayer.getInterface().find( '.overlay-win' ).length != 0 ) { + //Update the content + embedPlayer.getInterface().find( '.overlay-content' ).html( + overlayContent + ); + return ; + } + + // Add an overlay + embedPlayer.getInterface().append( + $('<div />') + .addClass( 'ui-widget-overlay' ) + .css( { + 'height' : '100%', + 'width' : '100%', + 'z-index' : 2 + } ) + ); + + var $closeButton = []; + + if ( !hideCloseButton ) { + // Setup the close button + $closeButton = $('<div />') + .addClass( 'ui-state-default ui-corner-all ui-icon_link rButton') + .css({ + 'position': 'absolute', + 'cursor' : 'pointer', + 'top' : '2px', + 'right' : '2px' + }) + .click( function() { + _this.closeMenuOverlay(); + if( closeCallback ){ + closeCallback(); + } + } ) + .append( + $('<span />') + .addClass( 'ui-icon ui-icon-closethick' ) + ); + } + + var controlBarHeight = embedPlayer.getInterface().find( '.control-bar' ).height(); + var overlayWidth = (embedPlayer.getWidth() - 30); + var overlayHeight = (embedPlayer.getHeight() - (controlBarHeight + 30)); + var overlayTop = (( (embedPlayer.getInterface().height() - controlBarHeight) - overlayHeight) / 2); + var overlayLeft = ((embedPlayer.getInterface().width() - overlayWidth) / 2); + + var overlayMenuCss = { + 'height' : overlayHeight + 'px', + 'width' : overlayWidth + 'px', + 'position' : 'absolute', + 'top' : overlayTop + 'px', + 'left': overlayLeft + 'px', + 'margin': '0 10px 10px 0', + 'overflow' : 'auto', + 'padding' : '4px', + 'z-index' : 3 + }; + var $overlayMenu = $('<div />') + .addClass( 'overlay-win ui-state-default ui-widget-header ui-corner-all' ) + .css( overlayMenuCss ) + .append( + $closeButton, + $('<div />') + .addClass( 'overlay-content' ) + .append( overlayContent ) + ); + + + // Append the overlay menu to the player interface + embedPlayer.getInterface().prepend( + $overlayMenu + ) + .find( '.overlay-win' ) + .fadeIn( "slow" ); + + // Trigger menu overlay display + $( embedPlayer ).trigger( 'displayMenuOverlay' ); + + return false; // onclick action return false + }, + + /** + * Close an alert + */ + closeAlert: function( keepOverlay ) { + var embedPlayer = this.embedPlayer; + var $alert = $( '#alertContainer' ); + + mw.log( 'mw.PlayerControlBuilder::closeAlert' ); + if ( !keepOverlay || ( mw.isIpad() && this.inFullScreen ) ) { + embedPlayer.controlBuilder.closeMenuOverlay(); + if ( mw.isIpad() ) { + embedPlayer.disablePlayControls(); + } + } + + $alert.remove(); + + return false; // onclick action return false; + }, + + /** + * Generic function to display custom alert overlay on video. + * + * @param (Object) Object which includes: + * title Alert Title + * body Alert body + * buttonSet[label,callback] Array of buttons + * style CSS object + */ + displayAlert: function( alertObj ) { + var embedPlayer = this.embedPlayer; + var callback; + mw.log( 'PlayerControlBuilder::displayAlert:: ' + alertObj.title ); + // Check if callback is external or internal (Internal by default) + + // Check if overlay window is already present: + if ( embedPlayer.getInterface().find( '.overlay-win' ).length != 0 ) { + return; + } + if( typeof alertObj.callbackFunction == 'string' ) { + if ( alertObj.isExternal ) { + // TODO better support of running external JS functions, instead of window.parent + try{ + callback = window.parent[ alertObj.callbackFunction ]; + } catch ( e ){ + // could not call parent method + } + } else { + callback = window[ alertObj.callbackFunction ]; + } + } else if( typeof alertObj.callbackFunction == 'function' ) { + // Make life easier for internal usage of the listener mapping by supporting + // passing a callback by function ref + callback = alertObj.callbackFunction; + } else { + mw.log( "PlayerControlBuilder :: displayAlert :: Error: bad callback type" ); + callback = function() {}; + } + + var $container = $( '<div />' ).attr( 'id', 'alertContainer' ).addClass( 'alert-container' ); + var $title = $( '<div />' ).text( alertObj.title ).addClass( 'alert-title alert-text' ); + if ( alertObj.props && alertObj.props.titleTextColor ) { + $title.removeClass( 'alert-text' ); + $title.css( 'color', mw.getHexColor( alertObj.props.titleTextColor ) ); + } + var $message = $( '<div />' ).text( alertObj.message ).addClass( 'alert-message alert-text' ); + if ( alertObj.isError ) { + $message.addClass( 'error' ); + } + if ( alertObj.props && alertObj.props.textColor ) { + $message.removeClass( 'alert-text' ); + $message.css( 'color', mw.getHexColor( alertObj.props.textColor ) ); + } + var $buttonsContainer = $( '<div />' ).addClass( 'alert-buttons-container' ); + if ( alertObj.props && alertObj.props.buttonRowSpacing ) { + $buttonsContainer.css( 'margin-top', alertObj.props.buttonRowSpacing ); + } + var $buttonSet = alertObj.buttons || []; + + // If no button was passed display just OK button + var buttonsNum = $buttonSet.length; + if ( buttonsNum == 0 && !alertObj.noButtons ) { + $buttonSet = ["OK"]; + buttonsNum++; + } + + $.each( $buttonSet, function(i) { + var label = this.toString(); + var $currentButton = $( '<button />' ) + .addClass( 'alert-button' ) + .text( label ) + .click( function( eventObject ) { + callback( eventObject ); + embedPlayer.controlBuilder.closeAlert( alertObj.keepOverlay ); + } ); + if ( alertObj.props && alertObj.props.buttonHeight ) { + $currentButton.css( 'height', alertObj.props.buttonHeight ); + } + // Apply buttons spacing only when more than one is present + if (buttonsNum > 1) { + if (i < buttonsNum-1) { + if ( alertObj.props && alertObj.props.buttonSpacing ) { + $currentButton.css( 'margin-right', alertObj.props.buttonSpacing ); + } + } + } + $buttonsContainer.append( $currentButton ); + } ) + $container.append( $title, $message, $buttonsContainer ); + return embedPlayer.controlBuilder.displayMenuOverlay( $container, false, true ); + }, + + aboutPlayerLibrary: function(){ + return $( '<div />' ) + .append( + $( '<h2 />' ) + .text( + mw.msg('mwe-embedplayer-about-library') + ) + , + $( '<span />') + .append( + mw.msg('mwe-embedplayer-about-library-desc', + $('<div>').append( + $('<a />').attr({ + 'href' : mw.config.get( 'EmbedPlayer.LibraryPage' ), + 'target' : '_new' + }) + )[0].innerHTML + ) + ) + ); + }, + /** + * Get the "share" interface + * + * TODO share should be enabled via <embed> tag usage to be compatible + * with sites social networking sites that allow <embed> tags but not js + * + * @param {Object} $target Target jQuery object to set share html + */ + getShare: function( ) { + var embedPlayer = this.embedPlayer; + var embed_code = embedPlayer.getSharingEmbedCode(); + var embed_wiki_code = embedPlayer.getWikiEmbedCode(); + var _this = this; + + var $shareInterface = $('<div />'); + + var $shareList = $( '<ul />' ); + + $shareList + .append( + $('<li />').text( + mw.msg( 'mwe-embedplayer-embed_site_or_blog' ) + ) + /* + .append( + $('<a />') + .attr('href', '#') + .addClass( 'active' ) + .text( + mw.msg( 'mwe-embedplayer-embed_site_or_blog' ) + ) + ) + */ + ); + + $shareInterface.append( + $( '<h2 />' ) + .text( embedPlayer.isAudio() ? + mw.msg( 'mwe-embedplayer-share_this_audio' ) : + mw.msg( 'mwe-embedplayer-share_this_video' ) ) + ); + + if ( embed_wiki_code ) { + $shareInterface.append( + $('<ul />').append( + $('<li />').text( + mw.msg( 'mwe-embedplayer-embed_wiki' ) + ) + ), + $( '<textarea />' ) + .attr( 'rows', 1 ) + .html( embed_wiki_code ) + .click( function() { + $( this ).select(); + }), + $('<br />') + ); + } + + $shareInterface.append( + $shareList + ); + + $shareInterface.append( + + $( '<textarea />' ) + .attr( 'rows', 4 ) + .html( embed_code ) + .click( function() { + $( this ).select(); + }), + + $('<br />'), + $('<br />') + ); + return $shareInterface; + }, + + /** + * Shows the Player Select interface + * + * @param {Object} $target jQuery target for output + */ + getPlayerSelect: function( ) { + mw.log('PlayerControlBuilder::getPlayerSelect: source:' + + this.embedPlayer.mediaElement.selectedSource.getSrc() + + ' player: ' + this.embedPlayer.selectedPlayer.id ); + + var embedPlayer = this.embedPlayer; + + var _this = this; + + var $playerSelect = $('<div />') + .append( + $( '<h2 />' ) + .text( mw.msg( 'mwe-embedplayer-choose_player' ) ) + ); + + $.each( embedPlayer.mediaElement.getPlayableSources(), function( sourceId, source ) { + + var isPlayable = (typeof mw.EmbedTypes.getMediaPlayers().defaultPlayer( source.getMIMEType() ) == 'object' ); + var isSelected = ( source.getSrc() == embedPlayer.mediaElement.selectedSource.getSrc() ); + + $playerSelect.append( + $( '<h3 />' ) + .text( source.getTitle() ) + ); + + if ( isPlayable ) { + var $playerList = $('<ul />'); + // output the player select code: + + var supportingPlayers = mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( source.getMIMEType() ); + + for ( var i = 0; i < supportingPlayers.length ; i++ ) { + // Add link to select the player if not already selected ) + if( embedPlayer.selectedPlayer.id == supportingPlayers[i].id && isSelected ) { + // Active player ( no link ) + var $playerLine = $( '<span />' ) + .append( + $('<a />') + .attr({ + 'href' : '#' + }) + .addClass( 'active') + .text( + supportingPlayers[i].getName() + ).click( function(){ + embedPlayer.controlBuilder.closeMenuOverlay(); + // Don't follow the # link: + return false; + }) + ); + //.addClass( 'ui-state-highlight ui-corner-all' ); removed by ran + } else { + // Non active player add link to select: + $playerLine = $( '<a />') + .attr({ + 'href' : '#', + 'id' : 'sc_' + sourceId + '_' + supportingPlayers[i].id + }) + .addClass( 'ui-corner-all') + .text( supportingPlayers[i].getName() ) + .click( function() { + var iparts = $( this ).attr( 'id' ).replace(/sc_/ , '' ).split( '_' ); + var sourceId = iparts[0]; + var player_id = iparts[1]; + mw.log( 'PlayerControlBuilder:: source id: ' + sourceId + ' player id: ' + player_id ); + + embedPlayer.controlBuilder.closeMenuOverlay(); + + // Close fullscreen if we are in fullscreen mode + if( _this.isInFullScreen() ){ + _this.restoreWindowPlayer(); + } + + embedPlayer.mediaElement.setSourceByIndex( sourceId ); + var playableSources = embedPlayer.mediaElement.getPlayableSources(); + + mw.EmbedTypes.getMediaPlayers().setPlayerPreference( + player_id, + playableSources[ sourceId ].getMIMEType() + ); + + // Issue a stop + embedPlayer.stop(); + + // Don't follow the # link: + return false; + } ) + .hover( + function(){ + $( this ).addClass('active'); + }, + function(){ + $( this ).removeClass('active'); + } + ); + } + + // Add the player line to the player list: + $playerList.append( + $( '<li />' ).append( + $playerLine + ) + ); + } + + // Append the player list: + $playerSelect.append( $playerList ); + + } else { + // No player available: + $playerSelect.append( mw.msg( 'mwe-embedplayer-no-player', source.getTitle() ) ); + } + } ); + + // Return the player select elements + return $playerSelect; + }, + + /** + * Loads sources and calls showDownloadWithSources + * @param {Object} $target jQuery target to output to + */ + showDownload: function( $target ) { + var _this = this; + var embedPlayer = this.embedPlayer; + _this.showDownloadWithSources( $target ); + }, + + /** + * Shows the download interface with sources loaded + * @param {Object} $target jQuery target to output to + */ + showDownloadWithSources : function( $target ) { + var _this = this; + mw.log( 'PlayerControlBuilder:: showDownloadWithSources::' + $target.length ); + var embedPlayer = this.embedPlayer; + // Empty the target: + $target.empty(); + $target.append( $('<div />') ); + $target = $target.find('div'); + + var $mediaList = $( '<ul />' ); + var $textList = $( '<ul />' ); + $.each( embedPlayer.mediaElement.getSources(), function( index, source ) { + if( source.getSrc() ) { + mw.log("showDownloadWithSources:: Add src: " + source.getTitle() ); + var fileName = source.mwtitle; + if ( !fileName ) { + var path = new mw.Uri( source.getSrc() ).path; + var pathParts = path.split( '/' ); + fileName = pathParts[ pathParts.length -1 ]; + } + var $dlLine = $( '<li />').append( + $('<a />') + .attr( { + 'href': source.getSrc(), + 'download': fileName + }) + .text( source.getTitle() ) + ); + // Add link to correct "bucket" + + //Add link to time segment: + if ( source.getSrc().indexOf( '?t=' ) !== -1 ) { + $target.append( $dlLine ); + } else if ( this.getMIMEType().indexOf('text') === 0 ) { + // Add link to text list + $textList.append( $dlLine ); + } else { + // Add link to media list + $mediaList.append( $dlLine ); + } + + } + } ); + if( $mediaList.find('li').length != 0 ) { + $target.append( + $('<h2 />') + .text( embedPlayer.isAudio() ? + mw.msg( 'mwe-embedplayer-download_full_audio' ) : + mw.msg( 'mwe-embedplayer-download_full_video' ) ), + $mediaList + ); + } + + if( $textList.find('li').length != 0 ) { + $target.append( + $('<h2 />') + .html( mw.msg( 'mwe-embedplayer-download_text' ) ), + $textList + ); + } + }, + getSwitchSourceMenu: function(){ + var _this = this; + var embedPlayer = this.embedPlayer; + // for each source with "native playback" + var $sourceMenu = $('<ul />'); + + // Local function to closure the "source" variable scope: + function addToSourceMenu( source ){ + // Check if source is selected: + var icon = ( source.getSrc() == embedPlayer.mediaElement.selectedSource.getSrc() ) ? 'bullet' : 'radio-on'; + $sourceMenu.append( + $.getLineItem( source.getShortTitle() , icon, function(){ + mw.log( 'PlayerControlBuilder::SwitchSourceMenu: ' + source.getSrc() ); + // update menu selecting parent li siblings + $( this ).parent().siblings().find('span.ui-icon').removeClass( 'ui-icon-bullet').addClass( 'ui-icon-radio-on' ); + $( this ).find('span.ui-icon').removeClass( 'ui-icon-radio-on').addClass( 'ui-icon-bullet' ); + // update control bar text + embedPlayer.getInterface().find( '.source-switch' ).text( source.getShortTitle() ); + + + // TODO this logic should be in mw.EmbedPlayer + embedPlayer.mediaElement.setSource( source ); + if( ! _this.embedPlayer.isStopped() ){ + // Get the exact play time from the video element ( instead of parent embed Player ) + var oldMediaTime = _this.embedPlayer.getPlayerElement().currentTime; + var oldPaused = _this.embedPlayer.paused; + // Do a live switch + embedPlayer.playerSwitchSource( source, function( vid ){ + // issue a seek + embedPlayer.setCurrentTime( oldMediaTime, function(){ + // reflect pause state + if( oldPaused ){ + embedPlayer.pause(); + } + } ); + }); + } + }) + ); + } + var addedSources = {}; + $.each( this.embedPlayer.mediaElement.getPlayableSources(), function( sourceIndex, source ) { + // Output the player select code: + var supportingPlayers = mw.EmbedTypes.getMediaPlayers().getMIMETypePlayers( source.getMIMEType() ); + for ( var i = 0; i < supportingPlayers.length ; i++ ) { + var lib = supportingPlayers[i].library; + if( lib === 'Native' || lib === 'OgvJs' ){ // @fixme use supports.sourceSwitch ... if preloaded? + if ( !( source.getSrc() in addedSources ) ) { + addedSources[source.getSrc()] = true; + addToSourceMenu( source ); + } + } + } + }); + return $sourceMenu; + }, + + /** + * Get component + * + * @param {String} componentId Component key to grab html output + */ + getComponent: function( componentId ) { + if ( this.components[ componentId ] ) { + return this.components[ componentId ].o( this ); + } else { + return false; + } + }, + + /** + * Get a component height + * + * @param {String} componentId Component key to grab height + * @return height or false if not set + */ + getComponentHeight: function( componentId ) { + if ( this.components[ componentId ] + && this.components[ componentId ].h ) + { + return this.components[ componentId ].h; + } + return 0; + }, + + /** + * Get a component width + * @param {String} componentId Component key to grab width + * @return width or false if not set + */ + getComponentWidth: function( componentId ){ + if ( this.components[ componentId ] + && this.components[ componentId ].w ) + { + return this.components[ componentId ].w; + } + return 0; + }, + + // Set up the disable playhead function: + // TODO this will move into the disableSeekBar binding in the new theme framework + disableSeekBar : function(){ + var $playHead = this.embedPlayer.getInterface().find( ".play_head" ); + if( $playHead.length ){ + $playHead.slider( "option", "disabled", true ); + } + }, + enableSeekBar : function(){ + var $playHead = this.embedPlayer.getInterface().find( ".play_head" ); + if( $playHead.length ){ + $playHead.slider( "option", "disabled", false); + } + }, + + /** + * Components Object + * Take in the embedPlayer and return some html for the given component. + * + * components can be overwritten by skin javascript + * + * Component JSON structure is as follows: + * 'o' Function to return a binded jQuery object ( accepts the ctrlObject as a parameter ) + * 'w' The width of the component + * 'h' The height of the component ( if height is undefined the height of the control bar is used ) + * 'position' elements are inserted into the dom based on component order and available space. + * if the element is inserted, position is then used to set relative dom insert order. + */ + components: { + /** + * The pause / play button + */ + 'pause': { + 'w': 28, + 'position': 1, + 'o': function( ctrlObj ) { + return $( '<div />' ) + .attr( 'title', mw.msg( 'mwe-embedplayer-play_clip' ) ) + .addClass ( "ui-state-default ui-corner-all ui-icon_link lButton play-btn" ) + .append( + $( '<span />' ) + .addClass( "ui-icon ui-icon-play" ) + ) + // Play / pause binding + .buttonHover() + .click( function() { + ctrlObj.embedPlayer.play(); + // Don't follow the # link: + return false; + }); + } + }, + + /** + * The volume control interface html + */ + 'volumeControl': { + 'w' : 28, + 'position': 7, + 'o' : function( ctrlObj ) { + mw.log( 'PlayerControlBuilder::Set up volume control for: ' + ctrlObj.embedPlayer.id ); + var $volumeOut = $( '<span />' ); + if ( ctrlObj.volumeLayout == 'horizontal' ) { + $volumeOut.append( + $( '<div />' ) + .addClass( "ui-slider ui-slider-horizontal rButton volume-slider" ) + ); + } + + // Add the volume control icon + $volumeOut.append( + $('<div />') + .attr( 'title', mw.msg( 'mwe-embedplayer-volume_control' ) ) + .addClass( "ui-state-default ui-corner-all ui-icon_link rButton volume_control" ) + .append( + $( '<span />' ) + .addClass( "ui-icon ui-icon-volume-on" ) + ) + ); + if ( ctrlObj.volumeLayout == 'vertical' ) { + $volumeOut.find('.volume_control').append( + $( '<div />' ) + .hide() + .addClass( "vol_container ui-corner-all" ) + .append( + $( '<div />' ) + .addClass ( "volume-slider" ) + ) + ); + } + //Return the inner html + return $volumeOut.html(); + } + }, + + /** + * The large play button in center of the player + */ + 'playButtonLarge': { + 'w' : 70, + 'h' : 53, + 'position': 2, + 'o' : function( ctrlObj ) { + return $( '<div />' ) + .attr( { + 'title' : mw.msg( 'mwe-embedplayer-play_clip' ), + 'class' : "play-btn-large" + } ) + // Get dynamic position for big play button + .css( ctrlObj.getPlayButtonPosition() ) + // Add play hook: + .click( function() { + ctrlObj.embedPlayer.play(); + return false; // Event Stop Propagation + } ); + } + }, + + /** + * The Attribution button ( by default this is kaltura-icon + */ + 'attributionButton' : { + 'w' : 28, + 'position': 3, + 'o' : function( ctrlObj ){ + var buttonConfig = mw.config.get( 'EmbedPlayer.AttributionButton'); + // Check for source ( by configuration convention this is a 16x16 image + if( buttonConfig.iconurl ){ + var $icon = $('<img />') + .attr('src', buttonConfig.iconurl ); + } else { + var $icon = $('<span />') + .addClass( 'ui-icon' ); + if( buttonConfig['class'] ){ + $icon.addClass( buttonConfig['class'] ); + } + } + if( typeof buttonConfig.style != 'object'){ + buttonConfig.style = {}; + } + // update the configured size of the attribution button if we have a specific width configured + if( buttonConfig.style.width ){ + this.w = parseInt( buttonConfig.style.width ); + } else { + buttonConfig.style.width = parseInt( this.w ) + 'px'; + } + + return $( '<div />' ) + .addClass( 'rButton' ) + .css({ + 'top' : '1px', + 'left' : '2px' + }) + // Allow button config style to override + .css( buttonConfig.style ) + .append( + $('<a />') + .attr({ + 'href': buttonConfig.href, + 'title' : buttonConfig.title, + 'target' : '_new' + }) + .append( $icon ) + ); + } + }, + + /* + * The time display area + */ + 'timeDisplay': { + 'w' : mw.config.get( 'EmbedPlayer.TimeDisplayWidth' ), + 'position': 6, + 'o' : function( ctrlObj ) { + return $( '<div />' ) + .addClass( "ui-widget time-disp" ) + .append( + ctrlObj.embedPlayer.getTimeRange() + ); + } + }, + + /** + * The options button, invokes display of the options menu + */ + 'options': { + 'w': 28, + 'position': 10, + 'o': function( ctrlObj ) { + return $( '<div />' ) + .attr( 'title', mw.msg( 'mwe-embedplayer-player_options' ) ) + .addClass( 'ui-state-default ui-corner-all ui-icon_link rButton options-btn' ) + .append( + $('<span />') + .addClass( 'ui-icon ui-icon-wrench' ) + ) + .buttonHover() + // Options binding: + .embedMenu( { + 'content' : ctrlObj.getOptionsMenu(), + 'zindex' : mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 2, + 'positionOpts': { + 'directionV' : 'up', + 'offsetY' : 30, + 'directionH' : 'left', + 'offsetX' : -28 + } + } ); + } + }, + + /** + * The fullscreen button for displaying the video fullscreen + */ + 'fullscreen': { + 'w': 24, + 'position': 8, + 'o': function( ctrlObj ) { + var $btn = $( '<div />' ) + .attr( 'title', mw.msg( 'mwe-embedplayer-player_fullscreen' ) ) + .addClass( "ui-state-default ui-corner-all ui-icon_link rButton fullscreen-btn" ) + .append( + $( '<span />' ) + .addClass( "ui-icon ui-icon-arrow-4-diag" ) + ) + // Fullscreen binding: + .buttonHover(); + // Link out to another window if iPad 3x ( broken iframe resize ) + if( ( + mw.config.get('EmbedPlayer.IsIframeServer') + && + mw.isIpad3() + ) + || + mw.config.get( "EmbedPlayer.NewWindowFullscreen" ) + || + ( mw.config.get('EmbedPlayer.IsIframeServer') && mw.config.get('EmbedPlayer.EnableIframeApi') === false ) + ){ + // Get the iframe url: + var url = ctrlObj.embedPlayer.getIframeSourceUrl(); + // Change button into new window ( of the same url as the iframe ) : + return $('<a />').attr({ + 'href': url, + 'target' : '_new' + }) + .click(function(){ + // Update the url: + var url = $(this).attr('href'); + var iframeMwConfig = {}; + + iframeMwConfig['EmbedPlayer.IsFullscreenIframe'] = true; + // add a seek offset: + iframeMwConfig['EmbedPlayer.IframeCurrentTime'] = ctrlObj.embedPlayer.currentTime; + // add play state: + iframeMwConfig['EmbedPlayer.IframeIsPlaying'] = ctrlObj.embedPlayer.isPlaying(); + + // Append the configuration and request domain to the iframe hash: + + // Add the parentUrl to the iframe config: + iframeMwConfig['EmbedPlayer.IframeParentUrl'] = document.URL; + + url += '#' + encodeURIComponent( + JSON.stringify({ + 'mwConfig' :iframeMwConfig, + 'playerId' : playerId + }) + ); + ctrlObj.embedPlayer.pause(); + // try and do a browser popup: + var newwin = window.open( + url, + ctrlObj.embedPlayer.id, + // Fullscreen window params: + 'width=' + screen.width + + ', height=' + ( screen.height - 90 ) + + ', top=0, left=0' + + ', fullscreen=yes' + ); + // if for some reason we could not open the window run the href link: + if( newwin === null){ + return true; + } + if ( window.focus ) { + newwin.focus(); + } + // Else do not follow the href link + return false; + }) + .append($btn); + } else { + return $btn.click( function() { + ctrlObj.embedPlayer.fullscreen(); + } ); + } + } + }, + + 'sourceSwitch' : { + 'w' : 70, + 'position': 9, + 'o' : function( ctrlObj ){ + var $menuContainer = $('<div />').addClass( 'swMenuContainer' ).hide(); + ctrlObj.embedPlayer.getInterface().append( + $menuContainer + ) + // Stream switching widget ( display the current selected stream text ) + return $( '<div />' ) + .addClass('ui-widget source-switch') + .append( + ctrlObj.embedPlayer.mediaElement.selectedSource.getShortTitle() + ).embedMenu( { + 'content' : ctrlObj.getSwitchSourceMenu(), + 'zindex' : mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 2, + 'keepPosition' : true, + 'targetMenuContainer' : $menuContainer, + 'width' : 130, + 'showSpeed': 0, + 'createMenuCallback' : function(){ + var $interface = ctrlObj.embedPlayer.getInterface(); + var $sw = $interface.find( '.source-switch' ); + var $swMenuContainer = $interface.find('.swMenuContainer'); + var height = $swMenuContainer.find( 'li' ).length * 30; + // position from top ( unkown why we can't use bottom here ) + var top = $interface.height() - height - ctrlObj.getHeight() - 6; + $menuContainer.css({ + 'position' : 'absolute', + 'left': $sw[0].offsetLeft, + 'top' : top, + 'bottom': ctrlObj.getHeight(), + 'height' : height + }) + ctrlObj.showControlBar( true ); + }, + 'closeMenuCallback' : function(){ + ctrlObj.restoreControlsHover() + } + } ); + } + }, + + /** + * The playhead component + */ + 'playHead': { + 'w':0, // special case (takes up remaining space) + 'position': 5, + 'o':function( ctrlObj ) { + + var sliderConfig = { + range: "min", + value: 0, + min: 0, + max: 1000, + start: function( event, ui ) { + var id = ( embedPlayer.pc != null ) ? embedPlayer.pc.pp.id:embedPlayer.id; + embedPlayer.userSlide = true; + $( id + ' .play-btn-large' ).fadeOut( 'fast' ); + // If playlist always start at 0 + embedPlayer.startTimeSec = ( embedPlayer.instanceOf == 'mvPlayList' ) ? 0: + mw.npt2seconds( embedPlayer.getTimeRange().split( '/' )[0] ); + }, + slide: function( event, ui ) { + var perc = ui.value / 1000; + embedPlayer.jumpTime = mw.seconds2npt( parseFloat( parseFloat( embedPlayer.getDuration() ) * perc ) + embedPlayer.startTimeSec ); + // mw.log('perc:' + perc + ' * ' + embedPlayer.getDuration() + ' jt:'+ this.jumpTime); + if ( _this.longTimeDisp ) { + ctrlObj.setStatus( mw.msg( 'mwe-embedplayer-seek_to', embedPlayer.jumpTime ) ); + } else { + ctrlObj.setStatus( embedPlayer.jumpTime ); + } + // Update the thumbnail / frame + if ( embedPlayer.isPlaying == false ) { + embedPlayer.updateThumbPerc( perc ); + } + }, + change: function( event, ui ) { + // Only run the onChange event if done by a user slide + // (otherwise it runs times it should not) + if ( embedPlayer.userSlide ) { + embedPlayer.userSlide = false; + embedPlayer.seeking = true; + + var perc = ui.value / 1000; + // set seek time (in case we have to do a url seek) + embedPlayer.seekTimeSec = mw.npt2seconds( embedPlayer.jumpTime, true ); + mw.log( 'PlayerControlBuilder:: seek to: ' + embedPlayer.jumpTime + ' perc:' + perc + ' sts:' + embedPlayer.seekTimeSec ); + ctrlObj.setStatus( mw.msg( 'mwe-embedplayer-seeking' ) ); + if( embedPlayer.isStopped() ){ + embedPlayer.play(); + } + embedPlayer.seek( perc ); + } + } + }; + + var embedPlayer = ctrlObj.embedPlayer; + var _this = this; + var $playHead = $( '<div />' ) + .addClass ( "play_head" ) + .css({ + "position" : 'absolute', + "left" : '33px', + "right" : ( ( embedPlayer.getPlayerWidth() - ctrlObj.availableWidth - 33 ) ) + 'px' + }) + // Playhead binding + .slider( sliderConfig ); + + // Up the z-index of the default status indicator: + $playHead.find( '.ui-slider-handle' ).css( 'z-index', 4 ); + $playHead.find( '.ui-slider-range' ).addClass( 'ui-corner-all' ).css( 'z-index', 2 ); + + // Add buffer html: + $playHead.append( + $('<div />') + .addClass( "ui-slider-range ui-slider-range-min ui-widget-header") + .addClass( "ui-state-highlight ui-corner-all mw_buffer") + ); + + // Show video timeline position on hover and when dragging playhead + function showPosition(event) { + var pos = ( event.clientX - $playHead.offset().left ) / $playHead.width(); + var time = mw.seconds2npt( parseFloat( embedPlayer.getDuration() ) * pos + (embedPlayer.startTimeSec || 0) ); + $playHead.attr('title', time); + } + $playHead.on({ + mouseenter: showPosition, + mouseleave: function(event) { + $playHead.attr({title: ''}); + }, + mousemove: showPosition + }); + + return $playHead; + } + } + } +}; + +} )( mediaWiki, jQuery ); |