summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources')
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js504
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js1313
-rw-r--r--extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.style.TimedText.css18
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 --&gt 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('--&gt;', '-->') // restore --&gt 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('--&gt;', '-->')
+ .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;
+}