/**
* The Core timed Text interface object
*
* handles class mappings for:
* menu display ( jquery.ui themeable )
* timed text loading request
* timed text edit requests
* timed text search & seek interface ( version 2 )
*
* @author: Michael Dale
*
*/
( function( mw, $ ) {"use strict";
// Merge in timed text related attributes:
mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [
'srclang',
'kind',
'label'
]);
/**
* Timed Text Object
* @param embedPlayer Host player for timedText interfaces
*/
mw.TimedText = function( embedPlayer ) {
return this.init( embedPlayer );
};
mw.TimedText.prototype = {
/**
* Preferences config order is presently:
* 1) user cookie
* 2) defaults provided in this config var:
*/
config: {
// Layout for basic "timedText" type can be 'ontop', 'off', 'below'
'layout' : 'ontop',
//Set the default local ( should be grabbed from the browser )
'userLanguage' : mw.config.get( 'wgUserLanguage' ) || 'en',
//Set the default kind of timedText to display ( un-categorized timed-text is by default "subtitles" )
'userKind' : 'subtitles'
},
// The default display mode is 'ontop'
defaultDisplayMode : 'ontop',
// Save last layout mode
lastLayout : 'ontop',
// The bind prefix:
bindPostFix: '.timedText',
// Default options are empty
options: {},
/**
* The list of enabled sources
*/
enabledSources: [],
// First loading flag - To set the layout at first load
firstLoad: true,
/**
* The current language key
*/
currentLangKey : null,
/**
* The direction of the current language
*/
currentLangDir : null,
/**
* Stores the last text string per kind to avoid dom checks for updated text
*/
prevText: [],
/**
* Text sources ( a set of textSource objects )
*/
textSources: [],
/**
* Valid "Track" categories
*/
validCategoriesKeys: [
"CC",
"SUB",
"TAD",
"KTV",
"TIK",
"AR",
"NB",
"META",
"TRX",
"LRC",
"LIN",
"CUE"
],
/**
* @constructor
* @param {Object} embedPlayer Host player for timedText interfaces
*/
init: function( embedPlayer ) {
var _this = this;
mw.log("TimedText: init() ");
this.embedPlayer = embedPlayer;
// don't display captions on native player:
if( embedPlayer.useNativePlayerControls() ){
return this;
}
// Load user preferences config:
var preferenceConfig = $.cookie( 'TimedText.Preferences' );
if( preferenceConfig !== "false" && preferenceConfig != null ) {
this.config = JSON.parse( preferenceConfig );
}
// remove any old bindings on change media:
$( this.embedPlayer ).bind( 'onChangeMedia' + this.bindPostFix , function(){
_this.destroy();
});
// Remove any old bindings before we add the current bindings:
_this.destroy();
// Add player bindings
_this.addPlayerBindings();
return this;
},
destroy: function(){
// remove any old player bindings;
$( this.embedPlayer ).unbind( this.bindPostFix );
// Clear out enabled sources:
this.enabledSources = [];
// Clear out text sources:
this.textSources = [];
},
/**
* Add timed text related player bindings
* @return
*/
addPlayerBindings: function(){
var _this = this;
var embedPlayer = this.embedPlayer;
// Check for timed text support:
_this.addInterface();
$( embedPlayer ).bind( 'timeupdate' + this.bindPostFix, function( event, jEvent, id ) {
// regain scope
_this = $('#' + id)[0].timedText;
// monitor text updates
_this.monitor();
} );
$( embedPlayer ).bind( 'firstPlay' + this.bindPostFix, function(event, id ) {
// regain scope
_this = $('#' + id)[0].timedText;
// Will load and setup timedText sources (if not loaded already loaded )
_this.setupTextSources();
// Hide the caption menu if presently displayed
$( '#textMenuContainer_' + _this.embedPlayer.id ).hide();
} );
// Re-Initialize when changing media
$( embedPlayer ).bind( 'onChangeMedia' + this.bindPostFix, function() {
_this.destroy();
_this.updateLayout();
_this.setupTextSources();
$( '#textMenuContainer_' + embedPlayer.id ).hide();
} );
// Resize the timed text font size per window width
$( embedPlayer ).bind( 'onCloseFullScreen' + this.bindPostFix + ' onOpenFullScreen' + this.bindPostFix, function() {
// Check if we are in fullscreen or not, if so add an additional bottom offset of
// double the default bottom padding.
var textOffset = _this.embedPlayer.controlBuilder.inFullScreen ?
mw.config.get("TimedText.BottomPadding") * 2 :
mw.config.get("TimedText.BottomPadding");
var textCss = _this.getInterfaceSizeTextCss({
'width' : embedPlayer.getInterface().width(),
'height' : embedPlayer.getInterface().height()
});
mw.log( 'TimedText::set text size for: : ' + embedPlayer.getInterface().width() + ' = ' + textCss['font-size'] );
if ( embedPlayer.controlBuilder.isOverlayControls() && !embedPlayer.getInterface().find( '.control-bar' ).is( ':hidden' ) ) {
textOffset += _this.embedPlayer.controlBuilder.getHeight();
}
embedPlayer.getInterface().find( '.track' )
.css( textCss )
.css({
// Get the text size scale then set it to control bar height + TimedText.BottomPadding;
'bottom': textOffset + 'px'
});
});
// Update the timed text size
$( embedPlayer ).bind( 'updateLayout'+ this.bindPostFix, function() {
// If the the player resize action is an animation, animate text resize,
// else instantly adjust the css.
var textCss = _this.getInterfaceSizeTextCss( {
'width': embedPlayer.getPlayerWidth(),
'height': embedPlayer.getPlayerHeight()
});
mw.log( 'TimedText::updateLayout: ' + textCss['font-size']);
embedPlayer.getInterface().find( '.track' ).css( textCss );
});
// Setup display binding
$( embedPlayer ).bind( 'onShowControlBar'+ this.bindPostFix, function(event, layout, id ){
// update embedPlayer ref:
var embedPlayer = $('#' + id )[0];
if ( embedPlayer.controlBuilder.isOverlayControls() ) {
// Move the text track if present
embedPlayer.getInterface().find( '.track' )
.stop()
.animate( layout, 'fast' );
}
});
$( embedPlayer ).bind( 'onHideControlBar' + this.bindPostFix, function(event, layout, id ){
var embedPlayer = $('#' + id )[0];
if ( embedPlayer.controlBuilder.isOverlayControls() ) {
// Move the text track down if present
embedPlayer.getInterface().find( '.track' )
.stop()
.animate( layout, 'fast' );
}
});
$( embedPlayer ).bind( 'AdSupport_StartAdPlayback' + this.bindPostFix, function() {
if ( $( '#textMenuContainer_' + embedPlayer.id ).length ) {
$( '#textMenuContainer_' + embedPlayer.id ).hide();
}
var $textButton = embedPlayer.getInterface().find( '.timed-text' );
if ( $textButton.length ) {
$textButton.unbind( 'click' );
}
_this.lastLayout = _this.getLayoutMode();
_this.setLayoutMode( 'off' );
} );
$( embedPlayer ).bind( 'AdSupport_EndAdPlayback' + this.bindPostFix, function() {
var $textButton = embedPlayer.getInterface().find( '.timed-text' );
if ( $textButton.length ) {
_this.bindTextButton( $textButton );
}
_this.setLayoutMode( _this.lastLayout );
} );
},
addInterface: function(){
var _this = this;
// By default we include a button in the control bar.
$( _this.embedPlayer ).bind( 'addControlBarComponent' + this.bindPostFix, function(event, controlBar ){
if( controlBar.supportedComponents['timedText'] !== false &&
_this.includeCaptionButton() ) {
controlBar.supportedComponents['timedText'] = true;
controlBar.components['timedText'] = _this.getTimedTextButton();
}
});
},
includeCaptionButton:function(){
return mw.config.get( 'TimedText.ShowInterface' ) == 'always' ||
this.embedPlayer.getTextTracks().length;
},
/**
* Get the current language key
* @return
* @type {string}
*/
getCurrentLangKey: function(){
return this.currentLangKey;
},
/**
* Get the current language direction
* @return
* @type {string}
*/
getCurrentLangDir: function(){
if ( !this.currentLangDir ) {
var source = this.getSourceByLanguage( this.getCurrentLangKey() );
this.currentLangDir = source.dir;
}
return this.currentLangDir;
},
/**
* The timed text button to be added to the interface
*/
getTimedTextButton: function(){
var _this = this;
/**
* The closed captions button
*/
return {
'w': 30,
'position': 6.9,
'o': function( ctrlObj ) {
var $textButton = $( '
' )
.attr( 'title', mw.msg( 'mwe-embedplayer-timed_text' ) )
.addClass( "ui-state-default ui-corner-all ui-icon_link rButton timed-text" )
.append(
$( '' )
.addClass( "ui-icon ui-icon-comment" )
)
// Captions binding:
.buttonHover();
_this.bindTextButton( $textButton );
return $textButton;
}
};
},
bindTextButton: function( $textButton ){
var _this = this;
$textButton.unbind('click.textMenu').bind('click.textMenu', function() {
_this.showTextMenu();
return true;
} );
},
/**
* Get the fullscreen text css
*/
getInterfaceSizeTextCss: function( size ) {
//mw.log(' win size is: ' + $( window ).width() + ' ts: ' + textSize );
return {
'font-size' : this.getInterfaceSizePercent( size ) + '%'
};
},
/**
* Show the text interface library and show the text interface near the player.
*/
showTextMenu: function() {
var embedPlayer = this.embedPlayer;
var loc = embedPlayer.getInterface().find( '.rButton.timed-text' ).offset();
mw.log('TimedText::showTextMenu:: ' + embedPlayer.id + ' location: ', loc);
// TODO: Fix menu animation
var $menuButton = this.embedPlayer.getInterface().find( '.timed-text' );
// Check if a menu has already been built out for the menu button:
if ( $menuButton[0].m ) {
$menuButton.embedMenu( 'show' );
} else {
// Bind the text menu:
this.buildMenu( true );
}
},
getTextMenuContainer: function(){
var textMenuId = 'textMenuContainer_' + this.embedPlayer.id;
if( !$( '#' + textMenuId ).length ){
//Setup the menu:
this.embedPlayer.getInterface().append(
$('')
.addClass('ui-widget ui-widget-content ui-corner-all')
.attr( 'id', textMenuId )
.css( {
'position' : 'absolute',
'height' : '180px',
'width' : '180px',
'font-size' : '12px',
'display' : 'none',
'overflow' : 'auto'
} )
);
}
return $( '#' + textMenuId );
},
/**
* Gets a text size percent relative to about 30 columns of text for 400
* pixel wide player, at 100% text size.
*
* @param size {object} The size of the target player area width and height
*/
getInterfaceSizePercent: function( size ) {
// This is a ugly hack we should read "original player size" and set based
// on some standard ish normal 31 columns 15 rows
var sizeFactor = 4;
if( size.height / size.width < .7 ){
sizeFactor = 6;
}
var textSize = size.width / sizeFactor;
if( textSize < 95 ){
textSize = 95;
}
if( textSize > 150 ){
textSize = 150;
}
return textSize;
},
/**
* Setups available text sources
* loads text sources
* auto-selects a source based on the user language
* @param {Function} callback Function to be called once text sources are setup.
*/
setupTextSources: function( callback ) {
mw.log( 'TimedText::setupTextSources');
var _this = this;
// Load textSources
_this.loadTextSources( function() {
// Enable a default source and issue a request to "load it"
_this.autoSelectSource();
// Load and parse the text value of enabled text sources:
_this.loadEnabledSources();
if( callback ) {
callback();
}
} );
},
/**
* Binds the timed text menu
* and updates its content from "getMainMenu"
*
* @param {Object} target to display the menu
* @param {Boolean} autoShow If the menu should be displayed
*/
buildMenu: function( autoShow ) {
var _this = this;
var embedPlayer = this.embedPlayer;
// Setup text sources ( will callback inline if already loaded )
_this.setupTextSources( function() {
var $menuButton = _this.embedPlayer.getInterface().find( '.timed-text' );
var positionOpts = { };
if( _this.embedPlayer.supports[ 'overlays' ] ){
var positionOpts = {
'directionV' : 'up',
'offsetY' : _this.embedPlayer.controlBuilder.getHeight(),
'directionH' : 'left',
'offsetX' : -28
};
}
if( !_this.embedPlayer.getInterface() ){
mw.log("TimedText:: interface called before interface ready, just wait for interface");
return ;
}
var $menuButton = _this.embedPlayer.getInterface().find( '.timed-text' );
var ctrlObj = _this.embedPlayer.controlBuilder;
// NOTE: Button target should be an option or config
$menuButton.embedMenu( {
'content' : _this.getMainMenu(),
'zindex' : mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 2,
'crumbDefaultText' : ' ',
'autoShow': autoShow,
'keepPosition' : true,
'showSpeed': 0,
'height' : 100,
'width' : 300,
'targetMenuContainer' : _this.getTextMenuContainer(),
'positionOpts' : positionOpts,
'backLinkText' : mw.msg( 'mwe-timedtext-back-btn' ),
'createMenuCallback' : function(){
var $interface = _this.embedPlayer.getInterface();
var $textContainer = _this.getTextMenuContainer();
var textHeight = 130;
var top = $interface.height() - textHeight - ctrlObj.getHeight() - 6;
if( top < 0 ){
top = 0;
}
// check for audio
if( _this.embedPlayer.isAudio() ){
top = _this.embedPlayer.controlBuilder.getHeight() + 4;
}
$textContainer.css({
'top' : top,
'height': textHeight,
'position' : 'absolute',
'left': $menuButton[0].offsetLeft - 165,
'bottom': ctrlObj.getHeight()
})
ctrlObj.showControlBar( true );
},
'closeMenuCallback' : function(){
ctrlObj.restoreControlsHover();
}
});
});
},
/**
* Monitor video time and update timed text filed[s]
*/
monitor: function() {
//mw.log(" timed Text monitor: " + this.enabledSources.length );
var embedPlayer = this.embedPlayer;
// Setup local reference to currentTime:
var currentTime = embedPlayer.currentTime;
// Get the text per kind
var textCategories = [ ];
var source = this.enabledSources[ 0 ];
if( source ) {
this.updateSourceDisplay( source, currentTime );
}
},
/**
* Load all the available text sources from the inline embed
* @param {Function} callback Function to call once text sources are loaded
*/
loadTextSources: function( callback ) {
var _this = this;
// check if text sources are already loaded ( not em )
if( this.textSources.length ){
callback( this.textSources );
return ;
}
this.textSources = [];
// load inline text sources:
$.each( this.embedPlayer.getTextTracks(), function( inx, textSource ){
_this.textSources.push( new mw.TextSource( textSource ) );
});
// return the callback with sources
callback( _this.textSources );
},
/**
* Get the layout mode
*
* Takes into consideration:
* Playback method overlays support ( have to put subtitles below video )
*
*/
getLayoutMode: function() {
// Re-map "ontop" to "below" if player does not support
if( this.config.layout == 'ontop' && !this.embedPlayer.supports['overlays'] ) {
this.config.layout = 'below';
}
return this.config.layout;
},
/**
* Auto selects a source given the local configuration
*
* NOTE: presently this selects a "single" source.
* In the future we could support multiple "enabled sources"
*/
autoSelectSource: function() {
var _this = this;
// If a source is enabled then don't auto select
if ( this.enabledSources.length ) {
return false;
}
this.enabledSources = [];
var setDefault = false;
// Check if any source is marked default:
$.each( this.textSources, function(inx, source){
if( source['default'] ){
_this.enableSource( source );
setDefault = true;
return false;
}
});
if ( setDefault ) {
return true;
}
var setLocalPref = false;
// Check if any source matches our "local" pref
$.each( this.textSources, function(inx, source){
if( _this.config.userLanguage == source.srclang.toLowerCase()
&&
_this.config.userKind == source.kind
) {
_this.enableSource( source );
setLocalPref = true;
return false;
}
});
if ( setLocalPref ) {
return true;
}
var setEnglish = false;
// If no userLang, source try enabling English:
if( this.enabledSources.length == 0 ) {
for( var i=0; i < this.textSources.length; i++ ) {
var source = this.textSources[ i ];
if( source.srclang.toLowerCase() == 'en' ) {
_this.enableSource( source );
setEnglish = true;
return false;
}
}
}
if ( setEnglish ) {
return true;
}
var setFirst = false;
// If still no source try the first source we get;
if( this.enabledSources.length == 0 ) {
for( var i=0; i < this.textSources.length; i++ ) {
var source = this.textSources[ i ];
_this.enableSource( source );
setFirst = true;
return false;
}
}
if ( setFirst ) {
return true;
}
return false;
},
/**
* Enable a source and update the currentLangKey
* @param {object} source
* @return
*/
enableSource: function( source ){
var _this = this;
// check if we have any source set yet:
if( !_this.enabledSources.length ){
_this.enabledSources.push( source );
_this.currentLangKey = source.srclang;
_this.currentLangDir = null;
return ;
}
var sourceEnabled = false;
// Make sure the source is not already enabled
$.each( this.enabledSources, function( inx, enabledSource ){
if( source.id == enabledSource.id ){
sourceEnabled = true;
}
});
if ( !sourceEnabled ) {
_this.enabledSources.push( source );
_this.currentLangKey = source.srclang;
_this.currentLangDir = null;
}
},
/**
* Get the current source sub captions
* @param {function} callback function called once source is loaded
*/
loadCurrentSubSource: function( callback ){
mw.log("loadCurrentSubSource:: enabled source:" + this.enabledSources.length);
for( var i =0; i < this.enabledSources.length; i++ ){
var source = this.enabledSources[i];
if( source.kind == 'SUB' ){
source.load( function(){
callback( source);
return ;
});
}
}
return false;
},
/**
* Get sub captions by language key:
*
* @param {string} langKey Key of captions to load
* @pram {function} callback function called once language key is loaded
*/
getSubCaptions: function( langKey, callback ){
for( var i=0; i < this.textSources.length; i++ ) {
var source = this.textSources[ i ];
if( source.srclang.toLowerCase() === langKey ) {
var source = this.textSources[ i ];
source.load( function(){
callback( source.captions );
});
}
}
},
/**
* Issue a request to load all enabled Sources
* Should be called anytime enabled Source list is updated
*/
loadEnabledSources: function() {
var _this = this;
mw.log( "TimedText:: loadEnabledSources " + this.enabledSources.length );
$.each( this.enabledSources, function( inx, enabledSource ) {
// check if the source requires ovelray ( ontop ) layout mode:
if( enabledSource.isOverlay() && _this.config.layout== 'ontop' ){
_this.setLayoutMode( 'ontop' );
}
enabledSource.load(function(){
// Trigger the text loading event:
$( _this.embedPlayer ).trigger('loadedTextSource', enabledSource);
});
});
},
/**
* Checks if a source is "on"
* @return {Boolean}
* true if source is on
* false if source is off
*/
isSourceEnabled: function( source ) {
// no source is "enabled" if subtitles are "off"
if( this.getLayoutMode() == 'off' ){
return false;
}
var isEnabled = false;
$.each( this.enabledSources, function( inx, enabledSource ) {
if( source.id ) {
if( source.id === enabledSource.id ){
isEnabled = true;
}
}
if( source.src ){
if( source.src == enabledSource.src ){
isEnabled = true;
}
}
});
return isEnabled;
},
/**
* Marks the active captions in the menu
*/
markActive: function( source ) {
var $menu = $( '#textMenuContainer_' + this.embedPlayer.id );
if ( $menu.length ) {
var $captionRows = $menu.find( '.captionRow' );
if ( $captionRows.length ) {
$captionRows.each( function() {
$( this ).removeClass( 'ui-icon-bullet ui-icon-radio-on' );
var iconClass = ( $( this ).data( 'caption-id' ) === source.id ) ? 'ui-icon-bullet' : 'ui-icon-radio-on';
$( this ).addClass( iconClass );
} );
}
}
},
/**
* Marks the active layout mode in the menu
*/
markLayoutActive: function ( layoutMode ) {
var $menu = $( '#textMenuContainer_' + this.embedPlayer.id );
if ( $menu.length ) {
var $layoutRows = $menu.find( '.layoutRow' );
if ( $layoutRows.length ) {
$layoutRows.each( function() {
$( this ).removeClass( 'ui-icon-bullet ui-icon-radio-on' );
var iconClass = ( $( this ).data( 'layoutMode' ) === layoutMode ) ? 'ui-icon-bullet' : 'ui-icon-radio-on';
$( this ).addClass( iconClass );
} );
}
}
},
/**
* Get a source object by language, returns "false" if not found
* @param {string} langKey The language key filter for selected source
*/
getSourceByLanguage: function ( langKey ) {
for(var i=0; i < this.textSources.length; i++) {
var source = this.textSources[ i ];
if( source.srclang == langKey ){
return source;
}
}
return false;
},
/**
* Builds the core timed Text menu and
* returns the binded jquery object / dom set
*
* Assumes text sources have been setup: ( _this.setupTextSources() )
*
* calls a few sub-functions:
* Basic menu layout:
* Chose Language
* All Subtiles here ( if we have categories list them )
* Layout
* Below video
* Ontop video ( only available to supported plugins )
* TODO features:
* [ Search Text ]
* [ This video ]
* [ All videos ]
* [ Chapters ] seek to chapter
*/
getMainMenu: function() {
var _this = this;
// Set the menut to avaliable languages:
var $menu = _this.getLanguageMenu();
if( _this.textSources.length == 0 ){
$menu.append(
$.getLineItem( mw.msg( 'mwe-timedtext-no-subs'), 'close' )
);
} else {
// Layout Menu option if not in an iframe and we can expand video size:
$menu.append(
$.getLineItem(
mw.msg( 'mwe-timedtext-layout-off'),
( _this.getLayoutMode() == 'off' ) ? 'bullet' : 'radio-on',
function() {
_this.setLayoutMode( 'off' );
},
'layoutRow',
{ 'layoutMode' : 'off' }
)
)
}
// Allow other modules to add to the timed text menu:
$( _this.embedPlayer ).trigger( 'TimedText_BuildCCMenu', [ $menu, _this.embedPlayer.id ] ) ;
// Test if only one menu item move its children to the top level
if( $menu.children('li').length == 1 ){
$menu.find('li > ul > li').detach().appendTo( $menu );
$menu.find('li').eq(0).remove();
}
return $menu;
},
/**
* Utility function to assist in menu build out:
* Get menu line item (li) html:
msgKey
*
* @param {String} msgKey Msg key for menu item
*/
/**
* Get line item (li) from source object
* @param {Object} source Source to get menu line item from
*/
getLiSource: function( source ) {
var _this = this;
//See if the source is currently "on"
var sourceIcon = ( this.isSourceEnabled( source ) )? 'bullet' : 'radio-on';
if( source.title ) {
return $.getLineItem( source.title, sourceIcon, function() {
_this.selectTextSource( source );
}, 'captionRow', { 'caption-id' : source.id } );
}
if( source.srclang ) {
var langKey = source.srclang.toLowerCase();
return $.getLineItem(
mw.msg('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) ),
sourceIcon,
function() {
// select the current text source:
_this.selectTextSource( source );
},
'captionRow',
{ 'caption-id' : source.id }
);
}
},
/**
* Get language name from language key
* @param {String} lang_key Language key
*/
getLanguageName: function( lang_key ) {
if( mw.Language.names[ lang_key ]) {
return mw.Language.names[ lang_key ];
}
return false;
},
/**
* set the layout mode
* @param {Object} layoutMode The selected layout mode
*/
setLayoutMode: function( layoutMode ) {
var _this = this;
mw.log("TimedText:: setLayoutMode: " + layoutMode + ' ( old mode: ' + _this.config.layout + ' )' );
if( ( layoutMode != _this.config.layout ) || _this.firstLoad ) {
// Update the config and redraw layout
_this.config.layout = layoutMode;
// Update the display:
_this.updateLayout();
_this.firstLoad = false;
}
_this.markLayoutActive( layoutMode );
},
toggleCaptions: function(){
mw.log( "TimedText:: toggleCaptions was:" + this.config.layout );
if( this.config.layout == 'off' ){
this.setLayoutMode( this.defaultDisplayMode );
} else {
this.setLayoutMode( 'off' );
}
},
/**
* Updates the timed text layout ( should be called when config.layout changes )
*/
updateLayout: function() {
mw.log( "TimedText:: updateLayout " );
var $playerTarget = this.embedPlayer.getInterface();
if( $playerTarget ) {
// remove any existing caption containers:
$playerTarget.find('.captionContainer,.captionsOverlay').remove();
}
this.refreshDisplay();
},
/**
* Select a new source
*
* @param {Object} source Source object selected
*/
selectTextSource: function( source ) {
var _this = this;
mw.log("TimedText:: selectTextSource: select lang: " + source.srclang );
// enable last non-off layout:
_this.setLayoutMode( _this.lastLayout );
// For some reason we lose binding for the menu ~sometimes~ re-bind
this.bindTextButton( this.embedPlayer.getInterface().find('timed-text') );
this.currentLangKey = source.srclang;
this.currentLangDir = null;
// Update the config language if the source includes language
if( source.srclang ){
this.config.userLanguage = source.srclang;
}
if( source.kind ){
this.config.userKind = source.kind;
}
// (@@todo update kind & setup kind language buckets? )
// Remove any other sources selected in sources kind
this.enabledSources = [];
this.enabledSources.push( source );
// Set any existing text target to "loading"
if( !source.loaded ) {
var $playerTarget = this.embedPlayer.getInterface();
$playerTarget.find('.track').text( mw.msg('mwe-timedtext-loading-text') );
// Load the text:
source.load( function(){
// Refresh the interface:
_this.refreshDisplay();
});
} else {
_this.refreshDisplay();
}
_this.markActive( source );
// Trigger the event
$( this.embedPlayer ).trigger( 'TimedText_ChangeSource' );
},
/**
* Refresh the display, updates the timedText layout, menu, and text display
* also updates the cookie preference.
*
* Called after a user option change
*/
refreshDisplay: function() {
// Update the configuration object
$.cookie( 'TimedText.Preferences', JSON.stringify( this.config ) );
// Empty out previous text to force an interface update:
this.prevText = [];
// Refresh the Menu (if it has a target to refresh)
mw.log( 'TimedText:: bind menu refresh display' );
this.buildMenu();
this.resizeInterface();
// add an empty catption:
this.displayTextTarget( $( '
').text( '') );
// Issues a "monitor" command to update the timed text for the new layout
this.monitor();
},
/**
* Builds the language source list menu
* Cehck if the "track" tags had the "kind" attribute.
*
* The kind attribute forms "categories" of text tracks like "subtitles",
* "audio description", "chapter names". We check for these categories
* when building out the language menu.
*/
getLanguageMenu: function() {
var _this = this;
// See if we have categories to worry about
// associative array of SUB etc categories. Each kind contains an array of textSources.
var categorySourceList = {};
var sourcesWithCategoryCount = 0;
// ( All sources should have a kind (depreciate )
var sourcesWithoutCategory = [ ];
for( var i=0; i < this.textSources.length; i++ ) {
var source = this.textSources[ i ];
if( source.kind ) {
var categoryKey = source.kind ;
// Init Category menu item if it does not already exist:
if( !categorySourceList[ categoryKey ] ) {
// Set up catList pointer:
categorySourceList[ categoryKey ] = [];
sourcesWithCategoryCount++;
}
// Append to the source kind key menu item:
categorySourceList[ categoryKey ].push(
_this.getLiSource( source )
);
}else{
sourcesWithoutCategory.push( _this.getLiSource( source ) );
}
}
var $langMenu = $('
');
// Check if we have multiple categories ( if not just list them under the parent menu item)
if( sourcesWithCategoryCount > 1 ) {
for(var categoryKey in categorySourceList) {
var $catChildren = $('');
for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
$catChildren.append(
categorySourceList[ categoryKey ][i]
);
}
// Append a cat menu item for each kind list
// Give grep a chance to find the usages:
// mwe-timedtext-textcat-cc, mwe-timedtext-textcat-sub, mwe-timedtext-textcat-tad,
// mwe-timedtext-textcat-ktv, mwe-timedtext-textcat-tik, mwe-timedtext-textcat-ar,
// mwe-timedtext-textcat-nb, mwe-timedtext-textcat-meta, mwe-timedtext-textcat-trx,
// mwe-timedtext-textcat-lrc, mwe-timedtext-textcat-lin, mwe-timedtext-textcat-cue
$langMenu.append(
$.getLineItem( mw.msg( 'mwe-timedtext-textcat-' + categoryKey.toLowerCase() ) ).append(
$catChildren
)
);
}
} else {
for(var categoryKey in categorySourceList) {
for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
$langMenu.append(
categorySourceList[ categoryKey ][i]
);
}
}
}
// Add any remaning sources that did nto have a category
for(var i=0; i < sourcesWithoutCategory.length; i++) {
$langMenu.append( sourcesWithoutCategory[i] );
}
return $langMenu;
},
/**
* Updates a source display in the interface for a given time
* @param {object} source Source to update
* @param {number} time Caption time used to add and remove active captions.
*/
updateSourceDisplay: function ( source, time ) {
var _this = this;
if( this.timeOffset ){
time = time + parseInt( this.timeOffset );
}
// Get the source text for the requested time:
var activeCaptions = source.getCaptionForTime( time );
var addedCaption = false;
// Show captions that are on:
$.each( activeCaptions, function( capId, caption ){
var $cap = _this.embedPlayer.getInterface().find( '.track[data-capId="' + capId +'"]');
if( caption.content != $cap.html() ){
// remove old
$cap.remove();
// add the updated value:
_this.addCaption( source, capId, caption );
addedCaption = true;
}
});
// hide captions that are off:
_this.embedPlayer.getInterface().find( '.track' ).each(function( inx, caption){
if( !activeCaptions[ $( caption ).attr('data-capId') ] ){
if( addedCaption ){
$( caption ).remove();
} else {
$( caption ).fadeOut( mw.config.get('EmbedPlayer.MonitorRate'), function(){$(this).remove();} );
}
}
});
},
addCaption: function( source, capId, caption ){
if( this.getLayoutMode() == 'off' ){
return ;
}
// use capId as a class instead of id for easy selections and no conflicts with
// multiple players on page.
var $textTarget = $('')
.addClass( 'track' )
.attr( 'data-capId', capId )
.hide();
// Update text ( use "html" instead of "text" so that subtitle format can
// include html formating
// TOOD we should scrub this for non-formating html
$textTarget.append(
$('')
.addClass( 'ttmlStyled' )
.css( 'pointer-events', 'auto')
.css( this.getCaptionCss() )
.append(
$('')
// Prevent background (color) overflowing TimedText
// http://stackoverflow.com/questions/9077887/avoid-overlapping-rows-in-inline-element-with-a-background-color-applied
.css( 'position', 'relative' )
.html( caption.content )
)
);
// Add/update the lang option
$textTarget.attr( 'lang', source.srclang.toLowerCase() );
// Update any links to point to a new window
$textTarget.find( 'a' ).attr( 'target', '_blank' );
// Add TTML or other complex text styles / layouts if we have ontop captions:
if( this.getLayoutMode() == 'ontop' ){
if( caption.css ){
$textTarget.css( caption.css );
} else {
$textTarget.css( this.getDefaultStyle() );
}
}
// Apply any custom style ( if we are ontop of the video )
this.displayTextTarget( $textTarget );
// apply any interface size adjustments:
$textTarget.css( this.getInterfaceSizeTextCss({
'width' : this.embedPlayer.getInterface().width(),
'height' : this.embedPlayer.getInterface().height()
})
);
// Update the style of the text object if set
if( caption.styleId ){
var capCss = source.getStyleCssById( caption.styleId );
$textTarget.find('span.ttmlStyled').css(
capCss
);
}
$textTarget.fadeIn('fast');
},
displayTextTarget: function( $textTarget ){
var embedPlayer = this.embedPlayer;
var $interface = embedPlayer.getInterface();
var controlBarHeight = embedPlayer.controlBuilder.getHeight();
if( this.getLayoutMode() == 'off' ){
// sync player size per audio player:
if( embedPlayer.isAudio() ){
$interface.find( '.overlay-win' ).css( 'top', controlBarHeight );
$interface.css( 'height', controlBarHeight );
}
return;
}
if( this.getLayoutMode() == 'ontop' ){
this.addTextOverlay(
$textTarget
);
} else if( this.getLayoutMode() == 'below' ){
this.addTextBelowVideo( $textTarget );
} else {
mw.log("Possible Error, layout mode not recognized: " + this.getLayoutMode() );
}
// sync player size per audio player:
if( embedPlayer.isAudio() && embedPlayer.getInterface().height() < 80 ){
$interface.find( '.overlay-win' ).css( 'top', 80);
$interface.css( 'height', 80 );
$interface.find('.captionsOverlay' )
.css('bottom', embedPlayer.controlBuilder.getHeight() )
}
},
getDefaultStyle: function(){
var defaultBottom = 15;
if( this.embedPlayer.controlBuilder.isOverlayControls() && !this.embedPlayer.getInterface().find( '.control-bar' ).is( ':hidden' ) ) {
defaultBottom += this.embedPlayer.controlBuilder.getHeight();
}
var baseCss = {
'position':'absolute',
'bottom': defaultBottom,
'width': '100%',
'display': 'block',
'opacity': .8,
'text-align': 'center'
};
baseCss =$.extend( baseCss, this.getInterfaceSizeTextCss({
'width' : this.embedPlayer.getInterface().width(),
'height' : this.embedPlayer.getInterface().height()
}));
return baseCss;
},
addTextOverlay: function( $textTarget ){
var _this = this;
var $captionsOverlayTarget = this.embedPlayer.getInterface().find('.captionsOverlay');
var layoutCss = {
'left': 0,
'top': 0,
'bottom': 0,
'right': 0,
'position': 'absolute',
'direction': this.getCurrentLangDir(),
'z-index': mw.config.get( 'EmbedPlayer.FullScreenZIndex' )
};
if( $captionsOverlayTarget.length == 0 ){
// TODO make this look more like addBelowVideoCaptionsTarget
$captionsOverlayTarget = $( '' )
.addClass( 'captionsOverlay' )
.css( layoutCss )
.css('pointer-events', 'none');
this.embedPlayer.getVideoHolder().append( $captionsOverlayTarget );
}
// Append the text:
$captionsOverlayTarget.append( $textTarget );
},
/**
* Applies the default layout for a text target
*/
addTextBelowVideo: function( $textTarget ) {
var $playerTarget = this.embedPlayer.getInterface();
// Get the relative positioned player class from the controlBuilder:
this.embedPlayer.controlBuilder.keepControlBarOnScreen = true;
if( !$playerTarget.find('.captionContainer').length || this.embedPlayer.useNativePlayerControls() ) {
this.addBelowVideoCaptionContainer();
}
$playerTarget.find('.captionContainer').html(
$textTarget.css( {
'color':'white'
} )
);
},
addBelowVideoCaptionContainer: function(){
var _this = this;
mw.log( "TimedText:: addBelowVideoCaptionContainer" );
var $playerTarget = this.embedPlayer.getInterface();
if( $playerTarget.find('.captionContainer').length ) {
return ;
}
// Append after video container
this.embedPlayer.getVideoHolder().after(
$('').addClass( 'captionContainer block' )
.css({
'width' : '100%',
'height' : mw.config.get( 'TimedText.BelowVideoBlackBoxHeight' ) + 'px',
'background-color' : '#000',
'text-align' : 'center',
'padding-top' : '5px'
} )
);
_this.embedPlayer.triggerHelper('updateLayout');
},
/**
* Resize the interface for layoutMode == 'below' ( if not in full screen)
*/
resizeInterface: function(){
var _this = this;
if( !_this.embedPlayer.controlBuilder ){
// too soon
return ;
}
if( !_this.embedPlayer.controlBuilder.inFullScreen && _this.originalPlayerHeight ){
_this.embedPlayer.triggerHelper( 'resizeIframeContainer', [{'height' : _this.originalPlayerHeight}] );
} else {
// removed resize on container content, since syncPlayerSize calls now handle keeping player aspect.
_this.embedPlayer.triggerHelper('updateLayout');
}
},
/**
* Build css for caption using this.options
*/
getCaptionCss: function() {
return {};
}
};
} )( mediaWiki, jQuery );