diff options
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources')
3 files changed, 1835 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js new file mode 100644 index 00000000..cce8310f --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js @@ -0,0 +1,504 @@ +/** + * Base mw.TextSource object + * + * @param {Object} source Source object to extend + * @param {Object} textProvider [Optional] The text provider interface ( to load source from api ) + */ +( function( mw, $ ) { "use strict"; + + mw.TextSource = function( source ) { + return this.init( source ); + }; + mw.TextSource.prototype = { + + //The load state: + loaded: false, + + // Container for the captions + // captions include "start", "end" and "content" fields + captions: [], + + // The css style for captions ( some file formats specify display types ) + styleCss: {}, + + // The previous index of the timed text served + // Avoids searching the entire array on time updates. + prevIndex: 0, + + /** + * @constructor Inherits mediaSource from embedPlayer + * @param {source} Base source element + * @param {Object} Pointer to the textProvider + */ + init: function( source , textProvider) { + // Inherits mediaSource + for( var i in source){ + this[ i ] = source[ i ]; + } + + // Set default category to subtitle if unset: + if( ! this.kind ) { + this.kind = 'subtitle'; + } + //Set the textProvider if provided + if( textProvider ) { + this.textProvider = textProvider; + } + return this; + }, + + /** + * Function to load and parse the source text + * @param {Function} callback Function called once text source is loaded + */ + load: function( callback ) { + var _this = this; + mw.log("TextSource:: load src "+ _this.getSrc() ); + + // Setup up a callback ( in case it was not defined ) + if( !callback ){ + callback = function(){ return ; }; + } + + // Check if the captions have already been loaded: + if( this.loaded ){ + return callback(); + } + + // Try to load src via XHR source + if( !this.getSrc() ) { + mw.log( "Error: TextSource no source url for text track"); + return callback(); + } + + // Check type for special loaders: + $( mw ).triggerQueueCallback( 'TimedText_LoadTextSource', _this, function(){ + if( _this.loaded ){ + callback(); + } else { + // if no module loaded the text source use the normal ajax proxy: + new mw.ajaxProxy({ + url: _this.getSrc(), + success: function( resultXML ) { + _this.captions = _this.getCaptions( resultXML ); + _this.loaded = true; + mw.log("mw.TextSource :: loaded from " + _this.getSrc() + " Found: " + _this.captions.length + ' captions' ); + callback(); + }, + error: function() { + mw.log("Error: TextSource Error with http response"); + _this.loaded = true; + callback(); + } + }); + } + }) + }, + /** + * Returns the text content for requested time + * + * @param {Number} time Time in seconds + */ + getCaptionForTime: function ( time ) { + var prevCaption = this.captions[ this.prevIndex ]; + var captionSet = {}; + + // Setup the startIndex: + if( prevCaption && time >= prevCaption.start ) { + var startIndex = this.prevIndex; + }else{ + // If a backwards seek start searching at the start: + var startIndex = 0; + } + var firstCapIndex = 0; + // Start looking for the text via time, add all matches that are in range + for( var i = startIndex ; i < this.captions.length; i++ ) { + var caption = this.captions[ i ]; + // Don't handle captions with 0 or -1 end time: + if( caption.end == 0 || caption.end == -1) + continue; + + if( time >= caption.start && + time <= caption.end ) { + // set the earliest valid time to the current start index: + if( !firstCapIndex ){ + firstCapIndex = caption.start; + } + + //mw.log("Start cap time: " + caption.start + ' End time: ' + caption.end ); + captionSet[i] = caption ; + } + // captions are stored in start order stop search if we get larger than time + if( caption.start > time ){ + break; + } + } + // Update the prevIndex: + this.prevIndex = firstCapIndex; + //Return the set of captions in range: + return captionSet; + }, + + /** + * Check if the caption is an overlay format ( and must be ontop of the player ) + */ + isOverlay: function(){ + return this.mimeType == 'text/xml'; + }, + + getCaptions: function( data ){ + // Detect caption data type: + switch( this.mimeType ){ + case 'text/mw-srt': + return this.getCaptiosnFromMediaWikiSrt( data ); + break; + case 'text/x-srt': + return this.getCaptionsFromSrt( data); + break; + case 'text/xml': + return this.getCaptionsFromTMML( data ); + break; + } + // caption mime not found return empty set: + return []; + }, + + getStyleCssById: function( styleId ){ + if( this.styleCss[ styleId ] ){ + return this.styleCss[ styleId ]; + } + return {}; + }, + /** + * Grab timed text from TMML format + * + * @param data + * @return + */ + getCaptionsFromTMML: function( data ){ + var _this = this; + mw.log("TextSource::getCaptionsFromTMML", data); + // set up display information: + var captions = []; + var xml = ( $( data ).find("tt").length ) ? data : $.parseXML( data ); + + // Check for parse error: + try { + if( !xml || $( xml ).find('parsererror').length ){ + mw.log("Error: close caption parse error: " + $( xml ).find('parsererror').text() ); + return captions; + } + } catch ( e ) { + mw.log( "Error: close caption parse error: " + e.toString() ); + return captions; + } + + // Set the body Style + var bodyStyleId = $( xml ).find('body').attr('style'); + + // Set style translate ttml to css + $( xml ).find( 'style').each( function( inx, style){ + var cssObject = {}; + // Map CamelCase css properties: + $( style.attributes ).each(function(inx, attr){ + var attrName = attr.name; + if( attrName.substr(0, 4) !== 'tts:' ){ + // skip + return true; + } + var cssName = ''; + for( var c = 4; c < attrName.length; c++){ + if( attrName[c].toLowerCase() != attrName[c] ){ + cssName += '-' + attrName[c].toLowerCase(); + } else { + cssName+= attrName[c] + } + } + cssObject[ cssName ] = attr.nodeValue; + }); + // for(var i =0; i< style.length ) + _this.styleCss[ $( style).attr('id') ] = cssObject; + }); + + $( xml ).find( 'p' ).each( function( inx, p ){ + // Get text content by converting ttml node to html + var content = ''; + $.each( p.childNodes, function(inx, node){ + content+= _this.convertTTML2HTML( node ); + }); + // Get the end time: + var end = null; + if( $( p ).attr( 'end' ) ){ + end = mw.npt2seconds( $( p ).attr( 'end' ) ); + } + // Look for dur + if( !end && $( p ).attr( 'dur' )){ + end = mw.npt2seconds( $( p ).attr( 'begin' ) ) + + mw.npt2seconds( $( p ).attr( 'dur' ) ); + } + + // Create the caption object : + var captionObj ={ + 'start': mw.npt2seconds( $( p ).attr( 'begin' ) ), + 'end': end, + 'content': content + }; + + // See if we have custom metadata for position of this caption object + // there are 35 columns across and 15 rows high + var $meta = $(p).find( 'metadata' ); + if( $meta.length ){ + captionObj['css'] = { + 'position': 'absolute' + }; + if( $meta.attr('cccol') ){ + captionObj['css']['left'] = ( $meta.attr('cccol') / 35 ) * 100 +'%'; + // also means the width has to be reduced: + //captionObj['css']['width'] = 100 - parseInt( captionObj['css']['left'] ) + '%'; + } + if( $meta.attr('ccrow') ){ + captionObj['css']['top'] = ( $meta.attr('ccrow') / 15 ) * 100 +'%'; + } + } + if( $(p).attr('tts:textAlign') ){ + if( !captionObj['css'] ){ + captionObj['css'] = {}; + } + captionObj['css']['text-align'] = $(p).attr('tts:textAlign'); + + // Remove text align is "right" flip the css left: + if( captionObj['css']['text-align'] == 'right' && captionObj['css']['left'] ){ + //captionObj['css']['width'] = captionObj['css']['left']; + captionObj['css']['left'] = null; + } + } + + // check if this p has any style else use the body parent + if( $(p).attr('style') ){ + captionObj['styleId'] = $(p).attr('style') ; + } else { + captionObj['styleId'] = bodyStyleId; + } + captions.push( captionObj); + }); + return captions; + }, + convertTTML2HTML: function( node ){ + var _this = this; + + // look for text node: + if( node.nodeType == 3 ){ + return node.textContent; + } + // skip metadata nodes: + if( node.nodeName == 'metadata' ){ + return ''; + } + // if a br just append + if( node.nodeName == 'br' ){ + return '<br />'; + } + // Setup tts mappings TODO should be static property of a ttmlSource object. + var ttsStyleMap = { + 'tts:color' : 'color', + 'tts:fontWeight' : 'font-weight', + 'tts:fontStyle' : 'font-style' + }; + if( node.childNodes.length ){ + var nodeString = ''; + var styleVal = ''; + for( var attr in ttsStyleMap ){ + if( node.getAttribute( attr ) ){ + styleVal+= ttsStyleMap[ attr ] + ':' + node.getAttribute( attr ) + ';'; + } + } + nodeString += '<' + node.nodeName + ' style="' + styleVal + '" >'; + $.each( node.childNodes, function( inx, childNode ){ + nodeString += _this.convertTTML2HTML( childNode ); + }); + nodeString += '</' + node.nodeName + '>'; + return nodeString; + } + }, + /** + * srt timed text parse handle: + * @param {String} data Srt string to be parsed + */ + getCaptionsFromSrt: function ( data ){ + mw.log("TextSource::getCaptionsFromSrt"); + var _this = this; + // Check if the "srt" parses as an XML + try{ + var xml = $.parseXML( data ); + if( xml && $( xml ).find('parsererror').length == 0 ){ + return this.getCaptionsFromTMML( data ); + } + } catch ( e ){ + // srt should not be xml + } + // Remove dos newlines + var srt = data.replace(/\r+/g, ''); + + // Trim white space start and end + srt = srt.replace(/^\s+|\s+$/g, ''); + + // Remove all html tags for security reasons + srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, ''); + + // Get captions + var captions = []; + var caplist = srt.split('\n\n'); + for (var i = 0; i < caplist.length; i++) { + var captionText = ""; + var caption = false; + captionText = caplist[i]; + var s = captionText.split(/\n/); + if (s.length < 2) { + // file format error or comment lines + continue; + } + if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) { + // ignore caption number in s[0] + // parse time string + var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); + if (m) { + caption = _this.match2caption( m ); + } else { + // Unrecognized timestring + continue; + } + if( caption ){ + // concatenate text lines to html text + caption['content'] = s.slice(2).join("<br>"); + } + } else { + // file format error or comment lines + continue; + } + // Add the current caption to the captions set: + captions.push( caption ); + } + + return captions; + }, + + /** + * Get srts from a mediawiki html / srt string + * + * Right now wiki -> html is not always friendly to our srt parsing. + * The long term plan is to move the srt parsing to server side and have the api + * server up the srt's times in JSON form + * + * Also see https://bugzilla.wikimedia.org/show_bug.cgi?id=29126 + * + * TODO move to mediaWiki specific module. + */ + getCaptiosnFromMediaWikiSrt: function( data ){ + mw.log("TimedText::getCaptiosnFromMediaWikiSrt:"); + var _this = this; + var captions = [ ]; + var curentCap = { + 'content': '' + }; + var parseNextAsTime = false; + // Note this string concatenation and html error wrapping sometimes causes + // parse issues where the wikitext includes many native <p /> tags without child + // subtitles. In prating this is not a deal breakers because the wikitext for + // TimedText namespace and associated srts already has a specific format. + // Long term we will move to server side parsing. + $( '<div>' + data + '</div>' ).find('p').each( function() { + var currentPtext = $(this).html(); + //mw.log( 'pText: ' + currentPtext ); + + // We translate raw wikitext gennerated html into a matched srt time sample. + // The raw html looks like: + // # + // hh:mm:ss,ms --> hh:mm:ss,ms + // text + // + // You can read more about the srt format here: + // http://en.wikipedia.org/wiki/SubRip + // + // We attempt to be fairly robust in our regular expression to catch a few + // srt variations such as omition of commas and empty text lines. + var m = currentPtext + .replace('-->', '-->') // restore --> with --> for easier srt parsing: + .match(/\d+\s([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\s*--?>\s*([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\n?(.*)/); + + if (m) { + captions.push( + _this.match2caption( m ) + ); + return true; + } + + /*** + * Handle multi line sytle output + * + * Handles cases parse cases where an entire line can't be parsed in the single + * regular expression above, Since the diffrent captions pars are outputed in + * diffrent <p /> tags by the wikitext parser output. + */ + + // Check if we have reached the end of a multi line match + if( parseInt( currentPtext ) == currentPtext ) { + if( curentCap.content != '' ) { + captions.push( curentCap ); + } + // Clear out the current caption content + curentCap = { + 'content': '' + }; + return true; + } + // Check only for time match: + var m = currentPtext + .replace('-->', '-->') + .match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); + if (m) { + // Update the currentCap: + curentCap = _this.match2caption( m ); + return true; + } + // Else append contnet for the curentCap + if( currentPtext != '<br>' ) { + curentCap['content'] += currentPtext; + } + }); + //Push last subtitle: + if( curentCap.length != 0) { + captions.push( curentCap ); + } + mw.log( "TimedText::getCaptiosnFromMediaWikiSrt found " + captions.length + ' captions'); + return captions; + }, + /** + * Takes a regular expresion match and converts it to a caption object + */ + match2caption: function( m ){ + var caption = {}; + // Look for ms: + var startMs = (m[4]) ? parseInt(m[4], 10) : 0; + var endMs = (m[8]) ? parseInt(m[8], 10) : 0; + caption['start'] = this.timeParts2seconds( m[1], m[2], m[3], startMs ); + caption['end'] = this.timeParts2seconds( m[5], m[6], m[7], endMs ); + if( m[9] ){ + caption['content'] = $.trim( m[9] ); + } + return caption; + }, + /** + * Takes time parts in hours, min, seconds and milliseconds and coverts to float seconds. + */ + timeParts2seconds: function( hours, min, sec, ms ){ + return mw.measurements2seconds({ + 'hours': hours, + 'minutes': min, + 'seconds' : sec, + 'milliseconds': ms + }); + } + }; + + +} )( mediaWiki, jQuery ); 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 ); diff --git a/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.style.TimedText.css b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.style.TimedText.css new file mode 100644 index 00000000..e924ba34 --- /dev/null +++ b/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.style.TimedText.css @@ -0,0 +1,18 @@ +.ttmlStyled { + color: white; + letter-spacing: 0.04em; + text-align: center; + padding: 0.2em; + /* + // Text shadow is too slow with current browsers use background-color + text-shadow:0 2px 1px #000000, -1px 3px 1px #000000, -2px 2px 1px #000000, -2px 1px 1px #000000, -2px 0 1px #000000, 2px 2px 1px #000000, 1px 2px 1px #000000, 0 -2px 1px #000000, 2px -2px 1px #000000, -2px -1px 1px #000000, -1px -3px 1px #000000, -3px -2px 1px #000000, 0 0 25px #000000, 0 0 35px #000000, 0 0 35px #000000, 0 0 31px #FFFFFF, 0 0 31px #FFFFFF, 0 0 31px #FFFFFF; + */ + background-color: #333; +} +.ttmlStyled a { + text-decoration: none; + color : #BBF; +} +.ttmlStyled a:visited{ + color : #BBF; +} |