summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js')
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js1313
1 files changed, 1313 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js
new file mode 100644
index 00000000..2a69343e
--- /dev/null
+++ b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js
@@ -0,0 +1,1313 @@
+/**
+ * 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 = $( '<div />' )
+ .attr( 'title', mw.msg( 'mwe-embedplayer-timed_text' ) )
+ .addClass( "ui-state-default ui-corner-all ui-icon_link rButton timed-text" )
+ .append(
+ $( '<span />' )
+ .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(
+ $('<div>')
+ .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: <li><a> msgKey </a></li>
+ *
+ * @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( $( '<span /> ').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 = $('<ul>');
+ // 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 = $('<ul>');
+ 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 = $('<div />')
+ .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(
+ $('<span>')
+ .addClass( 'ttmlStyled' )
+ .css( 'pointer-events', 'auto')
+ .css( this.getCaptionCss() )
+ .append(
+ $('<span>')
+ // 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 = $( '<div />' )
+ .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(
+ $('<div>').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 );