/** * 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 );