summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources')
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4bin0 -> 4575 bytes
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js2760
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerGeneric.js36
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.css18
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerIEWebMPrompt.js46
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerImageOverlay.js307
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerKplayer.js485
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerNative.js1088
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerOgvJs.js222
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVLCApp.js101
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayerVlc.js358
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedTypes.js360
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaElement.js491
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayer.js85
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaPlayers.js195
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.MediaSource.js490
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.processEmbedPlayers.js353
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/EmbedPlayer.css166
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/PlayerSkinKskin.css484
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gifbin0 -> 3368 bytes
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.pngbin0 -> 302 bytes
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.pngbin0 -> 13520 bytes
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/mw.PlayerSkinKskin.js394
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/PlayerSkinMvpcf.css194
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.pngbin0 -> 2935 bytes
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/mw.PlayerSkinMvpcf.js7
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mw.PlayerControlBuilder.js2721
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
new file mode 100644
index 00000000..cf86d1a8
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/blackvideo.mp4
Binary files differ
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js
new file mode 100644
index 00000000..131302a2
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/mw.EmbedPlayer.js
@@ -0,0 +1,2760 @@
+/**
+* embedPlayer is the base class for html5 video tag javascript abstraction library
+* embedPlayer include a few subclasses:
+*
+* mediaPlayer Media player embed system ie: java, vlc or native.
+* mediaElement Represents source media elements
+* mw.PlayerControlBuilder Handles skinning of the player controls
+*/
+( function( mw, $ ) {"use strict";
+ /**
+ * Merge in the default video attributes supported by embedPlayer:
+ */
+ mw.mergeConfig('EmbedPlayer.Attributes', {
+ /*
+ * Base html element attributes:
+ */
+
+ // id: Auto-populated if unset
+ "id" : null,
+
+ // Width: alternate to "style" to set player width
+ "width" : null,
+
+ // Height: alternative to "style" to set player height
+ "height" : null,
+
+ /*
+ * Base html5 video element attributes / states also see:
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html
+ */
+
+ // Media src URI, can be relative or absolute URI
+ "src" : null,
+
+ // Poster attribute for displaying a place holder image before loading
+ // or playing the video
+ "poster" : null,
+
+ // Autoplay if the media should start playing
+ "autoplay" : false,
+
+ // Loop attribute if the media should repeat on complete
+ "loop" : false,
+
+ // If the player controls should be displayed
+ "controls" : true,
+
+ // Video starts "paused"
+ "paused" : true,
+
+ // ReadyState an attribute informs clients of video loading state:
+ // see: http://www.whatwg.org/specs/web-apps/current-work/#readystate
+ "readyState" : 0,
+
+ // Loading state of the video element
+ "networkState" : 0,
+
+ // Current playback position
+ "currentTime" : 0,
+
+ // Previous player set time
+ // Lets javascript use $('#videoId')[0].currentTime = newTime;
+ "previousTime" : 0,
+
+ // Previous player set volume
+ // Lets javascript use $('#videoId')[0].volume = newVolume;
+ "previousVolume" : 1,
+
+ // Initial player volume:
+ "volume" : 0.75,
+
+ // Caches the volume before a mute toggle
+ "preMuteVolume" : 0.75,
+
+ // Media duration: Value is populated via
+ // custom data-durationhint attribute or via the media file once its played
+ "duration" : null,
+
+ // A hint to the duration of the media file so that duration
+ // can be displayed in the player without loading the media file
+ 'data-durationhint': null,
+
+ // to disable menu or timedText for a given embed
+ 'data-disablecontrols': null,
+
+ // Also support direct durationHint attribute ( backwards compatibly )
+ // @deprecated please use data-durationhint instead.
+ 'durationHint' : null,
+
+ // Mute state
+ "muted" : false,
+
+ /**
+ * Custom attributes for embedPlayer player: (not part of the html5
+ * video spec)
+ */
+
+ // Default video aspect ratio
+ 'videoAspect' : '4:3',
+
+ // Start time of the clip
+ "start" : 0,
+
+ // End time of the clip
+ "end" : null,
+
+ // If the player controls should be overlaid
+ // ( Global default via config EmbedPlayer.OverlayControls in module
+ // loader.js)
+ "overlaycontrols" : true,
+
+ // Attribute to use 'native' controls
+ "usenativecontrols" : false,
+
+ // If the player should include an attribution button:
+ 'attributionbutton' : true,
+
+ // A player error object (Includes title and message)
+ // * Used to display an error instead of a play button
+ // * The full player api available
+ 'playerError' : {},
+
+ // A flag to hide the player gui and disable autoplay
+ // * Used for empty players or a player where you want to dynamically set sources, then play.
+ // * The player API remains active.
+ 'data-blockPlayerDisplay': null,
+
+ // If serving an ogg_chop segment use this to offset the presentation time
+ // ( for some plugins that use ogg page time rather than presentation time )
+ "startOffset" : 0,
+
+ // If the download link should be shown
+ "downloadLink" : true,
+
+ // Content type of the media
+ "type" : null
+
+ } );
+
+
+ /**
+ * The base source attribute checks also see:
+ * http://dev.w3.org/html5/spec/Overview.html#the-source-element
+ */
+ mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [
+ // source id
+ 'id',
+
+ // media url
+ 'src',
+
+ // Title string for the source asset
+ 'title',
+
+ // The html5 spec uses label instead of 'title' for naming sources
+ 'label',
+
+ // boolean if we support temporal url requests on the source media
+ 'URLTimeEncoding',
+
+ // Media has a startOffset ( used for plugins that
+ // display ogg page time rather than presentation time
+ 'startOffset',
+
+ // Media start time
+ 'start',
+
+ // Media end time
+ 'end',
+
+ // If the source is the default source
+ 'default',
+
+ // Title of the source
+ 'title',
+
+ // titleKey ( used for api lookups TODO move into mediaWiki specific support
+ 'titleKey'
+ ] );
+
+
+ /**
+ * Base embedPlayer object
+ *
+ * @param {Element}
+ * element, the element used for initialization.
+ * @constructor
+ */
+ mw.EmbedPlayer = function( element ) {
+ return this.init( element );
+ };
+
+ mw.EmbedPlayer.prototype = {
+
+ // The mediaElement object containing all mediaSource objects
+ 'mediaElement' : null,
+
+ // Object that describes the supported feature set of the underling plugin /
+ // Support list is described in PlayerControlBuilder components
+ 'supports': { },
+
+ // If the player is done loading ( does not guarantee playability )
+ // for example if there is an error playerReadyFlag is still set to true once
+ // no more loading is to be done
+ 'playerReadyFlag' : false,
+
+ // Stores the loading errors
+ 'loadError' : false,
+
+ // Thumbnail updating flag ( to avoid rewriting an thumbnail thats already
+ // being updated)
+ 'thumbnailUpdatingFlag' : false,
+
+ // Stopped state flag
+ 'stopped' : true,
+
+ // Local variable to hold CMML meeta data about the current clip
+ // for more on CMML see: http://wiki.xiph.org/CMML
+ 'cmmlData': null,
+
+ // Stores the seek time request, Updated by the seek function
+ 'serverSeekTime' : 0,
+
+ // If the embedPlayer is current 'seeking'
+ 'seeking' : false,
+
+ // Percent of the clip buffered:
+ 'bufferedPercent' : 0,
+
+ // Holds the timer interval function
+ 'monitorTimerId' : null,
+
+ // Buffer flags
+ 'bufferStartFlag' : false,
+ 'bufferEndFlag' : false,
+
+ // For supporting media fragments stores the play end time
+ 'pauseTime' : null,
+
+ // On done playing
+ 'donePlayingCount' : 0
+ ,
+ // if player events should be Propagated
+ '_propagateEvents': true,
+
+ // If the onDone interface should be displayed
+ 'onDoneInterfaceFlag': true,
+
+ // if we should check for a loading spinner in the monitor function:
+ '_checkHideSpinner' : false,
+
+ // If pause play controls click controls should be active:
+ '_playContorls' : true,
+
+ // If player should be displayed (in some caused like audio, we don't need the player to be visible
+ 'displayPlayer': true,
+
+ // Widget loaded should only fire once
+ 'widgetLoaded': false,
+
+ /**
+ * embedPlayer
+ *
+ * @constructor
+ *
+ * @param {Element}
+ * element DOM element that we are building the player interface for.
+ */
+ init: function( element ) {
+ var _this = this;
+ mw.log('EmbedPlayer: initEmbedPlayer: ' + $(element).width() );
+
+ var playerAttributes = mw.config.get( 'EmbedPlayer.Attributes' );
+
+ // Store the rewrite element tag type
+ this.rewriteElementTagName = element.tagName.toLowerCase();
+
+ this.noPlayerFallbackHTML = $( element ).html();
+
+ // Setup the player Interface from supported attributes:
+ for ( var attr in playerAttributes ) {
+ // We can't use $(element).attr( attr ) because we have to check for boolean attributes:
+ if ( element.getAttribute( attr ) != null ) {
+ // boolean attributes
+ if( element.getAttribute( attr ) == '' ){
+ this[ attr ] = true;
+ } else {
+ this[ attr ] = element.getAttribute( attr );
+ }
+ } else {
+ this[attr] = playerAttributes[attr];
+ }
+ // string -> boolean
+ if( this[ attr ] == "false" ) this[attr] = false;
+ if( this[ attr ] == "true" ) this[attr] = true;
+ }
+
+ // Hide "controls" if using native player controls:
+ if( this.useNativePlayerControls() ){
+ _this.controls = true;
+ }
+ // Set the skin name from the class
+ var sn = $(element).attr( 'class' );
+
+ if ( sn && sn != '' ) {
+ var skinList = mw.config.get('EmbedPlayer.SkinList');
+ for ( var n = 0; n < skinList.length; n++ ) {
+ if ( sn.indexOf( skinList[n].toLowerCase() ) !== -1 ) {
+ this.skinName = skinList[ n ];
+ }
+ }
+ }
+ // Set the default skin if unset:
+ if ( !this.skinName ) {
+ this.skinName = mw.config.get( 'EmbedPlayer.DefaultSkin' );
+ }
+
+ // Support custom monitorRate Attribute ( if not use default )
+ if( !this.monitorRate ){
+ this.monitorRate = mw.config.get( 'EmbedPlayer.MonitorRate' );
+ }
+
+ // Make sure startOffset is cast as an float:
+ if ( this.startOffset && this.startOffset.split( ':' ).length >= 2 ) {
+ this.startOffset = parseFloat( mw.npt2seconds( this.startOffset ) );
+ }
+
+ // Make sure offset is in float:
+ this.startOffset = parseFloat( this.startOffset );
+
+ // Set the source duration
+ if ( $( element ).attr( 'duration' ) ) {
+ _this.duration = $( element ).attr( 'duration' );
+ }
+ // Add durationHint property form data-durationhint:
+ if( _this['data-durationhint']){
+ _this.durationHint = _this['data-durationhint'];
+ }
+ // Update duration from provided durationHint
+ if ( _this.durationHint && ! _this.duration){
+ _this.duration = mw.npt2seconds( _this.durationHint );
+ }
+
+ // Make sure duration is a float:
+ this.duration = parseFloat( this.duration );
+ mw.log( 'EmbedPlayer::init:' + this.id + " duration is: " + this.duration );
+
+ // Add disablecontrols property form data-disablecontrols:
+ if( _this['data-disablecontrols'] ){
+ _this.disablecontrols = _this['data-disablecontrols'];
+ }
+
+ // Set the playerElementId id
+ this.pid = 'pid_' + this.id;
+
+ // Add the mediaElement object with the elements sources:
+ this.mediaElement = new mw.MediaElement( element );
+
+ this.bindHelper( 'updateLayout', function() {
+ _this.updateLayout();
+ });
+ },
+ /**
+ * Bind helpers to help iOS retain bind context
+ *
+ * Yes, iOS will fail when you run $( embedPlayer ).bind()
+ * but "work" when you run embedPlayer.bind() if the script urls are from diffrent "resources"
+ */
+ bindHelper: function( name, callback ){
+ $( this ).bind( name, callback );
+ return this;
+ },
+ unbindHelper: function( bindName ){
+ if( bindName ) {
+ $( this ).unbind( bindName );
+ }
+ return this;
+ },
+ triggerQueueCallback: function( name, callback ){
+ $( this ).triggerQueueCallback( name, callback );
+ },
+ triggerHelper: function( name, obj ){
+ try{
+ $( this ).trigger( name, obj );
+ } catch( e ){
+ // ignore try catch calls
+ // mw.log( "EmbedPlayer:: possible error in trgger: " + name + " " + e.toString() );
+ }
+ },
+ /**
+ * Stop events from Propagation and blocks interface updates and trigger events.
+ * @return
+ */
+ stopEventPropagation: function(){
+ mw.log("EmbedPlayer:: stopEventPropagation");
+ this.stopMonitor();
+ this._propagateEvents = false;
+ },
+
+ /**
+ * Restores event propagation
+ * @return
+ */
+ restoreEventPropagation: function(){
+ mw.log("EmbedPlayer:: restoreEventPropagation");
+ this._propagateEvents = true;
+ this.startMonitor();
+ },
+
+ /**
+ * Enables the play controls ( for example when an ad is done )
+ */
+ enablePlayControls: function(){
+ mw.log("EmbedPlayer:: enablePlayControls" );
+ if( this.useNativePlayerControls() ){
+ return ;
+ }
+ this._playContorls = true;
+ // re-enable hover:
+ this.getInterface().find( '.play-btn' )
+ .buttonHover()
+ .css('cursor', 'pointer' );
+
+ this.controlBuilder.enableSeekBar();
+ /*
+ * We should pass an array with enabled components, and the controlBuilder will listen
+ * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer.
+ * [ 'playButton', 'seekBar' ]
+ */
+ $( this ).trigger( 'onEnableInterfaceComponents');
+ },
+
+ /**
+ * Disables play controls, for example when an ad is playing back
+ */
+ disablePlayControls: function(){
+ if( this.useNativePlayerControls() ){
+ return ;
+ }
+ this._playContorls = false;
+ // turn off hover:
+ this.getInterface().find( '.play-btn' )
+ .unbind('mouseenter mouseleave')
+ .css('cursor', 'default' );
+
+ this.controlBuilder.disableSeekBar();
+ /**
+ * We should pass an array with disabled components, and the controlBuilder will listen
+ * to this event and handle the layout changes. we should not call to this.controlBuilder inside embedPlayer.
+ * [ 'playButton', 'seekBar' ]
+ */
+ $( this ).trigger( 'onDisableInterfaceComponents');
+ },
+
+ /**
+ * For plugin-players to update supported features
+ */
+ updateFeatureSupport: function(){
+ $( this ).trigger('updateFeatureSupportEvent', this.supports );
+ return ;
+ },
+ /**
+ * Apply Intrinsic Aspect ratio of a given image to a poster image layout
+ */
+ applyIntrinsicAspect: function(){
+ var $this = $( this );
+ // Check if a image thumbnail is present:
+ if( this.getInterface().find('.playerPoster').length ){
+ var img = this.getInterface().find('.playerPoster')[0];
+ var pHeight = $this.height();
+ // Check for intrinsic width and maintain aspect ratio
+ if( img.naturalWidth && img.naturalHeight ){
+ var pWidth = parseInt( img.naturalWidth / img.naturalHeight * pHeight);
+ if( pWidth > $this.width() ){
+ pWidth = $this.width();
+ pHeight = parseInt( img.naturalHeight / img.naturalWidth * pWidth );
+ }
+ $( img ).css({
+ 'height' : pHeight + 'px',
+ 'width': pWidth + 'px',
+ 'left': ( ( $this.width() - pWidth ) * .5 ) + 'px',
+ 'top': ( ( $this.height() - pHeight ) * .5 ) + 'px',
+ 'position' : 'absolute'
+ });
+ }
+ }
+ },
+ /**
+ * Set the width & height from css style attribute, element attribute, or by
+ * default value if no css or attribute is provided set a callback to
+ * resize.
+ *
+ * Updates this.width & this.height
+ *
+ * @param {Element}
+ * element Source element to grab size from
+ */
+ loadPlayerSize: function( element ) {
+ // check for direct element attribute:
+ this.height = element.height > 0 ? element.height + '' : $(element).css( 'height' );
+ this.width = element.width > 0 ? element.width + '' : $(element).css( 'width' );
+
+ // Special check for chrome 100% with re-mapping to 32px
+ // Video embed at 32x32 will have to wait for intrinsic video size later on
+ if( this.height == '32px' || this.height =='32px' ){
+ this.width = '100%';
+ this.height = '100%';
+ }
+ mw.log('EmbedPlayer::loadPlayerSize: css size:' + this.width + ' h: ' + this.height);
+
+ // Set to parent size ( resize events will cause player size updates)
+ if( this.height.indexOf('100%') != -1 || this.width.indexOf('100%') != -1 ){
+ var $relativeParent = $(element).parents().filter(function() {
+ // reduce to only relative position or "body" elements
+ return $( this ).is('body') || $( this ).css('position') == 'relative';
+ }).slice(0,1); // grab only the "first"
+ this.width = $relativeParent.width();
+ this.height = $relativeParent.height();
+ }
+ // Make sure height and width are a number
+ this.height = parseInt( this.height );
+ this.width = parseInt( this.width );
+
+ // Set via attribute if CSS is zero or NaN and we have an attribute value:
+ this.height = ( this.height==0 || isNaN( this.height )
+ && $(element).attr( 'height' ) ) ?
+ parseInt( $(element).attr( 'height' ) ): this.height;
+ this.width = ( this.width == 0 || isNaN( this.width )
+ && $(element).attr( 'width' ) )?
+ parseInt( $(element).attr( 'width' ) ): this.width;
+
+
+ // Special case for audio
+
+ // Firefox sets audio height to "0px" while webkit uses 32px .. force zero:
+ if( this.isAudio() && this.height == '32' ) {
+ this.height = 20;
+ }
+
+ // Use default aspect ration to get height or width ( if rewriting a non-audio player )
+ if( this.isAudio() && this.videoAspect ) {
+ var aspect = this.videoAspect.split( ':' );
+ if( this.height && !this.width ) {
+ this.width = parseInt( this.height * ( aspect[0] / aspect[1] ) );
+ }
+ if( this.width && !this.height ) {
+ var apectRatio = ( aspect[1] / aspect[0] );
+ this.height = parseInt( this.width * ( aspect[1] / aspect[0] ) );
+ }
+ }
+
+ // On load sometimes attr is temporally -1 as we don't have video metadata yet.
+ // or in IE we get NaN for width height
+ //
+ // NOTE: browsers that do support height width should set "waitForMeta" flag in addElement
+ if( ( isNaN( this.height )|| isNaN( this.width ) ) ||
+ ( this.height == -1 || this.width == -1 ) ||
+ // Check for firefox defaults
+ // Note: ideally firefox would not do random guesses at css
+ // values
+ ( (this.height == 150 || this.height == 64 ) && this.width == 300 )
+ ) {
+ var defaultSize = mw.config.get( 'EmbedPlayer.DefaultSize' ).split( 'x' );
+ if( isNaN( this.width ) ){
+ this.width = defaultSize[0];
+ }
+
+ // Special height default for audio tag ( if not set )
+ if( this.isAudio() ) {
+ this.height = 20;
+ }else{
+ this.height = defaultSize[1];
+ }
+ }
+ },
+
+ /**
+ * Get the player pixel width not including controls
+ *
+ * @return {Number} pixel height of the video
+ */
+ getPlayerWidth: function() {
+ var profile = $.client.profile();
+
+ if ( profile.name === 'firefox' && profile.versionNumber < 2 ) {
+ return ( $( this ).parent().parent().width() );
+ }
+ return $( this ).width();
+ },
+
+ /**
+ * Get the player pixel height not including controls
+ *
+ * @return {Number} pixel height of the video
+ */
+ getPlayerHeight: function() {
+ return $( this ).height();
+ },
+
+ /**
+ * Check player for sources. If we need to get media sources form an
+ * external file that request is issued here
+ */
+ checkPlayerSources: function() {
+ mw.log( 'EmbedPlayer::checkPlayerSources: ' + this.id );
+ var _this = this;
+ // Allow plugins to listen to a preCheckPlayerSources ( for registering the source loading point )
+ $( _this ).trigger( 'preCheckPlayerSources' );
+
+ // Allow plugins to block on sources lookup ( cases where we just have an api key for example )
+ $( _this ).triggerQueueCallback( 'checkPlayerSourcesEvent', function(){
+ _this.setupSourcePlayer();
+ });
+ },
+
+ /**
+ * Get text tracks from the mediaElement
+ */
+ getTextTracks: function(){
+ if( !this.mediaElement ){
+ return [];
+ }
+ return this.mediaElement.getTextTracks();
+ },
+ /**
+ * Empty the player sources
+ */
+ emptySources: function(){
+ if( this.mediaElement ){
+ this.mediaElement.sources = [];
+ this.mediaElement.selectedSource = null;
+ }
+ // setup pointer to old source:
+ this.prevPlayer = this.selectedPlayer;
+ // don't null out the selected player on empty sources
+ //this.selectedPlayer =null;
+ },
+
+ /**
+ * Switch and play a video source
+ *
+ * Checks if the target source is the same playback mode and does player switch if needed.
+ * and calls playerSwitchSource
+ */
+ switchPlaySource: function( source, switchCallback, doneCallback ){
+ var _this = this;
+ var targetPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( source.mimeType ) ;
+ if( targetPlayer.library != this.selectedPlayer.library ){
+ this.selectedPlayer = targetPlayer;
+ this.updatePlaybackInterface( function(){
+ _this.playerSwitchSource( source, switchCallback, doneCallback );
+ });
+ } else {
+ // Call the player switch directly:
+ _this.playerSwitchSource( source, switchCallback, doneCallback );
+ }
+ },
+ /**
+ * abstract function player interface must support actual source switch
+ */
+ playerSwitchSource: function( source, switchCallback, doneCallback ){
+ mw.log( "Error player interface must support actual source switch");
+ },
+
+ /**
+ * Set up the select source player
+ *
+ * issues autoSelectSource call
+ *
+ * Sets load error if no source is playable
+ */
+ setupSourcePlayer: function() {
+ var _this = this;
+ mw.log("EmbedPlayer::setupSourcePlayer: " + this.id + ' sources: ' + this.mediaElement.sources.length );
+
+ // Check for source replace configuration:
+ if( mw.config.get('EmbedPlayer.ReplaceSources' ) ){
+ this.emptySources();
+ $.each( mw.config.get('EmbedPlayer.ReplaceSources' ), function( inx, source ){
+ _this.mediaElement.tryAddSource( source );
+ });
+ }
+
+ // Autoseletct the media source
+ this.mediaElement.autoSelectSource();
+
+ // Auto select player based on default order
+ if( this.mediaElement.selectedSource ){
+ this.selectedPlayer = mw.EmbedTypes.getMediaPlayers().defaultPlayer( this.mediaElement.selectedSource.mimeType );
+ // Check if we need to switch player rendering libraries:
+ if ( this.selectedPlayer && ( !this.prevPlayer || this.prevPlayer.library != this.selectedPlayer.library ) ) {
+ // Inherit the playback system of the selected player:
+ this.updatePlaybackInterface();
+ return ;
+ }
+ }
+
+ // Check if no player is selected
+ if( !this.selectedPlayer || !this.mediaElement.selectedSource ){
+ this.showPlayerError();
+ mw.log( "EmbedPlayer:: setupSourcePlayer > player ready ( but with errors ) ");
+ } else {
+ // Trigger layout ready event
+ $( this ).trigger( 'layoutReady' );
+ // Show the interface:
+ this.getInterface().find( '.control-bar').show();
+ this.addLargePlayBtn();
+ }
+ // We still do the playerReady sequence on errors to provide an api
+ // and player error events
+ this.playerReadyFlag = true;
+ // trigger the player ready event;
+ $( this ).trigger( 'playerReady' );
+ this.triggerWidgetLoaded();
+ },
+
+ /**
+ * Updates the player interface
+ *
+ * Loads and inherit methods from the selected player interface.
+ *
+ * @param {Function}
+ * callback Function to be called once playback-system has been
+ * inherited
+ */
+ updatePlaybackInterface: function( callback ) {
+ var _this = this;
+ mw.log( "EmbedPlayer::updatePlaybackInterface: duration is: " + this.getDuration() + ' playerId: ' + this.id );
+ // Clear out any non-base embedObj methods:
+ if ( this.instanceOf ) {
+ // Update the prev instance var used for swiching interfaces to know the previous instance.
+ $( this ).data( 'previousInstanceOf', this.instanceOf );
+ var tmpObj = window['mw.EmbedPlayer' + this.instanceOf ];
+ for ( var i in tmpObj ) {
+ // Restore parent into local location
+ if ( typeof this[ 'parent_' + i ] != 'undefined' ) {
+ this[i] = this[ 'parent_' + i];
+ } else {
+ this[i] = null;
+ }
+ }
+ }
+ // Set up the new embedObj
+ mw.log( 'EmbedPlayer::updatePlaybackInterface: embedding with ' + this.selectedPlayer.library );
+ this.selectedPlayer.load( function() {
+ _this.updateLoadedPlayerInterface( callback );
+ });
+ },
+ /**
+ * Update a loaded player interface by setting local methods to the
+ * updated player prototype methods
+ *
+ * @parma {function}
+ * callback function called once player has been loaded
+ */
+ updateLoadedPlayerInterface: function( callback ){
+ var _this = this;
+ mw.log( 'EmbedPlayer::updateLoadedPlayerInterface ' + _this.selectedPlayer.library + " player loaded for " + _this.id );
+
+ // Get embed library player Interface
+ var playerInterface = mw[ 'EmbedPlayer' + _this.selectedPlayer.library ];
+
+ // Build the player interface ( if the interface includes an init )
+ if( playerInterface.init ){
+ playerInterface.init();
+ }
+
+ for ( var method in playerInterface ) {
+ if ( typeof _this[method] != 'undefined' && !_this['parent_' + method] ) {
+ _this['parent_' + method] = _this[method];
+ }
+ _this[ method ] = playerInterface[ method ];
+ }
+ // Update feature support
+ _this.updateFeatureSupport();
+ // Update duration
+ _this.getDuration();
+ // show player inline
+ _this.showPlayer();
+ // Run the callback if provided
+ if ( callback && $.isFunction( callback ) ){
+ callback();
+ }
+ },
+
+ /**
+ * Select a player playback system
+ *
+ * @param {Object}
+ * player Player playback system to be selected player playback
+ * system include vlc, native, java etc.
+ */
+ selectPlayer: function( player ) {
+ mw.log("EmbedPlayer:: selectPlayer " + player.id );
+ var _this = this;
+ if ( this.selectedPlayer.id != player.id ) {
+ this.selectedPlayer = player;
+ this.updatePlaybackInterface( function(){
+ // Hide / remove track container
+ _this.getInterface().find( '.track' ).remove();
+ // We have to re-bind hoverIntent ( has to happen in this scope )
+ if( !_this.useNativePlayerControls() && _this.controls && _this.controlBuilder.isOverlayControls() ){
+ _this.controlBuilder.showControlBar();
+ _this.getInterface().hoverIntent({
+ 'sensitivity': 4,
+ 'timeout' : 2000,
+ 'over' : function(){
+ _this.controlBuilder.showControlBar();
+ },
+ 'out' : function(){
+ _this.controlBuilder.hideControlBar();
+ }
+ });
+ }
+ });
+ }
+ },
+
+ /**
+ * Get a time range from the media start and end time
+ *
+ * @return startNpt and endNpt time if present
+ */
+ getTimeRange: function() {
+ var end_time = ( this.controlBuilder.longTimeDisp )? '/' + mw.seconds2npt( this.getDuration() ) : '';
+ var defaultTimeRange = '0:00' + end_time;
+ if ( !this.mediaElement ){
+ return defaultTimeRange;
+ }
+ if ( !this.mediaElement.selectedSource ){
+ return defaultTimeRange;
+ }
+ if ( !this.mediaElement.selectedSource.endNpt ){
+ return defaultTimeRange;
+ }
+ return this.mediaElement.selectedSource.startNpt + this.mediaElement.selectedSource.endNpt;
+ },
+
+ /**
+ * Get the duration of the embed player
+ */
+ getDuration: function() {
+ if ( isNaN(this.duration) && this.mediaElement && this.mediaElement.selectedSource &&
+ typeof this.mediaElement.selectedSource.durationHint != 'undefined' ){
+ this.duration = this.mediaElement.selectedSource.durationHint;
+ }
+ return this.duration;
+ },
+
+ /**
+ * Get the player height
+ */
+ getHeight: function() {
+ return this.getInterface().height();
+ },
+
+ /**
+ * Get the player width
+ */
+ getWidth: function(){
+ return this.getInterface().width();
+ },
+
+ /**
+ * Check if the selected source is an audio element:
+ */
+ isAudio: function(){
+ return ( this.rewriteElementTagName == 'audio'
+ ||
+ ( this.mediaElement && this.mediaElement.selectedSource && this.mediaElement.selectedSource.mimeType.indexOf('audio/') !== -1 )
+ );
+ },
+
+ /**
+ * Get the plugin embed html ( should be implemented by embed player interface )
+ */
+ embedPlayerHTML: function() {
+ return 'Error: function embedPlayerHTML should be implemented by embed player interface ';
+ },
+
+ /**
+ * Seek function ( should be implemented by embedPlayer interface
+ * playerNative, playerKplayer etc. ) embedPlayer seek only handles URL
+ * time seeks
+ * @param {Float}
+ * percent of the video total length to seek to
+ */
+ seek: function( percent ) {
+ var _this = this;
+ this.seeking = true;
+ // Trigger preSeek event for plugins that want to store pre seek conditions.
+ $( this ).trigger( 'preSeek', percent );
+
+ // Do argument checking:
+ if( percent < 0 ){
+ percent = 0;
+ }
+
+ if( percent > 1 ){
+ percent = 1;
+ }
+ // set the playhead to the target position
+ this.updatePlayHead( percent );
+
+ // See if we should do a server side seek ( player independent )
+ if ( this.supportsURLTimeEncoding() ) {
+ mw.log( 'EmbedPlayer::seek:: updated serverSeekTime: ' + mw.seconds2npt ( this.serverSeekTime ) +
+ ' currentTime: ' + _this.currentTime );
+ // make sure we need to seek:
+ if( _this.currentTime == _this.serverSeekTime ){
+ return ;
+ }
+
+ this.stop();
+ this.didSeekJump = true;
+ // Make sure this.serverSeekTime is up-to-date:
+ this.serverSeekTime = mw.npt2seconds( this.startNpt ) + parseFloat( percent * this.getDuration() );
+ }
+ // Run the onSeeking interface update
+ // NOTE controlBuilder should really bind to html5 events rather
+ // than explicitly calling it or inheriting stuff.
+ this.controlBuilder.onSeek();
+ },
+
+ /**
+ * Seeks to the requested time and issues a callback when ready (should be
+ * overwritten by client that supports frame serving)
+ */
+ setCurrentTime: function( time, callback ) {
+ mw.log( 'Error: EmbedPlayer, setCurrentTime not overriden' );
+ if( $.isFunction( callback ) ){
+ callback();
+ }
+ },
+
+ /**
+ * On clip done action. Called once a clip is done playing
+ * TODO clean up end sequence flow
+ */
+ triggeredEndDone: false,
+ postSequence: false,
+ onClipDone: function() {
+ var _this = this;
+ // Don't run onclipdone if _propagateEvents is off
+ if( !_this._propagateEvents ){
+ return ;
+ }
+ mw.log( 'EmbedPlayer::onClipDone: propagate:' + _this._propagateEvents + ' id:' + this.id + ' doneCount:' + this.donePlayingCount + ' stop state:' +this.isStopped() );
+ // Only run stopped once:
+ if( !this.isStopped() ){
+ // set the "stopped" flag:
+ this.stopped = true;
+
+ // Show the control bar:
+ this.controlBuilder.showControlBar();
+
+ // TOOD we should improve the end event flow
+ // First end event for ads or current clip ended bindings
+ if( ! this.onDoneInterfaceFlag ){
+ this.stopEventPropagation();
+ }
+
+ mw.log("EmbedPlayer:: trigger: ended ( inteface continue pre-check: " + this.onDoneInterfaceFlag + ' )' );
+ $( this ).trigger( 'ended' );
+ mw.log("EmbedPlayer::onClipDone:Trigged ended, continue? " + this.onDoneInterfaceFlag);
+
+
+ if( ! this.onDoneInterfaceFlag ){
+ // Restore events if we are not running the interface done actions
+ this.restoreEventPropagation();
+ return ;
+ }
+
+ // A secondary end event for playlist and clip sequence endings
+ if( this.onDoneInterfaceFlag ){
+ // We trigger two end events to match KDP and ensure playbackComplete always comes before playerPlayEnd
+ // in content ends.
+ mw.log("EmbedPlayer:: trigger: playbackComplete");
+ $( this ).trigger( 'playbackComplete' );
+ // now trigger postEnd for( playerPlayEnd )
+ mw.log("EmbedPlayer:: trigger: postEnded");
+ $( this ).trigger( 'postEnded' );
+ }
+ // if the ended event did not trigger more timeline actions run the actual stop:
+ if( this.onDoneInterfaceFlag ){
+ mw.log("EmbedPlayer::onDoneInterfaceFlag=true do interface done");
+ // Prevent the native "onPlay" event from propagating that happens when we rewind:
+ this.stopEventPropagation();
+
+ // Update the clip done playing count ( for keeping track of replays )
+ _this.donePlayingCount ++;
+
+ // Rewind the player to the start:
+ // NOTE: Setting to 0 causes lags on iPad when replaying, thus setting to 0.01
+ this.setCurrentTime(0.01, function(){
+
+ // Set to stopped state:
+ _this.stop();
+
+ // Restore events after we rewind the player
+ _this.restoreEventPropagation();
+
+ // Check if we have the "loop" property set
+ if( _this.loop ) {
+ _this.stopped = false;
+ _this.play();
+ return;
+ } else {
+ // make sure we are in a paused state.
+ _this.pause();
+ }
+ // Check if have a force display of the large play button
+ if( mw.config.get('EmbedPlayer.ForceLargeReplayButton') === true ){
+ _this.addLargePlayBtn();
+ } else{
+ // Check if we should hide the large play button on end:
+ if( $( _this ).data( 'hideEndPlayButton' ) || !_this.useLargePlayBtn() ){
+ _this.hideLargePlayBtn();
+ } else {
+ _this.addLargePlayBtn();
+ }
+ }
+ // An event for once the all ended events are done.
+ mw.log("EmbedPlayer:: trigger: onEndedDone");
+ if ( !_this.triggeredEndDone ){
+ _this.triggeredEndDone = true;
+ $( _this ).trigger( 'onEndedDone', [_this.id] );
+ }
+ })
+ }
+ }
+ },
+
+
+ /**
+ * Shows the video Thumbnail, updates pause state
+ */
+ showThumbnail: function() {
+ var _this = this;
+ mw.log( 'EmbedPlayer::showThumbnail::' + this.stopped );
+
+ // Close Menu Overlay:
+ this.controlBuilder.closeMenuOverlay();
+
+ // update the thumbnail html:
+ this.updatePosterHTML();
+
+ this.paused = true;
+ this.stopped = true;
+
+ // Once the thumbnail is shown run the mediaReady trigger (if not using native controls)
+ if( !this.useNativePlayerControls() ){
+ mw.log("mediaLoaded");
+ $( this ).trigger( 'mediaLoaded' );
+ }
+ },
+
+ /**
+ * Show the player
+ */
+ showPlayer: function () {
+ mw.log( 'EmbedPlayer:: showPlayer: ' + this.id + ' interface: w:' + this.width + ' h:' + this.height );
+ var _this = this;
+
+ // Remove the player loader spinner if it exists
+ this.hideSpinnerAndPlayBtn();
+ // If a isPersistentNativePlayer ( overlay the controls )
+ if( !this.useNativePlayerControls() && this.isPersistentNativePlayer() ){
+ $( this ).show();
+ }
+ // Add controls if enabled:
+ if ( this.controls ) {
+ if( this.useNativePlayerControls() ){
+ if( this.getPlayerElement() ){
+ $( this.getPlayerElement() ).attr('controls', "true");
+ }
+ } else {
+ this.controlBuilder.addControls();
+ }
+ }
+
+ // Update Thumbnail for the "player"
+ this.updatePosterHTML();
+
+ // Update temporal url if present
+ this.updateTemporalUrl();
+
+ // Do we need to show the player?
+ if( this.displayPlayer === false ) {
+ _this.getVideoHolder().hide();
+ _this.getInterface().height( _this.getComponentsHeight() );
+ _this.triggerHelper('updateLayout');
+ }
+
+ // Update layout
+ this.updateLayout();
+
+ // Make sure we have a play btn:
+ this.addLargePlayBtn();
+
+ // Update the playerReady flag
+ this.playerReadyFlag = true;
+ mw.log("EmbedPlayer:: Trigger: playerReady");
+ // trigger the player ready event;
+ $( this ).trigger( 'playerReady' );
+ this.triggerWidgetLoaded();
+
+ // Check if we want to block the player display
+ if( this['data-blockPlayerDisplay'] ){
+ this.blockPlayerDisplay();
+ return ;
+ }
+
+ // Check if there are any errors to be displayed:
+ if( this.getError() ){
+ this.showErrorMsg( this.getError() );
+ return ;
+ }
+ // Auto play stopped ( no playerReady has already started playback ) and if not on an iPad with iOS > 3
+ if ( this.isStopped() && this.autoplay && (!mw.isIOS() || mw.isIpad3() ) ) {
+ mw.log( 'EmbedPlayer::showPlayer::Do autoPlay' );
+ _this.play();
+ }
+ },
+
+ getComponentsHeight: function() {
+ var height = 0;
+
+ // Go over all playerContainer direct children with .block class
+ this.getInterface().find('.block').each(function() {
+ height += $( this ).outerHeight( true );
+ });
+
+ // FIXME embedPlayer should know nothing about playlist layout
+ /* If we're in vertical playlist mode, and not in fullscreen add playlist height
+ if( $('#container').hasClass('vertical') && ! this.controlBuilder.isInFullScreen() && this.displayPlayer ) {
+ height += $('#playlistContainer').outerHeight( true );
+ }
+ */
+
+ //
+ var offset = (mw.isIOS()) ? 5 : 0;
+
+ return height + offset;
+ },
+ updateLayout: function() {
+ // update image layout:
+ this.applyIntrinsicAspect();
+ if( !mw.config.get('EmbedPlayer.IsIframeServer' ) ){
+ // Use intrensic container size
+ return ;
+ }
+ // Set window height if in iframe:
+ var windowHeight;
+ if( mw.isIOS() && ! this.controlBuilder.isInFullScreen() ) {
+ windowHeight = $( window.parent.document.getElementById( this.id ) ).height();
+ } else {
+ windowHeight = window.innerHeight;
+ }
+
+ var newHeight = windowHeight - this.getComponentsHeight();
+ var currentHeight = this.getVideoHolder().height();
+ // Always update videoHolder height
+ if( currentHeight !== newHeight ) {
+ mw.log('EmbedPlayer: updateLayout:: window: ' + windowHeight + ', components: ' + this.getComponentsHeight() + ', videoHolder old height: ' + currentHeight + ', new height: ' + newHeight );
+ this.getVideoHolder().height( newHeight );
+ }
+ },
+ /**
+ * Gets a refrence to the main player interface, builds if not avaliable
+ */
+ getInterface: function(){
+ if( !this.$interface ){
+ // init the control builder
+ this.controlBuilder = new mw.PlayerControlBuilder( this );
+
+ // build the interface wrapper
+ this.$interface = $( this ).wrap(
+ $('<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 = '&lt;iframe src=&quot;' + mw.html.escape( iframeUrl ) + '&quot; ';
+
+ // Set width / height of embed object
+ embedCode += 'width=&quot;' + this.getPlayerWidth() +'&quot; ';
+ embedCode += 'height=&quot;' + this.getPlayerHeight() + '&quot; ';
+ embedCode += 'frameborder=&quot;0&quot; ';
+ embedCode += 'webkitAllowFullScreen mozallowfullscreen allowFullScreen';
+
+ // Close up the embedCode tag:
+ embedCode+='&gt;&lt/iframe&gt;';
+
+ // 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 = '&lt;script type=&quot;text/javascript&quot; ' +
+ 'src=&quot;' +
+ mw.html.escape(
+ mw.absoluteUrl(
+ mw.getMwEmbedSrc()
+ )
+ ) + '&quot;&gt;&lt;/script&gt' +
+ '&lt;' + embedtag + ' ';
+
+ if( this.poster ) {
+ embedCode += 'poster=&quot;' +
+ mw.html.escape( mw.absoluteUrl( this.poster ) ) +
+ '&quot; ';
+ }
+
+ // Set the skin if set to something other than default
+ if( this.skinName ){
+ embedCode += 'class=&quot;' +
+ mw.html.escape( this.skinName ) +
+ '&quot; ';
+ }
+
+ if( this.duration ) {
+ embedCode +='durationHint=&quot;' + parseFloat( this.duration ) + '&quot; ';
+ }
+
+ if( this.width || this.height ){
+ embedCode += 'style=&quot;';
+ embedCode += ( this.width )? 'width:' + this.width +'px;': '';
+ embedCode += ( this.height )? 'height:' + this.height +'px;': '';
+ embedCode += '&quot; ';
+ }
+
+ // Close the video attr
+ embedCode += '&gt;';
+
+ // 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 +='&lt;source src=&quot;' +
+ mw.absoluteUrl( source.src ) +
+ '&quot; &gt;&lt;/source&gt;';
+ }
+ }
+ // Close the video tag
+ embedCode += '&lt;/video&gt;';
+
+ 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
new file mode 100644
index 00000000..cf05af25
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.gif
Binary files differ
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
new file mode 100644
index 00000000..77f2a32a
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/kaltura_open_source_video_platform.png
Binary files differ
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png
new file mode 100644
index 00000000..53c772eb
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/kskin/images/ksprite.png
Binary files differ
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
new file mode 100644
index 00000000..155f15e1
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/EmbedPlayer/resources/skins/mvpcf/images/player_big_play_button.png
Binary files differ
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 );