r104400 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r104399‎ | r104400 | r104401 >
Date:06:24, 28 November 2011
Author:dale
Status:resolved (Comments)
Tags:
Comment:
updates per code review comments: http://www.mediawiki.org/wiki/TimedMediaHandler/ReviewNotes#mw.TextSource.js
== mw.TimedText.js ==
* fixed spelling: userLanugage to userLanguage
* added default bottom padding config for textOffset
* added code comment to clarify Animate param in text resize during player resize
* added comment to explain default relative text size
* corrected spelling of loadCurrentSubSrouce to loadCurrentSubSource and added function documentation
* Removed all the add transcript support. Should be part of a gadget ( i.e once this ships could restore the miro universal subs gadget to a working state )
* added code documentation for track "kind" attribute and associated menu build out
* moved TimedText.BelowVideoBlackBoxHeight to config

== mw.TextSource.js ==
( Moved TextSource to its own file )

* move loaded = true to after actual loading.
* updated comments to point to bug 29126
* Added some comments for large regex used in srt parsing.
* refactored the match handling of srt parsing to local convenience functions

== mw.TimedTextMediaWikiSources.js ==
* fixed spelling ofmw.MediaWikTrackProvider
* clean up getTimedTextNS conditional logic
* updated default value to 710 per http://www.mediawiki.org/wiki/Extension_namespace_registration namespace register
Modified paths:
  • /trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.config.php (modified) (history)
  • /trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.php (modified) (history)
  • /trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js (added) (history)
  • /trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js (modified) (history)
  • /trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedTextEdit.js (deleted) (history)

Diff [purge]

Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.config.php
@@ -20,6 +20,12 @@
2121 // If the link to request a transcript should be shown on video files
2222 'TimedText.ShowRequestTranscript' => false,
2323
24 - // The category for listing videos that need transcription:
25 - 'TimedText.NeedsTranscriptCategory' => 'Videos needing subtitles'
 24+ // Category for listing videos that need transcription:
 25+ 'TimedText.NeedsTranscriptCategory' => 'Videos needing subtitles',
 26+
 27+ // Default bottom text padding
 28+ 'TimedText.BottomPadding' => 10,
 29+
 30+ // Height of black box below video.
 31+ 'TimedText.BelowVideoBlackBoxHeight' => 60,
2632 );
\ No newline at end of file
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedTextEdit.js
@@ -1,455 +0,0 @@
2 -/**
3 -* Timed text edit interface based off of participatory culture foundation timed text mockups.
4 -*/
5 -( function( mw, $ ) {
6 -
7 -mw.TimedTextEdit = function( parentTimedText ) {
8 - return this.init( parentTimedText );
9 -};
10 -mw.TimedTextEdit.prototype = {
11 - // The target container for the interface:
12 - target_container: null,
13 -
14 - // Interface steps can be "transcribe", "sync", "translate"
15 - textEditStages:{
16 - 'upload':{
17 - 'icon' : 'folder-open'
18 - }
19 - /*
20 - 'transcribe':{
21 - 'icon' : 'comment'
22 - },
23 - 'sync':{
24 - 'icon' : 'clock'
25 - },
26 - 'translate':{
27 - 'icon' : 'flag'
28 - }
29 - */
30 - },
31 -
32 - /**
33 - * @constructor
34 - * @param {Object} parentTimedText The parent TimedText object that called the editor
35 - */
36 - init: function( parentTimedText ) {
37 - this.parentTimedText = parentTimedText;
38 - },
39 -
40 - /**
41 - * Show the editor UI
42 - */
43 - showUI: function() {
44 - // Setup the parent container:
45 - this.createDialogContainer();
46 -
47 - // Setup the timedText editor interface
48 - this.initDialog();
49 - },
50 -
51 - /**
52 - * Setup the dialog layout: s
53 - */
54 - initDialog: function() {
55 - var _this =this;
56 - _this.createTabs();
57 - },
58 -
59 - /**
60 - * Creates interface tabs from the textEditStages
61 - */
62 - createTabs: function() {
63 - var _this = this;
64 - $tabContainer = $( '<div />' )
65 - .attr( 'id', "TimedTextEdit-tabs" )
66 - .append( '<ul />' );
67 - for(var edit_stage_id in this.textEditStages) {
68 - var editStage = this.textEditStages[ edit_stage_id ];
69 - // Append the menu item:
70 - $tabContainer.find('ul').append(
71 - $('<li>').append(
72 - $('<a>')
73 - .attr( 'href', '#tab-' + edit_stage_id )
74 - .append(
75 - $('<span />')
76 - .css( "float","left" )
77 - .addClass( 'ui-icon ui-icon-' + editStage.icon )
78 - ,
79 - $('<span>')
80 - .text( gM('mwe-timedtext-stage-' + edit_stage_id) )
81 - )
82 - )
83 - );
84 - // Append the menu item content container
85 - $tabContainer.append(
86 - $('<div>')
87 - .attr( 'id', 'tab-' + edit_stage_id )
88 - .css({
89 - 'height': $( window ).height() - 270,
90 - 'position': 'relative'
91 - })
92 - );
93 - }
94 - //debugger
95 - // Add the tags to the target:
96 - $( _this.target_container ).append( $tabContainer );
97 -
98 - //Create all the "interfaces"
99 - for(var edit_stage_id in this.textEditStages) {
100 - _this.createInterface( edit_stage_id );
101 - }
102 -
103 - //Add tabs interface
104 - $('#TimedTextEdit-tabs').tabs( {
105 - select: function( event, ui ) {
106 - _this.selectTab( $( ui.tab ).attr( 'href' ).replace('#','') );
107 - }
108 - });
109 -
110 - },
111 - selectTab: function( tab_id ) {
112 - mw.log('sel: ' + tab_id);
113 - },
114 -
115 - /**
116 - * Creates an interface for a given stage id
117 - * @return {Object} the jquery interface
118 - */
119 - createInterface: function( edit_stage_id) {
120 - $target = $('#tab-' + edit_stage_id);
121 - if( this[edit_stage_id + 'Interface']) {
122 - this[ edit_stage_id + 'Interface']( $target );
123 - }else{
124 - $target.append( ' interface under development' );
125 - }
126 - },
127 - /**
128 - * Builds out and binds the upload interface to a given target
129 - * @param {Object} $target jQuery target for the upload interface
130 - */
131 - uploadInterface: function( $target ) {
132 - var _this = this;
133 - // Check if user has XHR file upload support & we are on the target wiki
134 -
135 - $target.append(
136 - $('<div />')
137 - .addClass( "leftcolumn" )
138 - .append('<h4>')
139 - .text( gM('mwe-timedtext-upload-text') ),
140 - $('<div />')
141 - .addClass( 'rightcolumn' )
142 - .append(
143 - $( '<span />' )
144 - .attr('id', "timed-text-rightcolum-desc")
145 - .append(
146 - $('<h4>')
147 - .text( gM('mwe-timedtext-upload-text-desc-title') ),
148 - $('<i>').text ( gM( 'mwe-timedtext-upload-text-desc-help' ) ),
149 - $('<ul>').append(
150 - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-browse') ),
151 - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-select') ),
152 - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-review') )
153 - )
154 - ),
155 - //The text preview
156 - $('<h3>')
157 - .text( gM( 'mwe-timedtext-upload-text-preview' ) ),
158 - $('<textarea id="timed-text-file-preview"></textarea>')
159 - )
160 - );
161 -
162 - // Adjust the height of the text preview:
163 - $('#timed-text-file-preview')
164 - .css({
165 - 'width':'100%',
166 - 'height': '300px'
167 - });
168 -
169 - // Add Select file:
170 - $target.append(
171 - $('<div>').css({
172 - 'width':'300px',
173 - 'float': 'left'
174 - }).append(
175 - $('<input />')
176 - .attr( {
177 - 'type': "file",
178 - 'id' : "timed-text-file-upload"
179 - }),
180 - $('<br />')
181 - )
182 - );
183 -
184 -
185 - $target.append(
186 - //Get a little helper input field to update the language
187 - $('<input />')
188 - .attr( {
189 - 'id' : "timed-text-langKey-input",
190 - 'type' : "text",
191 - 'maxlength' : "10",
192 - 'size' :"3"
193 - } )
194 - .change(function() {
195 - var langKey = $(this).val();
196 - if( mw.Language.names[ langKey ] ) {
197 - $buttonTarget.find('.btnText').text(
198 - mw.Language.names[ langKey ]
199 - );
200 - }
201 - }),
202 - // Get a jQuery button object with language menu:
203 - $.button( {
204 - 'style': { 'float' : 'left' },
205 - 'class': 'language-select-btn',
206 - 'text': gM('mwe-timedtext-select-language'),
207 - 'icon': 'triangle-1-e'
208 - } )
209 - .attr('id', 'language-select')
210 - )
211 -
212 -
213 - var $buttonTarget = $target.find('.language-select-btn');
214 -
215 - // Add menu container:
216 - var loc = $buttonTarget.position();
217 - $target.append(
218 - $('<div>')
219 - .addClass('ui-widget ui-widget-content ui-corner-all')
220 - .attr( 'id', 'upload-language-select' )
221 - .loadingSpinner()
222 - .css( {
223 - 'position' : 'relative',
224 - 'z-index' : 10,
225 - 'height' : '180px',
226 - 'width' : '180px',
227 - 'overflow' : 'auto',
228 - 'font-size' : '12px',
229 - 'z-index' : 1005
230 - } )
231 - .hide()
232 - );
233 - // Add menu binding to button target
234 - setTimeout(function(){
235 - $buttonTarget.menu( {
236 - 'content' : _this.getLanguageList(),
237 - 'backLinkText' : gM( 'mwe-timedtext-back-btn' ),
238 - 'targetMenuContainer': '#upload-language-select',
239 - 'keepPosition' : true
240 - } );
241 - // force the layout ( menu binding does strange things )
242 - $('#upload-language-select').css( {'left': '315px', 'top' : '87px', 'position' : 'absolute'});
243 - },10);
244 -
245 -
246 - //Add upload input bindings:
247 - $( '#timed-text-file-upload' ).change( function( ev ) {
248 - if ( $(this).val() ) {
249 -
250 - // Update the preview text area:
251 - var file = $( '#timed-text-file-upload' ).get(0).files[0];
252 - if( file.fileSize > 1048576 ) {
253 - $( '#timed-text-file-preview' ).text( 'Error the file you selected is too lage');
254 - return ;
255 - }
256 - var srtData = file.getAsBinary();
257 - srtData = srtData.replace( '\r\n', '\n' );
258 - $( '#timed-text-file-preview' ).text( srtData );
259 -
260 - // Update the selected language
261 - var langKey = $(this).val().split( '.' );
262 - var extension = langKey.pop();
263 - langKey = langKey.pop();
264 - if( mw.Language.names[ langKey ] ) {
265 - $buttonTarget.find('.btnText').text(
266 - mw.Language.names[ langKey ]
267 - );
268 - // Update the key code
269 - $('#timed-text-langKey-input').val( langKey );
270 - }
271 - }
272 - });
273 -
274 - //Add an upload button:
275 - $target.append(
276 - $('<div />')
277 - .css('clear', 'both'),
278 - $('<br />'),
279 - $('<br />'),
280 - $.button( {
281 - 'style': { 'float' : 'left' },
282 - 'text': gM('mwe-timedtext-upload-text'),
283 - 'icon': 'disk'
284 - } )
285 - .click( function() {
286 - _this.uploadTextFile();
287 - })
288 - );
289 -
290 - },
291 - /**
292 - * Uploads the text content
293 - */
294 - uploadTextFile: function() {
295 - // Put a dialog ontop
296 - mw.addLoaderDialog( gM( 'mwe-timedtext-uploading-text') );
297 -
298 - // Get timed text target title
299 - // NOTE: this should be cleaned up with accessors
300 - var targetTitleKey = this.parentTimedText.embedPlayer.apiTitleKey;
301 -
302 - // Add TimedText NS and language key and ".srt"
303 - targetTitleKey = 'TimedText:' + targetTitleKey + '.' + $('#timed-text-langKey-input').val() + '.srt';
304 -
305 - // Get a token
306 - mw.getToken( targetTitleKey, function( token ) {
307 - mw.log("got token: " + token);
308 - var request = {
309 - 'action' : 'edit',
310 - 'title' : targetTitleKey,
311 - 'text' : $('#timed-text-file-preview').val(),
312 - 'token': token
313 - };
314 - mw.getJSON( request, function( data ) {
315 - //Close the loader dialog:
316 - mw.closeLoaderDialog();
317 -
318 - if( data.edit && data.edit.result == 'Success' ) {
319 - var buttons = { };
320 - buttons[ gM("mwe-timedtext-upload-text-another")] = function() {
321 - // just close the current dialog:
322 - $( this ).dialog('close');
323 - };
324 - buttons[ gM( "mwe-timedtext-upload-text-done-uploading" ) ] = function() {
325 - window.location.reload();
326 - };
327 - //Edit success
328 - setTimeout(function(){
329 - mw.addDialog( {
330 - 'width' : '400px',
331 - 'title' : gM( "mwe-timedtext-upload-text-done"),
332 - 'content' : gM("mwe-timedtext-upload-text-success"),
333 - 'buttons' : buttons
334 - });
335 - }, 10 );
336 - }else{
337 - //Edit fail
338 - setTimeout(function(){
339 - mw.addDialog({
340 - 'width' : '400px',
341 - 'title' : gM( "mwe-timedtext-upload-text-fail-title"),
342 - 'content' :gM( "mwe-timedtext-upload-text-fail-desc"),
343 - 'buttons' : gM( 'mwe-ok' )
344 - });
345 - },10 );
346 - }
347 - });
348 - })
349 - },
350 -
351 - /**
352 - * Gets the language set.
353 - *
354 - * Checks off languages that area already "loaded" according to parentTimedText
355 - *
356 - * This is cpu intensive function
357 - * Optimize: switch to html string building, insert and bind
358 - * (instead of building html with jquery calls )
359 - * Optimize: pre-sort both language lists and continue checks where we left off
360 - *
361 - * ~ what really a lot of time is putting this ~into~ the dom ~
362 - */
363 - getLanguageList: function() {
364 - var _this = this;
365 - var $langMenu = $( '<ul>' );
366 - // Loop through all supported languages:
367 - for ( var langKey in mw.Language.names ) {
368 - var language = mw.Language.names [ langKey ];
369 - var source_icon = 'radio-on';
370 - //check if the key is in the _this.parentTimedText source array
371 - for( var i in _this.parentTimedText.textSources ) {
372 - var pSource = _this.parentTimedText.textSources[i];
373 - if( pSource.lang == langKey) {
374 - source_icon = 'bullet';
375 - }
376 - }
377 - // call out to "anonymous" function to variable scope the langKey
378 - $langMenu.append(
379 - _this.getLangMenuItem( langKey , source_icon)
380 - );
381 - }
382 - return $langMenu;
383 - },
384 -
385 - getLangMenuItem: function( langKey , source_icon) {
386 - return $.getLineItem(
387 - langKey + ' - ' + mw.Language.names[ langKey ],
388 - source_icon,
389 - function() {
390 - mw.log( "Selected: " + langKey );
391 - // Update the input box text
392 - $('#timed-text-langKey-input').val( langKey );
393 - // Update the menu item:
394 - $('#language-select').find('.btnText').text( mw.Language.names[ langKey ] )
395 - }
396 - );
397 - },
398 - /**
399 - * Creates the interface dialog container
400 - */
401 - createDialogContainer: function() {
402 - var _this = this;
403 - //Setup the target container:
404 - _this.target_container = '#timedTextEdit_target';
405 - $( _this.target_container ).remove();
406 - $( 'body' ).append(
407 - $('<div>')
408 - .attr({
409 - 'id' : 'timedTextEdit_target',
410 - 'title' : gM( 'mwe-timedtext-editor' )
411 - })
412 - .addClass('TimedTextEdit')
413 - );
414 -
415 - // Build cancel button
416 - var cancelButton = {};
417 - var cancelText = gM( 'mwe-cancel' );
418 - cancelButton[ cancelText ] = function() {
419 - _this.onCancelClipEdit();
420 - };
421 -
422 - $( _this.target_container ).dialog( {
423 - bgiframe: true,
424 - autoOpen: true,
425 - width: $(window).width()-50,
426 - height: $(window).height()-50,
427 - position : 'center',
428 - modal: true,
429 - draggable: false,
430 - resizable: false,
431 - buttons: cancelButton,
432 - close: function() {
433 - // @@TODO if we are 'editing' we should confirm they want to exist:
434 - $( this ).parents( '.ui-dialog' ).fadeOut( 'slow' );
435 - }
436 - } );
437 - // set a non-blocking fit window request
438 - setTimeout(function(){
439 - $( _this.target_container ).dialogFitWindow();
440 - },10);
441 -
442 - // Add the window resize hook to keep dialog layout
443 - $( window ).resize( function() {
444 - $( _this.target_container ).dialogFitWindow();
445 - } );
446 -
447 - },
448 -
449 - onCancelClipEdit: function() {
450 - var _this = this;
451 - // Cancel edit
452 - $( _this.target_container ).dialog( 'close' );
453 - }
454 -};
455 -
456 -} )( window.mediaWiki, window.jQuery );
\ No newline at end of file
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js
@@ -0,0 +1,471 @@
 2+/**
 3+ * Base mw.TextSource object
 4+ *
 5+ * @param {Object} source Source object to extend
 6+ * @param {Object} textProvider [Optional] The text provider interface ( to load source from api )
 7+ */
 8+( function( mw, $ ) {
 9+
 10+ mw.TextSource = function( source ) {
 11+ return this.init( source );
 12+ };
 13+ mw.TextSource.prototype = {
 14+
 15+ //The load state:
 16+ loaded: false,
 17+
 18+ // Container for the captions
 19+ // captions include "start", "end" and "content" fields
 20+ captions: [],
 21+
 22+ // The css style for captions ( some file formats specify display types )
 23+ styleCss: {},
 24+
 25+ // The previous index of the timed text served
 26+ // Avoids searching the entire array on time updates.
 27+ prevIndex: 0,
 28+
 29+ /**
 30+ * @constructor Inherits mediaSource from embedPlayer
 31+ * @param {source} Base source element
 32+ * @param {Object} Pointer to the textProvider
 33+ */
 34+ init: function( source , textProvider) {
 35+ // Inherits mediaSource
 36+ for( var i in source){
 37+ this[ i ] = source[ i ];
 38+ }
 39+
 40+ // Set default category to subtitle if unset:
 41+ if( ! this.kind ) {
 42+ this.kind = 'subtitle';
 43+ }
 44+ //Set the textProvider if provided
 45+ if( textProvider ) {
 46+ this.textProvider = textProvider;
 47+ }
 48+ return this;
 49+ },
 50+
 51+ /**
 52+ * Function to load and parse the source text
 53+ * @param {Function} callback Function called once text source is loaded
 54+ */
 55+ load: function( callback ) {
 56+ var _this = this;
 57+ // Setup up a callback ( in case it was not defined )
 58+ if( !callback ){
 59+ callback = function(){ return ; };
 60+ }
 61+
 62+ // Check if the captions have already been loaded:
 63+ if( this.loaded ){
 64+ return callback();
 65+ }
 66+
 67+ // Try to load src via XHR source
 68+ if( !this.getSrc() ) {
 69+ mw.log( "Error: TextSource no source url for text track");
 70+ return callback();
 71+ }
 72+ try {
 73+ $.ajax({
 74+ url: _this.getSrc(),
 75+ success: function( data ) {
 76+ _this.captions = _this.getCaptions( data );
 77+ _this.loaded = true;
 78+ mw.log("mw.TextSource :: loaded from " + _this.getSrc() + " Found: " + _this.captions.length + ' captions' );
 79+ callback();
 80+ },
 81+ error: function( jqXHR, textStatus, errorThrown ){
 82+ // try to load the file with the proxy:
 83+ _this.loadViaProxy( function(){
 84+ callback();
 85+ _this.loaded = true;
 86+ });
 87+ }
 88+ });
 89+ } catch ( e ){
 90+ mw.log( "TimedText source:: first cross domain request failed, trying via proxy" );
 91+ }
 92+ },
 93+ loadViaProxy: function( callback ){
 94+ var _this = this;
 95+ // Load via proxy:
 96+ var proxyUrl = mw.getConfig('Mw.XmlProxyUrl');
 97+ $.getJSON( proxyUrl + '?url=' + encodeURIComponent( this.getSrc() ) + '&callback=?', function( result ){
 98+ if( result['http_code'] == 'ERROR' || result['http_code'] == 0 ){
 99+ mw.log("Error: TextSource Error with http response");
 100+ return callback();
 101+ }
 102+ // Parse and load captions:
 103+ _this.captions = _this.getCaptions( result['contents'] );
 104+ mw.log("mw.TextSource :: loaded from proxy xml request: captions length: " + _this.captions.length + ' captions' );
 105+ callback();
 106+ });
 107+ },
 108+ /**
 109+ * Returns the text content for requested time
 110+ *
 111+ * @param {Number} time Time in seconds
 112+ */
 113+ getCaptionForTime: function ( time ) {
 114+ var prevCaption = this.captions[ this.prevIndex ];
 115+ var captionSet = {};
 116+
 117+ // Setup the startIndex:
 118+ if( prevCaption && time >= prevCaption.start ) {
 119+ var startIndex = this.prevIndex;
 120+ }else{
 121+ // If a backwards seek start searching at the start:
 122+ var startIndex = 0;
 123+ }
 124+ var firstCapIndex = 0;
 125+ // Start looking for the text via time, add all matches that are in range
 126+ for( var i = startIndex ; i < this.captions.length; i++ ) {
 127+ var caption = this.captions[ i ];
 128+ // Don't handle captions with 0 or -1 end time:
 129+ if( caption.end == 0 || caption.end == -1)
 130+ continue;
 131+
 132+ if( time >= caption.start &&
 133+ time <= caption.end ) {
 134+ // set the earliest valid time to the current start index:
 135+ if( !firstCapIndex ){
 136+ firstCapIndex = caption.start;
 137+ }
 138+
 139+ //mw.log("Start cap time: " + caption.start + ' End time: ' + caption.end );
 140+ captionSet[i] = caption ;
 141+ }
 142+ // captions are stored in start order stop search if we get larger than time
 143+ if( caption.start > time ){
 144+ break;
 145+ }
 146+ }
 147+ // Update the prevIndex:
 148+ this.prevIndex =firstCapIndex;
 149+ //Return the set of captions in range:
 150+ return captionSet;
 151+ },
 152+ getCaptions: function( data ){
 153+ // Detect caption data type:
 154+ switch( this.mimeType ){
 155+ case 'text/mw-srt':
 156+ return this.getCaptiosnFromMediaWikiSrt( data );
 157+ break;
 158+ case 'text/x-srt':
 159+ return this.getCaptionsFromSrt( data);
 160+ break;
 161+ case 'text/xml':
 162+ return this.getCaptionsFromTMML( data );
 163+ break;
 164+ }
 165+ },
 166+
 167+ getStyleCssById: function( styleId ){
 168+ if( this.styleCss[ styleId ] ){
 169+ return this.styleCss[ styleId ];
 170+ }
 171+ return {};
 172+ },
 173+ /**
 174+ * Grab timed text from TMML format
 175+ *
 176+ * @param data
 177+ * @return
 178+ */
 179+ getCaptionsFromTMML: function( data ){
 180+ var _this = this;
 181+ mw.log("TextSource::getCaptionsFromTMML", data);
 182+ // set up display information:
 183+ var captions = [];
 184+ var xml = ( $( data ).find("tt").length ) ? data : $.parseXML( data );
 185+
 186+ // Check for parse error:
 187+ try {
 188+ if( !xml || $( xml ).find('parsererror').length ){
 189+ mw.log("Error: close caption parse error: " + $( xml ).find('parsererror').text() );
 190+ return captions;
 191+ }
 192+ } catch ( e ) {
 193+ mw.log( "Error: close caption parse error: " + e.toString() );
 194+ return captions;
 195+ }
 196+
 197+ // Set the body Style
 198+ var bodyStyleId = $( xml ).find('body').attr('style');
 199+
 200+ // Set style translate ttml to css
 201+ $( xml ).find( 'style').each( function( inx, style){
 202+ var cssObject = {};
 203+ // Map CamelCase css properties:
 204+ $( style.attributes ).each(function(inx, attr){
 205+ var attrName = attr.name;
 206+ if( attrName.substr(0, 4) !== 'tts:' ){
 207+ // skip
 208+ return true;
 209+ }
 210+ var cssName = '';
 211+ for( var c = 4; c < attrName.length; c++){
 212+ if( attrName[c].toLowerCase() != attrName[c] ){
 213+ cssName += '-' + attrName[c].toLowerCase();
 214+ } else {
 215+ cssName+= attrName[c]
 216+ }
 217+ }
 218+ cssObject[ cssName ] = attr.nodeValue;
 219+ });
 220+ //for(var i =0; i< style.length )
 221+ _this.styleCss[ $( style).attr('id') ] = cssObject;
 222+ });
 223+
 224+ $( xml ).find( 'p' ).each( function( inx, p ){
 225+
 226+ // Get text content ( just a quick hack, we need more detailed spec or TTML parser )
 227+ var content = '';
 228+ $( p.childNodes ).each(function(inx,node){
 229+ if( node.nodeName != '#text' && node.nodeName != 'metadata' ){
 230+ // Add any html tags:
 231+ content +='<' + node.nodeName + '/>';
 232+ } else {
 233+ content += node.textContent;
 234+ }
 235+ });
 236+
 237+ // Get the end time:
 238+ var end = null;
 239+ if( $( p ).attr( 'end' ) ){
 240+ end = mw.npt2seconds( $( p ).attr( 'end' ) );
 241+ }
 242+ // Look for dur
 243+ if( !end && $( p ).attr( 'dur' )){
 244+ end = mw.npt2seconds( $( p ).attr( 'begin' ) ) +
 245+ mw.npt2seconds( $( p ).attr( 'dur' ) );
 246+ }
 247+
 248+ // Create the caption object :
 249+ var captionObj ={
 250+ 'start': mw.npt2seconds( $( p ).attr( 'begin' ) ),
 251+ 'end': end,
 252+ 'content': content
 253+ };
 254+
 255+ // See if we have custom metadata for position of this caption object
 256+ // there are 35 columns across and 15 rows high
 257+ var $meta = $(p).find( 'metadata' );
 258+ if( $meta.length ){
 259+ captionObj['css'] = {
 260+ 'position': 'absolute'
 261+ };
 262+ if( $meta.attr('cccol') ){
 263+ captionObj['css']['left'] = ( $meta.attr('cccol') / 35 ) * 100 +'%';
 264+ // also means the width has to be reduced:
 265+ captionObj['css']['width'] = 100 - parseInt( captionObj['css']['left'] ) + '%';
 266+ }
 267+ if( $meta.attr('ccrow') ){
 268+ captionObj['css']['top'] = ( $meta.attr('ccrow') / 15 ) * 100 +'%';
 269+ }
 270+ }
 271+ if( $(p).attr('tts:textAlign') ){
 272+ if( !captionObj['css'] )
 273+ captionObj['css'] = {};
 274+ captionObj['css']['text-align'] = $(p).attr('tts:textAlign');
 275+
 276+ // Remove text align is "right" flip the css left:
 277+ if( captionObj['css']['text-align'] == 'right' && captionObj['css']['left'] ){
 278+ captionObj['css']['width'] = captionObj['css']['left'];
 279+ captionObj['css']['left'] = null;
 280+ }
 281+ }
 282+
 283+ // check if this p has any style else use the body parent
 284+ if( $(p).attr('style') ){
 285+ captionObj['styleId'] = $(p).attr('style') ;
 286+ } else {
 287+ captionObj['styleId'] = bodyStyleId;
 288+ }
 289+ captions.push( captionObj);
 290+ });
 291+ return captions;
 292+ },
 293+ /**
 294+ * srt timed text parse handle:
 295+ * @param {String} data Srt string to be parsed
 296+ */
 297+ getCaptionsFromSrt: function ( data ){
 298+ mw.log("TextSource::getCaptionsFromSrt");
 299+ var _this = this;
 300+ // Check if the "srt" parses as an XML
 301+ try{
 302+ var xml = $.parseXML( data );
 303+ if( xml && $( xml ).find('parsererror').length == 0 ){
 304+ return this.getCaptionsFromTMML( data );
 305+ }
 306+ } catch ( e ){
 307+ // srt should not be xml
 308+ }
 309+ // Remove dos newlines
 310+ var srt = data.replace(/\r+/g, '');
 311+
 312+ // Trim white space start and end
 313+ srt = srt.replace(/^\s+|\s+$/g, '');
 314+
 315+ // Remove all html tags for security reasons
 316+ srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, '');
 317+
 318+ // Get captions
 319+ var captions = [];
 320+ var caplist = srt.split('\n\n');
 321+ for (var i = 0; i < caplist.length; i++) {
 322+ var captionText = "";
 323+ var caption = false;
 324+ captionText = caplist[i];
 325+ s = captionText.split(/\n/);
 326+ if (s.length < 2) {
 327+ // file format error or comment lines
 328+ continue;
 329+ }
 330+ if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) {
 331+ // ignore caption number in s[0]
 332+ // parse time string
 333+ var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/);
 334+ if (m) {
 335+ caption = _this.match2caption( m );
 336+ } else {
 337+ // Unrecognized timestring
 338+ continue;
 339+ }
 340+ if( caption ){
 341+ // concatenate text lines to html text
 342+ caption['content'] = s.slice(2).join("<br>");
 343+ }
 344+ } else {
 345+ // file format error or comment lines
 346+ continue;
 347+ }
 348+ // Add the current caption to the captions set:
 349+ captions.push( caption );
 350+ }
 351+
 352+ return captions;
 353+ },
 354+
 355+ /**
 356+ * Get srts from a mediawiki html / srt string
 357+ *
 358+ * Right now wiki -> html is not always friendly to our srt parsing.
 359+ * The long term plan is to move the srt parsing to server side and have the api
 360+ * server up the srt's times in JSON form
 361+ *
 362+ * Also see https://bugzilla.wikimedia.org/show_bug.cgi?id=29126
 363+ *
 364+ * TODO move to mediaWiki specific module.
 365+ */
 366+ getCaptiosnFromMediaWikiSrt: function( data ){
 367+ var _this = this;
 368+ var captions = [ ];
 369+ var curentCap = {
 370+ 'content': ''
 371+ };
 372+ var parseNextAsTime = false;
 373+ // Note this string concatenation and html error wrapping sometimes causes
 374+ // parse issues where the wikitext includes many native <p /> tags without child
 375+ // subtitles. In prating this is not a deal breakers because the wikitext for
 376+ // TimedText namespace and associated srts already has a specific format.
 377+ // Long term we will move to server side parsing.
 378+ $( '<div>' + data + '</div>' ).find('p').each( function() {
 379+ var currentPtext = $(this).html();
 380+ //mw.log( 'pText: ' + currentPtext );
 381+
 382+ // We translate raw wikitext gennerated html into a matched srt time sample.
 383+ // The raw html looks like:
 384+ // #
 385+ // hh:mm:ss,ms --&gt hh:mm:ss,ms
 386+ // text
 387+ //
 388+ // You can read more about the srt format here:
 389+ // http://en.wikipedia.org/wiki/SubRip
 390+ //
 391+ // We attempt to be fairly robust in our regular expression to catch a few
 392+ // srt variations such as omition of commas and empty text lines.
 393+ var m = currentPtext
 394+ .replace('--&gt;', '-->') // restore --&gt with --> for easier srt parsing:
 395+ .match(/\d+\s([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\s*--?>\s*([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\n?(.*)/);
 396+
 397+ if (m) {
 398+ captions.push(
 399+ _this.match2caption( m )
 400+ );
 401+ return true;
 402+ }
 403+
 404+ /***
 405+ * Handle multi line sytle output
 406+ *
 407+ * Handles cases parse cases where an entire line can't be parsed in the single
 408+ * regular expression above, Since the diffrent captions pars are outputed in
 409+ * diffrent <p /> tags by the wikitext parser output.
 410+ */
 411+
 412+ // Check if we have reached the end of a multi line match
 413+ if( parseInt( currentPtext ) == currentPtext ) {
 414+ if( curentCap.content != '' ) {
 415+ captions.push( curentCap );
 416+ }
 417+ // Clear out the current caption content
 418+ curentCap = {
 419+ 'content': ''
 420+ };
 421+ return true;
 422+ }
 423+ // Check only for time match:
 424+ var m = currentPtext
 425+ .replace('--&gt;', '-->')
 426+ .match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/);
 427+ if (m) {
 428+ // Update the currentCap:
 429+ curentCap = _this.match2caption( m );
 430+ return true;
 431+ }
 432+ // Else append contnet for the curentCap
 433+ if( currentPtext != '<br>' ) {
 434+ curentCap['content'] += currentPtext;
 435+ }
 436+ });
 437+ //Push last subtitle:
 438+ if( curentCap.length != 0) {
 439+ captions.push( curentCap );
 440+ }
 441+ return captions;
 442+ },
 443+ /**
 444+ * Takes a regular expresion match and converts it to a caption object
 445+ */
 446+ match2caption: function( m ){
 447+ var caption = {};
 448+ // Look for ms:
 449+ var startMs = (m[4])? (parseInt(m[4], 10) / 1000): 0;
 450+ var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0;
 451+ caption['start'] = this.timeParts2seconds( m[1], m[2], m[3], startMs );
 452+ caption['end'] = this.timeParts2seconds( m[5], m[6], m[7], endMs );
 453+ if( m[9] ){
 454+ caption['content'] = $.trim( m[9] );
 455+ }
 456+ return caption;
 457+ },
 458+ /**
 459+ * Takes time parts in hours, min, seconds and milliseconds and coverts to float seconds.
 460+ */
 461+ timeParts2seconds: function( hours, min, sec, ms ){
 462+ return mw.measurements2seconds({
 463+ 'hours': hours,
 464+ 'minutes': min,
 465+ 'seconds' : sec,
 466+ 'milliseconds': ms
 467+ });
 468+ }
 469+ };
 470+
 471+
 472+} )( window.mediaWiki, window.jQuery );
\ No newline at end of file
Property changes on: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js
___________________________________________________________________
Added: svn:mime-type
1473 + text/plain
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js
@@ -10,16 +10,15 @@
1111 * @author: Michael Dale
1212 *
1313 */
 14+mw.includeAllModuleMessages();
1415
15 -// Bind to mw ( for uncluttered global namespace )
1616 ( function( mw, $ ) {
1717
1818 // Merge in timed text related attributes:
1919 mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [
2020 'srclang',
21 - 'category',
22 - 'label',
23 - 'data-mwtitle'
 21+ 'kind',
 22+ 'label'
2423 ]);
2524
2625 /**
@@ -42,16 +41,21 @@
4342 'layout' : 'ontop',
4443
4544 //Set the default local ( should be grabbed from the browser )
46 - 'userLanugage' : 'en',
 45+ 'userLanguage' : 'en',
4746
48 - //Set the default category of timedText to display ( un-categorized timed-text is by default "SUB" )
49 - 'userCategory' : 'SUB'
 47+ //Set the default kind of timedText to display ( un-categorized timed-text is by default "subtitles" )
 48+ 'userKind' : 'subtitles'
5049 },
51 -
 50+ // The bind prefix:
 51+ bindPostFix: '.timedText',
 52+
 53+ // Default options are empty
 54+ options: {},
 55+
5256 /**
5357 * The list of enabled sources
5458 */
55 - enabledSources: null,
 59+ enabledSources: [],
5660
5761 /**
5862 * The current language key
@@ -59,10 +63,10 @@
6064 currentLangKey : null,
6165
6266 /**
63 - * Stores the last text string per category to avoid dom checks
 67+ * Stores the last text string per kind to avoid dom checks
6468 * for updated text
6569 */
66 - prevText: null,
 70+ prevText: [],
6771
6872 /**
6973 * Text sources ( a set of textSource objects )
@@ -70,17 +74,6 @@
7175 textSources: null,
7276
7377 /**
74 - * Text Source(s) Setup Flag
75 - */
76 - textSourceSetupFlag: null,
77 -
78 - /*
79 - * Hard coded to "commons" right now .. but we will want to support per-asset provider id's
80 - * in addition to a standard "callback" system from cross domain grabbing of srt's
81 - */
82 - textProviderId : 'commons',
83 -
84 - /**
8578 * Valid "Track" categories
8679 */
8780 validCategoriesKeys: [
@@ -99,113 +92,122 @@
10093 ],
10194
10295 /**
103 - * Timed text extension to mime map
104 - */
105 - timedTextExtMime: {
106 - 'srt': 'text/x-srt',
107 - 'mw-srt': 'text/mw-srt',
108 - 'cmml': 'text/cmml'
109 - },
110 -
111 - /**
11296 * @constructor
11397 * @param {Object} embedPlayer Host player for timedText interfaces
11498 */
11599 init: function( embedPlayer, options ) {
116100 var _this = this;
117101 mw.log("TimedText: init() ");
118 - this.embedPlayer = embedPlayer;
119 - this.options = options;
120 -
121 - //Init internal variables:
122 - this.enabledSources = [];
123 - this.prevText = '';
124 - this.textSources = [];
125 - this.textSourceSetupFlag = false;
126 - // Set default language via wgUserLanguage if set
127 - if( typeof wgUserLanguage == 'object' ) {
128 - this.config.userLanugage = wgUserLanguage;
 102+ this.embedPlayer = embedPlayer;
 103+ if( options ){
 104+ this.options = options;
129105 }
130 -
131106 // Load user preferences config:
132107 var preferenceConfig = $.cookie( 'TimedText.Preferences' );
133 - if( preferenceConfig !== null ) {
 108+ if( preferenceConfig !== "false" && preferenceConfig != null ) {
134109 this.config = JSON.parse( preferenceConfig );
135110 }
136 - // Set up embedPlayer hooks:
 111+ // remove any old bindings on change media:
 112+ $( this.embedPlayer ).bind('onChangeMedia', function(){
 113+ _this.destroy();
 114+ });
 115+ // Remove any old bindings before we add the current bindings:
 116+ _this.destroy();
 117+ // Add player bindings
 118+ this.addPlayerBindings();
 119+ },
 120+ destroy:function(){
 121+ // remove any old player bindings;
 122+ $( this.embedPlayer ).unbind( this.bindPostFix )
 123+ },
 124+ /**
 125+ * Add timed text related player bindings
 126+ * @return
 127+ */
 128+ addPlayerBindings: function(){
 129+ var _this = this;
 130+ var embedPlayer = this.embedPlayer;
137131
138132 // Check for timed text support:
139 - $( embedPlayer ).bind( 'addControlBarComponent', function(event, controlBar ){
140 - if( mw.isTimedTextSupported( embedPlayer ) ){
 133+ $( embedPlayer ).bind( 'addControlBarComponent' + this.bindPostFix, function(event, controlBar ){
 134+ if( embedPlayer.hasTextTracks() ){
141135 controlBar.supportedComponents['timedText'] = true;
142136 controlBar.components['timedText'] = _this.getTimedTextButton();
143137 }
144138 });
145139
146 -
147 - $( embedPlayer ).bind( 'monitorEvent', function() {
 140+ $( embedPlayer ).bind( 'monitorEvent'+ this.bindPostFix, function() {
148141 _this.monitor();
149142 } );
150143
151 - $( embedPlayer ).bind( 'onplay', function() {
 144+ $( embedPlayer ).bind( 'onplay'+ this.bindPostFix, function() {
152145 // Will load and setup timedText sources (if not loaded already loaded )
153146 _this.setupTextSources();
 147+ // Hide the caption menu if presently displayed
 148+ $( '#textMenuContainer_' + embedPlayer.id ).parent().remove();
154149 } );
155150
156151 // Resize the timed text font size per window width
157 - $( embedPlayer ).bind( 'onCloseFullScreen onOpenFullScreen', function() {
158 - var textOffset = _this.embedPlayer.controlBuilder.fullscreenMode ? 30 : 10;
159 -
160 - mw.log( 'TimedText::set text size for: : ' + embedPlayer.$interface.width() + ' = ' + _this.getInterfaceSizeTextCss({
 152+ $( embedPlayer ).bind( 'onCloseFullScreen'+ this.bindPostFix + ' onOpenFullScreen'+ this.bindPostFix, function() {
 153+ // Check if we are in fullscreen or not, if so add an additional bottom offset of
 154+ // double the default bottom padding.
 155+ var textOffset = _this.embedPlayer.controlBuilder.fullscreenMode ?
 156+ mw.getConfig("TimedText.BottomPadding") *2 :
 157+ mw.getConfig("TimedText.BottomPadding");
 158+
 159+ var textCss = _this.getInterfaceSizeTextCss({
161160 'width' : embedPlayer.$interface.width(),
162161 'height' : embedPlayer.$interface.height()
163 - })['font-size'] );
 162+ });
164163
165 - embedPlayer.$interface.find( '.track' ).css( _this.getInterfaceSizeTextCss({
166 - 'width' : embedPlayer.$interface.width(),
167 - 'height' : embedPlayer.$interface.height()
168 - }) ).css({
169 - // Get the text size scale then set it to control bar height + 10 px;
 164+ mw.log( 'TimedText::set text size for: : ' + embedPlayer.$interface.width() + ' = ' + textCss['font-size'] );
 165+
 166+ embedPlayer.$interface.find( '.track' )
 167+ .css( textCss )
 168+ .css({
 169+ // Get the text size scale then set it to control bar height + TimedText.BottomPadding;
170170 'bottom': ( _this.embedPlayer.controlBuilder.getHeight() + textOffset ) + 'px'
171171 });
172 -
173172 });
174173
175174 // Update the timed text size
176 - $( embedPlayer ).bind( 'onResizePlayer', function(e, size, animate) {
177 - mw.log( 'TimedText::onResizePlayer: ' + _this.getInterfaceSizeTextCss(size)['font-size'] );
178 - if (animate) {
179 - embedPlayer.$interface.find( '.track' ).animate( _this.getInterfaceSizeTextCss( size ) );
 175+ $( embedPlayer ).bind( 'onResizePlayer'+ this.bindPostFix, function(event, size, animate) {
 176+ // If the the player resize action is an animation, animate text resize,
 177+ // else instantly adjust the css.
 178+ var textCss = _this.getInterfaceSizeTextCss( size );
 179+ mw.log( 'TimedText::onResizePlayer: ' + textCss['font-size']);
 180+ if ( animate ) {
 181+ embedPlayer.$interface.find( '.track' ).animate( textCss);
180182 } else {
181 - embedPlayer.$interface.find( '.track' ).css( _this.getInterfaceSizeTextCss( size ) );
 183+ embedPlayer.$interface.find( '.track' ).css( textCss );
182184 }
183185 });
184186
185187 // Setup display binding
186 - $( embedPlayer ).bind( 'onShowControlBar', function(event, layout ){
 188+ $( embedPlayer ).bind( 'onShowControlBar'+ this.bindPostFix, function(event, layout ){
187189 // Move the text track if present
188190 embedPlayer.$interface.find( '.track' )
189191 .stop()
190192 .animate( layout, 'fast' );
191193 });
192194
193 - $( embedPlayer ).bind( 'onHideControlBar', function(event, layout ){
 195+ $( embedPlayer ).bind( 'onHideControlBar'+ this.bindPostFix, function(event, layout ){
194196 // Move the text track down if present
195197 embedPlayer.$interface.find( '.track' )
196198 .stop()
197199 .animate( layout, 'fast' );
198200 });
199 -
200201 },
 202+
201203 /**
202204 * Get the current language key
203 - *
204205 * @return
205206 * @type {string}
206207 */
207208 getCurrentLangKey: function(){
208209 return this.currentLangKey;
209210 },
 211+
210212 /**
211213 * The timed text button to be added to the interface
212214 */
@@ -249,6 +251,7 @@
250252 'font-size' : this.getInterfaceSizePercent( size ) + '%'
251253 };
252254 },
 255+
253256 /**
254257 * Show the text interface library and show the text interface near the player.
255258 */
@@ -258,7 +261,6 @@
259262 mw.log('showTextInterface::' + embedPlayer.id + ' t' + loc.top + ' r' + loc.right);
260263
261264 var $menu = $( '#timedTextMenu_' + embedPlayer.id );
262 - //This may be unnecessary .. we just need to show a spinner somewhere
263265 if ( $menu.length != 0 ) {
264266 // Hide show the menu:
265267 if( $menu.is( ':visible' ) ) {
@@ -267,12 +269,19 @@
268270 // move the menu to proper location
269271 $menu.show("fast");
270272 }
271 - }else{
 273+ }else{
 274+ // Bind the text menu:
 275+ this.bindMenu( true );
 276+ }
 277+ },
 278+ getTextMenuContainer: function(){
 279+ var textMenuId = 'textMenuContainer_' + this.embedPlayer.id;
 280+ if( !$( '#' + textMenuId ).length ){
272281 //Setup the menu:
273282 $('body').append(
274283 $('<div>')
275284 .addClass('ui-widget ui-widget-content ui-corner-all')
276 - .attr( 'id', 'timedTextMenu_' + embedPlayer.id )
 285+ .attr( 'id', textMenuId )
277286 .css( {
278287 'position' : 'absolute',
279288 'z-index' : 10,
@@ -281,17 +290,25 @@
282291 'font-size' : '12px',
283292 'display' : 'none'
284293 } )
285 -
 294+
286295 );
287 - // Load text interface ( if not already loaded )
288 - $( '#' + embedPlayer.id ).timedText( 'showMenu', '#timedTextMenu_' + embedPlayer.id );
289296 }
 297+ return $( '#' + textMenuId );
290298 },
 299+ /**
 300+ * Gets a text size percent relative to about 30 columns of text for 400
 301+ * pixel wide player, at 100% text size.
 302+ *
 303+ * @param size {object} The size of the target player area width and height
 304+ */
291305 getInterfaceSizePercent: function( size ) {
292 - // Some arbitrary scale relative to window size ( 400px wide is text size 105% )
293 - var textSize = size.width / 3.8;
294 - if( textSize < 95 ) textSize = 95;
295 - if( textSize > 200 ) textSize = 200;
 306+ var textSize = size.width / 4;
 307+ if( textSize < 95 ){
 308+ textSize = 95;
 309+ }
 310+ if( textSize > 200 ){
 311+ textSize = 200;
 312+ }
296313 return textSize;
297314 },
298315
@@ -304,17 +321,8 @@
305322 setupTextSources: function( callback ) {
306323 mw.log( 'mw.TimedText::setupTextSources');
307324 var _this = this;
308 - if( this.textSourceSetupFlag ) {
309 - if( callback ) {
310 - callback();
311 - }
312 - return ;
313 - }
314 - this.textSourceSetupFlag = true;
315 -
316325 // Load textSources
317326 _this.loadTextSources( function() {
318 -
319327 // Enable a default source and issue a request to "load it"
320328 _this.autoSelectSource();
321329
@@ -334,10 +342,8 @@
335343 * @param {Object} target to display the menu
336344 * @param {Boolean} autoShow If the menu should be displayed
337345 */
338 - bindMenu: function( target , autoShow) {
 346+ bindMenu: function( autoShow) {
339347 var _this = this;
340 - mw.log( "TimedText:bindMenu:" + target );
341 - _this.menuTarget = target;
342348 var $menuButton = this.embedPlayer.$interface.find( '.timed-text' );
343349
344350 var positionOpts = { };
@@ -360,7 +366,7 @@
361367 'zindex' : mw.getConfig( 'EmbedPlayer.FullScreenZIndex' ) + 2,
362368 'crumbDefaultText' : ' ',
363369 'autoShow': autoShow,
364 - 'targetMenuContainer' : _this.menuTarget,
 370+ 'targetMenuContainer' : _this.getTextMenuContainer(),
365371 'positionOpts' : positionOpts,
366372 'backLinkText' : gM( 'mwe-timedtext-back-btn' ),
367373 'createMenuCallback' : function(){
@@ -382,107 +388,43 @@
383389 // Setup local reference to currentTime:
384390 var currentTime = embedPlayer.currentTime;
385391
386 - // Get the text per category
 392+ // Get the text per kind
387393 var textCategories = [ ];
388394
389 - for( var i = 0; i < this.enabledSources.length ; i++ ) {
390 - var source = this.enabledSources[ i ];
391 - this.updateSourceDisplay( source, currentTime );
392 - }
 395+ //for( var i = 0; i < this.enabledSources.length ; i++ ) {
 396+ //var source = this.enabledSources[ i ];
 397+ var source = this.enabledSources[ 0 ];
 398+ if( source ) {
 399+ this.updateSourceDisplay( source, currentTime );
 400+ }
 401+ //}
393402 },
394403
395404 /**
396405 * Load all the available text sources from the inline embed
397 - * or from a apiProvider
398406 * @param {Function} callback Function to call once text sources are loaded
399407 */
400408 loadTextSources: function( callback ) {
401409 var _this = this;
402 - this.textSources = [];
403 -
404 - // Setup the provider id ( set to local if not found )
405 - var providerId = $( this.embedPlayer ).attr('data-mwprovider') ?
406 - $( this.embedPlayer ).attr('data-mwprovider') :
407 - 'local';
408 - var apiUrl = mw.getApiProviderURL( providerId );
409 - var apiTitleKey = this.embedPlayer.apiTitleKey;
410 - if( !apiUrl || !apiTitleKey ) {
411 - mw.log("Error: loading source without apiProvider or apiTitleKey");
 410+ // check if text sources are already loaded ( not null )
 411+ if( this.textSources !== null ){
 412+ callback( this.textSources );
412413 return ;
413414 }
414 -
415 - //For now only support mediaWikTrack provider library
416 - this.textProvider = new mw.MediaWikTrackProvider( {
417 - 'providerId' : providerId,
418 - 'apiUrl': apiUrl,
419 - 'embedPlayer': this.embedPlayer
420 - } );
421 -
422 - // Get local reference to all timed text sources: ( text/xml, text/x-srt etc )
423 - var inlineSources = this.embedPlayer.mediaElement.getSources( 'text' );
424 -
425 - // Add all the sources to textSources
426 - for( var i = 0 ; i < inlineSources.length ; i++ ) {
427 - // Check if the inline source has a text provider:
428 - var textSourceProvider = $( this.embedPlayer ).attr('data-mwprovider') ?
429 - $( this.embedPlayer ).attr('data-mwprovider') :
430 - this.textProvider;
431 -
432 - // Make a new textSource:
433 - var source = new TextSource( inlineSources[i] , this.textProvider);
434 - this.textSources.push( source);
435 - }
436 -
437 - // If there are inline sources don't check the api )
438 - if( this.textSources.length != 0 ){
439 - if( callback )
440 - callback();
441 - return ;
442 - }
443 -
444 - // Load the textProvider sources
445 - this.textProvider.loadSources( apiTitleKey, function( textSources ) {
446 - for( var i=0; i < textSources.length; i++ ) {
447 - var textSource = textSources[ i ];
448 - // Try to insert the track source:
449 - var textElm = document.createElement( 'track' );
450 - $( textElm ).attr({
451 - 'category' : 'SUB',
452 - 'srclang' : textSource.srclang,
453 - 'type' : _this.timedTextExtMime[ textSource.extension ],
454 - 'titleKey' : textSource.titleKey
455 - });
456 -
457 - // Build the url for downloading the text:
458 - $( textElm ).attr('src',
459 - _this.textProvider.apiUrl.replace('api.php', 'index.php?title=') +
460 - encodeURIComponent( textSource.titleKey ) + '&action=raw&ctype=text/x-srt'
461 - );
462 -
463 - // Add a title
464 - $( textElm ).attr('title',
465 - gM('mwe-timedtext-key-language', textSource.srclang, mw.Language.names[ textSource.srclang ] )
466 - );
467 -
468 - // Add the sources to the parent embedPlayer
469 - // ( in case other interfaces want to access them )
470 - var embedSource = _this.embedPlayer.mediaElement.tryAddSource( textElm );
471 -
472 - // Get a "textSource" object:
473 - var source = new TextSource( embedSource, _this.textProvider );
474 - _this.textSources.push( source );
475 - }
476 - // All sources loaded run callback:
477 - if( callback )
478 - callback();
479 - } );
 415+ this.textSources = [];
 416+ // load inline text sources:
 417+ $.each( this.embedPlayer.getTextTracks(), function( inx, textSource ){
 418+ _this.textSources.push( new mw.TextSource( textSource ) );
 419+ });
 420+ // return the callback with sources
 421+ callback( _this.textSources );
480422 },
481423
482424 /**
483425 * Get the layout mode
484426 *
485427 * Takes into consideration:
486 - * Playback method overlays support ( have to put subtitles bellow video )
 428+ * Playback method overlays support ( have to put subtitles below video )
487429 *
488430 */
489431 getLayoutMode: function() {
@@ -500,23 +442,32 @@
501443 * In the future we could support multiple "enabled sources"
502444 */
503445 autoSelectSource: function() {
 446+ var _this = this;
504447 this.enabledSources = [];
505 - // Check if any source matches our "local"
506 - for( var i=0; i < this.textSources.length; i++ ) {
507 - var source = this.textSources[ i ];
508 - if( this.config.userLanugage &&
509 - this.config.userLanugage == source.srclang.toLowerCase() ) {
510 - // Check for category if available
511 - this.enableSource( source );
 448+ // Check if any source matches our "local" pref
 449+ $.each( this.textSources, function(inx, source){
 450+ if( _this.config.userLanguage == source.srclang.toLowerCase()
 451+ &&
 452+ _this.config.userKind == source.kind
 453+ ) {
 454+ _this.enableSource( source );
512455 return ;
513456 }
514 - }
 457+ });
 458+ // Check if any source is marked default:
 459+ $.each( this.textSources, function(inx, source){
 460+ if( source['default'] ){
 461+ _this.enableSource( source );
 462+ return ;
 463+ }
 464+ });
 465+
515466 // If no userLang, source try enabling English:
516467 if( this.enabledSources.length == 0 ) {
517468 for( var i=0; i < this.textSources.length; i++ ) {
518469 var source = this.textSources[ i ];
519470 if( source.srclang.toLowerCase() == 'en' ) {
520 - this.enableSource( source );
 471+ _this.enableSource( source );
521472 return ;
522473 }
523474 }
@@ -525,14 +476,14 @@
526477 if( this.enabledSources.length == 0 ) {
527478 for( var i=0; i < this.textSources.length; i++ ) {
528479 var source = this.textSources[ i ];
529 - this.enableSource( source );
 480+ _this.enableSource( source );
530481 return ;
531482 }
532483 }
533484 },
534485 /**
535486 * Enable a source and update the currentLangKey
536 - * @param source
 487+ * @param {object} source
537488 * @return
538489 */
539490 enableSource: function( source ){
@@ -540,12 +491,15 @@
541492 this.currentLangKey = source.srclang;
542493 },
543494
544 - // Get the current source sub captions
545 - loadCurrentSubSrouce: function( callback ){
546 - mw.log("loadCurrentSubSrouce:: enabled source:" + this.enabledSources.length);
 495+ /**
 496+ * Get the current source sub captions
 497+ * @param {function} callback function called once source is loaded
 498+ */
 499+ loadCurrentSubSource: function( callback ){
 500+ mw.log("loadCurrentSubSource:: enabled source:" + this.enabledSources.length);
547501 for( var i =0; i < this.enabledSources.length; i++ ){
548502 var source = this.enabledSources[i];
549 - if( source.category == 'SUB' ){
 503+ if( source.kind == 'SUB' ){
550504 source.load( function(){
551505 callback( source);
552506 return ;
@@ -555,11 +509,16 @@
556510 return false;
557511 },
558512
559 - // Get sub captions by language key:
 513+ /**
 514+ * Get sub captions by language key:
 515+ *
 516+ * @param {string} langKey Key of captions to load
 517+ * @pram {function} callback function called once language key is loaded
 518+ */
560519 getSubCaptions: function( langKey, callback ){
561520 for( var i=0; i < this.textSources.length; i++ ) {
562521 var source = this.textSources[ i ];
563 - if( source.srclang.toLowerCase() == langKey ) {
 522+ if( source.srclang.toLowerCase() === langKey ) {
564523 var source = this.textSources[ i ];
565524 source.load( function(){
566525 callback( source.captions );
@@ -573,11 +532,9 @@
574533 * Should be called anytime enabled Source list is updated
575534 */
576535 loadEnabledSources: function() {
577 - for(var i=0; i < this.enabledSources.length; i++ ) {
578 - var enabledSource = this.enabledSources[ i ];
579 - if( ! enabledSource.loaded )
580 - enabledSource.load();
581 - }
 536+ $.each( this.enabledSources, function( inx, enabledSource ) {
 537+ enabledSource.load();
 538+ });
582539 },
583540
584541 /**
@@ -587,6 +544,7 @@
588545 */
589546 selectMenuItem: function( item ) {
590547 mw.log("selectMenuItem: " + $( item ).find('a').attr('class') );
 548+ //this.currentLangKey = ''
591549 },
592550
593551 /**
@@ -596,28 +554,31 @@
597555 * false if source is off
598556 */
599557 isSourceEnabled: function( source ) {
600 - for(var i=0; i < this.enabledSources.length; i++ ) {
601 - var enabledSource = this.enabledSources[i];
 558+ $.each( this.enabledSources, function( inx, enabledSource ) {
602559 if( source.id ) {
603 - if( source.id == enabledSource.id )
 560+ if( source.id === enabledSource.id ){
604561 return true;
 562+ }
605563 }
606564 if( source.srclang ) {
607 - if( source.srclang == enabledSource.srclang )
 565+ if( source.srclang === enabledSource.srclang ){
608566 return true;
 567+ }
609568 }
610 - }
 569+ });
611570 return false;
612571 },
613572
614573 /**
615574 * Get a source object by language, returns "false" if not found
 575+ * @param {string} langKey The language key filter for selected source
616576 */
617577 getSourceByLanguage: function ( langKey ) {
618578 for(var i=0; i < this.textSources.length; i++) {
619579 var source = this.textSources[ i ];
620 - if( source.srclang == langKey )
 580+ if( source.srclang == langKey ){
621581 return source;
 582+ }
622583 }
623584 return false;
624585 },
@@ -646,30 +607,28 @@
647608
648609 // Build the source list menu item:
649610 var $menu = $( '<ul>' );
650 - // Show text menu item ( if there are sources)
651 - if( _this.textSources.length != 0 ) {
 611+
 612+ // Show text menu item with layout option (if not fullscren )
 613+ if( _this.textSources.length !== 0 ) {
652614 $menu.append(
653615 $.getLineItem( gM( 'mwe-timedtext-choose-text'), 'comment' ).append(
654616 _this.getLanguageMenu()
655 - ),
656 - // Layout Menu option
657 - $.getLineItem( gM( 'mwe-timedtext-layout' ), 'image' ).append(
658 - _this.getLayoutMenu()
659 - )
 617+ )
660618 );
661 - } else {
662 - // Add a link to request timed text for this clip:
663 - if( mw.getConfig( 'TimedText.ShowRequestTranscript' ) ){
664 - $menu.append(
665 - $.getLineItem( gM( 'mwe-timedtext-request-subs'), 'comment', function(){
666 - _this.getAddSubRequest();
667 - })
668 - );
669 - } else {
670 - $menu.append(
671 - $.getLineItem( gM( 'mwe-timedtext-no-subs'), 'close' )
672 - )
673 - }
 619+ }
 620+
 621+ // Layout Menu option if not in an iframe and we can expand video size:
 622+
 623+ $menu.append(
 624+ $.getLineItem( gM( 'mwe-timedtext-layout' ), 'image' ).append(
 625+ _this.getLayoutMenu()
 626+ )
 627+ );
 628+
 629+ if( _this.textSources.length == 0 ){
 630+ $menu.append(
 631+ $.getLineItem( gM( 'mwe-timedtext-no-subs'), 'close' )
 632+ );
674633 }
675634
676635 // Put in the "Make Transcript" link if config enabled and we have an api key
@@ -682,117 +641,16 @@
683642 // Allow other modules to add to the timed text menu:
684643 $( _this.embedPlayer ).trigger( 'TimedText.BuildCCMenu', $menu ) ;
685644
 645+ // Test if only one menu item move its children to the top level
 646+ if( $menu.children('li').length == 1 ){
 647+ $menu.find('li > ul > li').detach().appendTo( $menu );
 648+ $menu.find('li').eq(0).remove();
 649+ }
 650+
686651 return $menu;
687652 },
688653
689 - // Simple interface to add a transcription request
690 - // TODO this should probably be moved to a gadget
691 - getAddSubRequest: function(){
692 - var _this = this;
693 - var buttons = {};
694 - buttons[ gM('mwe-timedtext-request-subs') ] = function(){
695 - var apiUrl = _this.textProvider.apiUrl;
696 - var videoTitle = 'File:' + _this.embedPlayer.apiTitleKey.replace('File:|Image:', '');
697 - var catName = mw.getConfig( 'TimedText.NeedsTranscriptCategory' );
698 - var $dialog = $(this);
699 -
700 - var subRequestCategoryUrl = apiUrl.replace('api.php', 'index.php') +
701 - '?title=Category:' + catName.replace(/ /g, '_');
702 -
703 - var buttonOk= {};
704 - buttonOk[gM('mwe-ok')] =function(){
705 - $(this).dialog('close');
706 - };
707 - // Set the loadingSpinner:
708 - $( this ).loadingSpinner();
709 - // Turn off buttons while loading
710 - $dialog.dialog( 'option', 'buttons', null );
711 -
712 - // Check if the category does not already exist:
713 - mw.getJSON( apiUrl, { 'titles': videoTitle, 'prop': 'categories' }, function( data ){
714 - if( data && data.query && data.query.pages ){
715 - for( var i in data.query.pages ){
716 - // we only request a single page:
717 - if( data.query.pages[i].categories ){
718 - var categories = data.query.pages[i].categories;
719 - for(var j =0; j < categories.length; j++){
720 - if( categories[j].title.indexOf( catName ) != -1 ){
721 - $dialog.html( gM('mwe-timedtext-request-already-done', subRequestCategoryUrl ) );
722 - $dialog.dialog( 'option', 'buttons', buttonOk);
723 - return ;
724 - }
725 - }
726 - }
727 - }
728 - }
729 -
730 - // Else category not found add to category:
731 - // check if the user is logged in:
732 - mw.getUserName( apiUrl, function( userName ){
733 - if( !userName ){
734 - $dialog.html( gM('mwe-timedtext-request-subs-fail') );
735 - return ;
736 - }
737 - // Get an edit token:
738 - mw.getToken( apiUrl, videoTitle, function( token ) {
739 - if( !token ){
740 - $dialog.html( gM('mwe-timedtext-request-subs-fail') );
741 - return ;
742 - }
743 - var request = {
744 - 'action' : 'edit',
745 - 'summary' : 'Added request for subtitles using [[Commons:MwEmbed|MwEmbed]]',
746 - 'title' : videoTitle,
747 - 'appendtext' : "\n[[Category:" + catName + "]]",
748 - 'token': token
749 - };
750 - // Do the edit request:
751 - mw.getJSON( apiUrl, request, function(data){
752 - if( data.edit && data.edit.newrevid){
753 -
754 - $dialog.html( gM('mwe-timedtext-request-subs-done', subRequestCategoryUrl )
755 - );
756 - } else {
757 - $dialog.html( gM('mwe-timedtext-request-subs-fail') );
758 - }
759 - $dialog.dialog( 'option', 'buttons', buttonOk );
760 - });
761 - });
762 - });
763 - });
764 - };
765 - buttons[ gM('mwe-cancel') ] = function(){
766 - $(this).dialog('close');
767 - };
768 - mw.addDialog({
769 - 'title' : gM( 'mwe-timedtext-request-subs'),
770 - 'width' : 450,
771 - 'content' : gM('mwe-timedtext-request-subs-desc'),
772 - 'buttons' : buttons
773 - });
774 - },
775654 /**
776 - * Shows the timed text edit ui
777 - *
778 - * @param {String} mode Mode or page to display ( to differentiate between edit vs new transcript)
779 - */
780 - showTimedTextEditUI: function( mode ) {
781 - var _this = this;
782 - // Show a loader:
783 - mw.addLoaderDialog( gM( 'mwe-timedtext-loading-text-edit' ) );
784 - // Load the timedText edit interface
785 - mw.load( 'mw.TimedTextEdit', function() {
786 - if( ! _this.editText ) {
787 - _this.editText = new mw.TimedTextEdit( _this );
788 - }
789 - // Close the loader:
790 - mw.closeLoaderDialog();
791 - // Show the upload text ui:
792 - _this.editText.showUI();
793 - });
794 - },
795 -
796 - /**
797655 * Utility function to assist in menu build out:
798656 * Get menu line item (li) html: <li><a> msgKey </a></li>
799657 *
@@ -805,8 +663,8 @@
806664 getLiAddText: function() {
807665 var _this = this;
808666 return $.getLineItem( gM( 'mwe-timedtext-upload-timed-text'), 'script', function() {
809 - _this.showTimedTextEditUI( 'add' );
810 - } );
 667+ _this.showTimedTextEditUI( 'add' );
 668+ });
811669 },
812670
813671 /**
@@ -822,10 +680,9 @@
823681 return $.getLineItem( source.title, source_icon, function() {
824682 _this.selectTextSource( source );
825683 });
826 - }
 684+ }
827685 if( source.srclang ) {
828686 var langKey = source.srclang.toLowerCase();
829 - var cat = gM('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) );
830687 return $.getLineItem(
831688 gM('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) ),
832689 source_icon,
@@ -857,11 +714,14 @@
858715 var layoutOptions = [ ];
859716
860717 //Only display the "ontop" option if the player supports it:
861 - if( this.embedPlayer.supports[ 'overlays' ] )
 718+ if( this.embedPlayer.supports[ 'overlays' ] ){
862719 layoutOptions.push( 'ontop' );
 720+ }
863721
864722 //Add below and "off" options:
865 - layoutOptions.push( 'below' );
 723+ if( ! mw.getConfig('EmbedPlayer.IsIframeServer') ){
 724+ layoutOptions.push( 'below' );
 725+ }
866726 layoutOptions.push( 'off' );
867727
868728 $ul = $('<ul>');
@@ -893,7 +753,13 @@
894754 _this.updateLayout();
895755 }
896756 },
897 -
 757+ toggleCaptions: function(){
 758+ if( this.config.layout == 'off' ){
 759+ this.selectLayout( 'ontop' );
 760+ } else {
 761+ this.selectLayout( 'off' );
 762+ }
 763+ },
898764 /**
899765 * Updates the timed text layout ( should be called when config.layout changes )
900766 */
@@ -911,22 +777,22 @@
912778 selectTextSource: function( source ) {
913779 var _this = this;
914780 mw.log("mw.TimedText:: selectTextSource: select lang: " + source.srclang );
 781+
915782 // For some reason we lose binding for the menu ~sometimes~ re-bind
916783 this.bindTextButton( this.embedPlayer.$interface.find('timed-text') );
917784
918 -
919785 this.currentLangKey = source.srclang;
920786
921787 // Update the config language if the source includes language
922788 if( source.srclang )
923 - this.config.userLanugage = source.srclang;
 789+ this.config.userLanguage = source.srclang;
924790
925 - if( source.category )
926 - this.config.userCategory = source.category;
 791+ if( source.kind )
 792+ this.config.userKind = source.kind;
927793
928 - // (@@todo update category & setup category language buckets? )
 794+ // (@@todo update kind & setup kind language buckets? )
929795
930 - // Remove any other sources selected in sources category
 796+ // Remove any other sources selected in sources kind
931797 this.enabledSources = [];
932798
933799 this.enabledSources.push( source );
@@ -943,6 +809,9 @@
944810 } else {
945811 _this.refreshDisplay();
946812 }
 813+
 814+ // Trigger the event
 815+ $( this.embedPlayer ).trigger( 'TimedText_ChangeSource' );
947816 },
948817
949818 /**
@@ -970,30 +839,34 @@
971840
972841 /**
973842 * Builds the language source list menu
974 - * checks all text sources for category and language key attribute
 843+ * Cehck if the "track" tags had the "kind" attribute.
 844+ *
 845+ * The kind attribute forms "categories" of text tracks like "subtitles",
 846+ * "audio description", "chapter names". We check for these categories
 847+ * when building out the language menu.
975848 */
976849 getLanguageMenu: function() {
977850 var _this = this;
978851
979852 // See if we have categories to worry about
980 - // associative array of SUB etc categories. Each category contains an array of textSources.
981 - var catSourceList = {};
982 - var catSourceCount = 0;
 853+ // associative array of SUB etc categories. Each kind contains an array of textSources.
 854+ var categorySourceList = {};
 855+ var sourcesWithCategoryCount = 0;
983856
984 - // ( All sources should have a category (depreciate )
 857+ // ( All sources should have a kind (depreciate )
985858 var sourcesWithoutCategory = [ ];
986859 for( var i=0; i < this.textSources.length; i++ ) {
987860 var source = this.textSources[ i ];
988 - if( source.category ) {
989 - var catKey = source.category ;
 861+ if( source.kind ) {
 862+ var categoryKey = source.kind ;
990863 // Init Category menu item if it does not already exist:
991 - if( !catSourceList[ catKey ] ) {
 864+ if( !categorySourceList[ categoryKey ] ) {
992865 // Set up catList pointer:
993 - catSourceList[ catKey ] = [ ];
994 - catSourceCount++;
 866+ categorySourceList[ categoryKey ] = [ ];
 867+ sourcesWithCategoryCount++;
995868 }
996 - // Append to the source category key menu item:
997 - catSourceList[ catKey ].push(
 869+ // Append to the source kind key menu item:
 870+ categorySourceList[ categoryKey ].push(
998871 _this.getLiSource( source )
999872 );
1000873 }else{
@@ -1002,31 +875,31 @@
1003876 }
1004877 var $langMenu = $('<ul>');
1005878 // Check if we have multiple categories ( if not just list them under the parent menu item)
1006 - if( catSourceCount > 1 ) {
1007 - for(var catKey in catSourceList) {
 879+ if( sourcesWithCategoryCount > 1 ) {
 880+ for(var categoryKey in categorySourceList) {
1008881 var $catChildren = $('<ul>');
1009 - for(var i=0; i < catSourceList[ catKey ].length; i++) {
 882+ for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
1010883 $catChildren.append(
1011 - catSourceList[ catKey ][i]
 884+ categorySourceList[ categoryKey ][i]
1012885 );
1013886 }
1014 - // Append a cat menu item for each category list
 887+ // Append a cat menu item for each kind list
1015888 $langMenu.append(
1016 - $.getLineItem( gM( 'mwe-timedtext-textcat-' + catKey.toLowerCase() ) ).append(
 889+ $.getLineItem( gM( 'mwe-timedtext-textcat-' + categoryKey.toLowerCase() ) ).append(
1017890 $catChildren
1018891 )
1019892 );
1020893 }
1021894 } else {
1022 - for(var catKey in catSourceList) {
1023 - for(var i=0; i < catSourceList[ catKey ].length; i++) {
 895+ for(var categoryKey in categorySourceList) {
 896+ for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
1024897 $langMenu.append(
1025 - catSourceList[ catKey ][i]
 898+ categorySourceList[ categoryKey ][i]
1026899 );
1027900 }
1028901 }
1029902 }
1030 -
 903+ // Add any remaning sources that did nto have a category
1031904 for(var i=0; i < sourcesWithoutCategory.length; i++) {
1032905 $langMenu.append( sourcesWithoutCategory[i] );
1033906 }
@@ -1037,706 +910,235 @@
1038911 _this.getLiAddText()
1039912 );
1040913 }
1041 -
 914+
1042915 return $langMenu;
1043916 },
1044917
1045918 /**
1046919 * Updates a source display in the interface for a given time
1047 - * @param {Object} source Source to update
 920+ * @param {object} source Source to update
 921+ * @param {number} time Caption time used to add and remove active captions.
1048922 */
1049923 updateSourceDisplay: function ( source, time ) {
1050 - // Get the source text for the requested time:
1051 - var text = source.getTimedText( time );
1052 -
1053 - // We do a type comparison so that "undefined" != "false"
1054 - // ( check if we are updating the text )
1055 - if( text === this.prevText[ source.category ] ){
1056 - return ;
 924+ var _this = this;
 925+ if( this.timeOffset ){
 926+ time = time + parseInt( this.timeOffset );
1057927 }
1058 -
1059 - //mw.log( 'mw.TimedText:: updateTextDisplay: ' + text );
1060 -
1061 - var $playerTarget = this.embedPlayer.$interface;
1062 - var $textTarget = $playerTarget.find( '.track_' + source.category + ' span' );
1063 - // If we are missing the target add it:
1064 - if( $textTarget.length == 0 ) {
1065 - this.addItextDiv( source.category );
1066 - // Re-grab the textTarget:
1067 - $textTarget = $playerTarget.find( '.track_' + source.category + ' span' );
1068 - }
1069 -
1070 - // If text is "false" fade out the subtitle:
1071 - if( text === false ) {
1072 - $textTarget.fadeOut('fast');
1073 - }else{
1074 - // Fade in the target if not visible
1075 - if( ! $textTarget.is(':visible') ) {
1076 - $textTarget.fadeIn('fast');
 928+
 929+ // Get the source text for the requested time:
 930+ var activeCaptions = source.getCaptionForTime( time );
 931+ var addedCaption = false;
 932+ // Show captions that are on:
 933+ $.each(activeCaptions, function( capId, caption){
 934+ if( _this.embedPlayer.$interface.find( '.track[data-capId="' + capId +'"]').length == 0){
 935+ _this.addCaption( source, capId, caption );
 936+ addedCaption = true;
1077937 }
1078 - // Update text ( use "html" instead of "text" so that subtitle format can
1079 - // include html formating
1080 - // TOOD we should scrub this for non-formating html
1081 - $textTarget.html( text );
1082 -
1083 - // Add/update the lang option
1084 - $textTarget.attr( 'lang', source.srclang.toLowerCase() );
1085 -
1086 - // Update any links to point to a new window
1087 - $textTarget.find( 'a' ).attr( 'target', '_blank' );
 938+ });
 939+
 940+ // hide captions that are off:
 941+ _this.embedPlayer.$interface.find( '.track' ).each(function( inx, caption){
 942+ if( !activeCaptions[ $( caption ).attr('data-capId') ] ){
 943+ if( addedCaption ){
 944+ $( caption ).remove();
 945+ } else {
 946+ $( caption ).fadeOut( mw.getConfig('EmbedPlayer.MonitorRate'), function(){ $(this).remove();} );
 947+ }
 948+ }
 949+ });
 950+ },
 951+ getCaptionsTarget: function(){
 952+ var $capTarget = this.embedPlayer.$interface.find('.captionsLayoutTarget');
 953+ var layoutCss = {
 954+ 'left' : 0,
 955+ 'top' :0,
 956+ 'right':0,
 957+ 'position': 'absolute'
 958+ };
 959+ if( this.embedPlayer.controlBuilder.isOverlayControls() ||
 960+ !mw.getConfig( 'EmbedPlayer.OverlayControls') )
 961+ {
 962+ layoutCss['bottom'] = 0;
 963+ } else {
 964+ layoutCss['bottom'] = this.embedPlayer.controlBuilder.getHeight();
1088965 }
1089 - // mw.log( ' len: ' + $textTarget.length + ' ' + $textTarget.html() );
1090 - // Update the prev text:
1091 - this.prevText[ source.category ] = text;
 966+
 967+ if( $capTarget.length == 0 ){
 968+ $capTarget = $( '<div />' )
 969+ .addClass( 'captionsLayoutTarget' )
 970+ .css( layoutCss )
 971+ this.embedPlayer.$interface.append( $capTarget )
 972+ }
 973+ return $capTarget;
1092974 },
 975+ addCaption: function( source, capId, caption ){
 976+ if( this.getLayoutMode() == 'off' ){
 977+ return ;
 978+ }
 979+ // use capId as a class instead of id for easy selections and no conflicts with
 980+ // multiple players on page.
 981+ var $textTarget = $('<div />')
 982+ .addClass( 'track' )
 983+ .attr( 'data-capId', capId )
 984+ .hide();
 985+
 986+ // Update text ( use "html" instead of "text" so that subtitle format can
 987+ // include html formating
 988+ // TOOD we should scrub this for non-formating html
 989+ $textTarget.append(
 990+ $('<span />')
 991+ .css( this.getCaptionCss() )
 992+ .html( caption.content )
 993+ );
1093994
1094995
1095 - /**
1096 - * Add an track div to the embedPlayer
1097 - */
1098 - addItextDiv: function( category ) {
1099 - mw.log(" addItextDiv: " + category );
1100 - // Get the relative positioned player class from the controlBuilder:
1101 - var $playerTarget = this.embedPlayer.$interface;
1102 - //Remove any existing track divs for this player;
1103 - $playerTarget.find('.track_' + category ).remove();
1104 -
1105 - // Setup the display text div:
1106 - var layoutMode = this.getLayoutMode();
1107 - if( layoutMode == 'ontop' ) {
1108 - this.embedPlayer.controlBuilder.keepControlBarOnScreen = false;
1109 - var $track = $('<div>')
1110 - .addClass( 'track' + ' ' + 'track_' + category )
1111 - .css( {
1112 - 'position':'absolute',
1113 - 'bottom': ( this.embedPlayer.controlBuilder.getHeight() + 10 ),
1114 - 'width': '100%',
1115 - 'display': 'block',
1116 - 'opacity': .8,
1117 - 'text-align':'center'
1118 - })
1119 - .append(
1120 - $('<span \>')
1121 - );
1122 -
1123 - // Scale the text Relative to player size:
1124 - $track.css(
1125 - this.getInterfaceSizeTextCss({
1126 - 'width' : this.embedPlayer.getWidth(),
1127 - 'height' : this.embedPlayer.getHeight()
1128 - })
1129 - );
1130 - // Resize the interface for layoutMode == 'below' ( if not in full screen)
1131 - if( ! this.embedPlayer.controlBuilder.fullscreenMode ){
1132 - this.embedPlayer.$interface.animate({
1133 - 'height': this.embedPlayer.getHeight()
1134 - });
 996+ // Add/update the lang option
 997+ $textTarget.attr( 'lang', source.srclang.toLowerCase() );
 998+
 999+ // Update any links to point to a new window
 1000+ $textTarget.find( 'a' ).attr( 'target', '_blank' );
 1001+
 1002+ // Apply any custom style ( if we are ontop of the video )
 1003+ if( this.getLayoutMode() == 'ontop' ){
 1004+ if( caption.css ){
 1005+ $textTarget.css( caption.css );
 1006+ } else {
 1007+ $textTarget.css( this.getDefaultStyle() );
11351008 }
1136 - $playerTarget.append( $track );
1137 -
1138 - } else if ( layoutMode == 'below') {
1139 - this.embedPlayer.controlBuilder.keepControlBarOnScreen = true;
1140 - // Set the belowBar size to 60 pixels:
1141 - var belowBarHeight = 60;
1142 - // Append before controls:
1143 - $playerTarget.find( '.control-bar' ).before(
1144 - $('<div>').addClass( 'track' + ' ' + 'track_' + category )
1145 - .css({
1146 - 'position' : 'absolute',
1147 - 'top' : this.embedPlayer.getHeight(),
1148 - 'display' : 'block',
1149 - 'width' : '100%',
1150 - 'height' : belowBarHeight + 'px',
1151 - 'background-color' : '#000',
1152 - 'text-align' : 'center',
1153 - 'padding-top' : '5px'
1154 - } ).append(
1155 - $('<span>').css( {
1156 - 'color':'white'
1157 - } )
1158 - )
 1009+ this.getCaptionsTarget().append(
 1010+ $textTarget
11591011 );
1160 - // Add some height for the bar and interface
1161 - var height = ( belowBarHeight + 8 ) + this.embedPlayer.getHeight() + this.embedPlayer.controlBuilder.getHeight();
1162 - // Resize the interface for layoutMode == 'below' ( if not in full screen)
1163 - if( ! this.embedPlayer.controlBuilder.fullscreenMode ){
1164 - this.embedPlayer.$interface.animate({
1165 - 'height': height
1166 - });
1167 - }
1168 - mw.log( ' height of ' + this.embedPlayer.id + ' is now: ' + $( '#' + this.embedPlayer.id ).height() );
 1012+ } else {
 1013+ // else apply the default layout system:
 1014+ this.addTextToDefaultLocation( $textTarget );
11691015 }
1170 - mw.log( 'should have been appended: ' + $playerTarget.find('.track').length );
1171 - }
1172 - };
1173 -
1174 - /**
1175 - * TextSource object extends a base mediaSource object
1176 - * with some timedText features
1177 - *
1178 - * @param {Object} source Source object to extend
1179 - * @param {Object} textProvider [Optional] The text provider interface ( to load source from api )
1180 - */
1181 - TextSource = function( source , textProvider) {
1182 - return this.init( source, textProvider );
1183 - };
1184 - TextSource.prototype = {
1185 -
1186 - //The load state:
1187 - loaded: false,
1188 -
1189 - // Container for the captions
1190 - // captions include "start", "end" and "content" fields
1191 - captions: [],
1192 -
1193 - // The previous index of the timed text served
1194 - // Avoids searching the entire array on time updates.
1195 - prevIndex: 0,
1196 -
1197 - /**
1198 - * @constructor Inherits mediaSource from embedPlayer
1199 - * @param {source} Base source element
1200 - * @param {Object} Pointer to the textProvider
1201 - */
1202 - init: function( source , textProvider) {
1203 - // Inherits mediaSource
1204 - for( var i in source){
1205 - this[ i ] = source[ i];
1206 - }
 1016+ // apply any interface size adjustments:
 1017+ $textTarget.css( this.getInterfaceSizeTextCss({
 1018+ 'width' : this.embedPlayer.$interface.width(),
 1019+ 'height' : this.embedPlayer.$interface.height()
 1020+ })
 1021+ );
12071022
1208 - // Set default category to subtitle if unset:
1209 - if( ! this.category ) {
1210 - this.category = 'SUB';
 1023+
 1024+ // Update the style of the text object if set
 1025+ if( caption.styleId ){
 1026+ var capCss = source.getStyleCssById( caption.styleId );
 1027+ $textTarget.find('span').css(
 1028+ capCss
 1029+ );
12111030 }
1212 - //Set the textProvider if provided
1213 - if( textProvider ) {
1214 - this.textProvider = textProvider;
1215 -
1216 - // switch type to mw-srt if we are going to load via api
1217 - // ( this is need because we want to represent one thing to search engines / crawlers,
1218 - // while representing the mw-srt type internally so that mediawiki parsed text
1219 - // gets converted to html before going into the video
1220 - if( this.mwtitle ){
1221 - this.mimeType = 'text/mw-srt';
1222 - }
1223 - }
1224 - return this;
 1031+
 1032+ $textTarget.fadeIn('fast');
12251033 },
1226 -
 1034+ getDefaultStyle: function(){
 1035+ var baseCss = {
 1036+ 'position':'absolute',
 1037+ 'bottom': 10,
 1038+ 'width': '100%',
 1039+ 'display': 'block',
 1040+ 'opacity': .8,
 1041+ 'text-align': 'center',
 1042+ 'z-index': 2
 1043+ };
 1044+ baseCss =$.extend( baseCss, this.getInterfaceSizeTextCss({
 1045+ 'width' : this.embedPlayer.$interface.width(),
 1046+ 'height' : this.embedPlayer.$interface.height()
 1047+ }));
 1048+ return baseCss;
 1049+ },
12271050 /**
1228 - * Function to load and parse the source text
1229 - * @param {Function} callback Function called once text source is loaded
 1051+ * Applies the default layout for a text target
12301052 */
1231 - load: function( callback ) {
1232 - var _this = this;
1233 -
1234 - //check if its already loaded:
1235 - if( _this.loaded ) {
1236 - if( callback ) {
1237 - callback();
1238 - return ;
1239 - }
1240 - };
1241 - _this.loaded = true;
1242 - // Set parser handler:
1243 - switch( this.getMIMEType() ) {
1244 - //Special mediaWiki srt format ( support wiki-text in srt's )
1245 - case 'text/mw-srt':
1246 - var handler = parseMwSrt;
1247 - break;
1248 - case 'text/x-srt':
1249 - var handler = parseSrt;
1250 - break;
1251 - case 'text/cmml':
1252 - var handler = parseCMML;
1253 - break;
1254 - default:
1255 - var hanlder = null;
1256 - break;
1257 - }
1258 - if( !handler ) {
1259 - mw.log("Error: no handler for type: " + this.getMIMEType() );
1260 - return ;
1261 - }
1262 - // Try to load src via textProvider:
1263 - if( this.textProvider && this.mwtitle) {
1264 - this.textProvider.loadTitleKey( this.mwtitle, function( data ) {
1265 - if( data ) {
1266 - _this.captions = handler( data );
1267 - }
1268 - mw.log("mw.TimedText:: loaded from titleKey: " + _this.captions.length + ' captions');
1269 - // Update the loaded state:
1270 - _this.loaded = true;
1271 - if( callback ) {
1272 - callback();
1273 - }
 1053+ addTextBelowVideo: function( $textTarget ) {
 1054+ var $playerTarget = this.embedPlayer.$interface;
 1055+ // Get the relative positioned player class from the controlBuilder:
 1056+ this.embedPlayer.controlBuilder.keepControlBarOnScreen = true;
 1057+ // Set the belowBar size to 60 pixels:
 1058+ var belowBarHeight = mw.getConfig('TimedText.BelowVideoBlackBoxHeight');
 1059+
 1060+ // Append before controls:
 1061+ $playerTarget.find( '.control-bar' ).before(
 1062+ $('<div>').addClass( 'captionContainer' )
 1063+ .css({
 1064+ 'position' : 'absolute',
 1065+ 'top' : this.embedPlayer.getHeight(),
 1066+ 'display' : 'block',
 1067+ 'width' : '100%',
 1068+ 'height' : belowBarHeight + 'px',
 1069+ 'background-color' : '#000',
 1070+ 'text-align' : 'center',
 1071+ 'padding-top' : '5px'
 1072+ } ).append(
 1073+ $textTarget.css( {
 1074+ 'color':'white'
 1075+ } )
 1076+ )
 1077+ );
 1078+
 1079+ // Add some height for the bar and interface
 1080+ var height = ( belowBarHeight + 8 ) + this.embedPlayer.getHeight() + this.embedPlayer.controlBuilder.getHeight();
 1081+
 1082+ // Resize the interface for layoutMode == 'below' ( if not in full screen)
 1083+ if( ! this.embedPlayer.controlBuilder.fullscreenMode ){
 1084+ this.embedPlayer.$interface.animate({
 1085+ 'height': height
12741086 });
1275 - return ;
12761087 }
1277 -
1278 - // Try to load src via XHR source
1279 - if( this.getSrc() ) {
1280 - // Issue the direct load request
1281 - if ( !mw.isLocalDomain( this.getSrc() ) ) {
1282 - mw.log("Error: cant load crossDomain src:" + this.getSrc() );
1283 - return ;
1284 - }
1285 - $.get( this.getSrc(), function( data ) {
1286 - // Parse and load captions:
1287 - _this.captions = handler( data );
1288 - mw.log("mw.TimedText:: loaded from srt file: " + _this.captions.length + ' captions');
1289 - // Update the loaded state:
1290 - _this.loaded = true;
1291 - if( callback ) {
1292 - callback();
1293 - }
1294 - }, 'text' );
1295 - return ;
1296 - }
1297 -
1298 -
 1088+ mw.log( 'TimedText:: height of ' + this.embedPlayer.id + ' is now: ' + $( '#' + this.embedPlayer.id ).height() );
12991089 },
1300 -
13011090 /**
1302 - * Returns the text content for requested time
1303 - *
1304 - * @param {String} time Time in seconds
1305 - */
1306 - getTimedText: function ( time ) {
1307 - var prevCaption = this.captions[ this.prevIndex ];
 1091+ * Build css for caption using this.options
 1092+ */
 1093+ getCaptionCss: function() {
 1094+ var options = this.options;
 1095+ var style = {'display': 'inline'};
13081096
1309 - // Setup the startIndex:
1310 - if( prevCaption && time >= prevCaption.start ) {
1311 - var startIndex = this.prevIndex;
1312 - }else{
1313 - //If a backwards seek start searching at the start:
1314 - var startIndex = 0;
 1097+ if( options.bg ) {
 1098+ style["background-color"] = this.getHexColor( options.bg );
13151099 }
1316 - // Start looking for the text via time, return first match:
1317 - for( var i = startIndex ; i < this.captions.length; i++ ) {
1318 - var caption = this.captions[ i ];
1319 - // Don't handle captions with 0 or -1 end time:
1320 - if( caption.end == 0 || caption.end == -1)
1321 - continue;
1322 -
1323 - if( time >= caption.start &&
1324 - time <= caption.end ) {
1325 - this.prevIndex = i;
1326 - //mw.log("Start cap time: " + caption.start + ' End time: ' + caption.end );
1327 - return caption.content;
1328 - }
 1100+ if( options.fontColor ) {
 1101+ style["color"] = this.getHexColor( options.fontColor );
13291102 }
1330 - //No text found in range return false:
1331 - return false;
1332 - }
1333 - };
1334 -
1335 - /**
1336 - * parse mediaWiki html srt
1337 - * @param {Object} data XML data string to be parsed
1338 - */
1339 - function parseMwSrt( data ) {
1340 - var captions = [ ];
1341 - var curentCap = [];
1342 - var parseNextAsTime = false;
1343 - // Optimize: we could use javascript strings functions instead of jQuery XML parsing:
1344 - $( '<div>' + data + '</div>' ).find('p').each( function() {
1345 - var currentPtext = $(this).html();
1346 - //mw.log( 'pText: ' + currentPtext );
1347 -
1348 - //Check if the p matches the "all in one line" match:
1349 - var m = currentPtext
1350 - .replace('--&gt;', '-->')
1351 - .match(/\d+\s([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\s*--?>\s*([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\n?(.*)/);
1352 -
1353 - if (m) {
1354 - var startMs = (m[4])? (parseInt(m[4], 10) / 1000):0;
1355 - var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0;
1356 - captions.push({
1357 - 'start':
1358 - (parseInt(m[1], 10) * 60 * 60) +
1359 - (parseInt(m[2], 10) * 60) +
1360 - (parseInt(m[3], 10)) +
1361 - startMs ,
1362 - 'end':
1363 - (parseInt(m[5], 10) * 60 * 60) +
1364 - (parseInt(m[6], 10) * 60) +
1365 - (parseInt(m[7], 10)) +
1366 - endMs,
1367 - 'content': $.trim( m[9] )
1368 - });
1369 - return true;
 1103+ if( options.fontFamily ){
 1104+ style["font-family"] = options.fontFamily;
13701105 }
1371 - // Else check for multi-line match:
1372 - if( parseInt( currentPtext ) == currentPtext ) {
1373 - if( curentCap.length != 0) {
1374 - captions.push( curentCap );
1375 - }
1376 - curentCap = {
1377 - 'content': ''
1378 - };
1379 - return true;
 1106+ if( options.fontsize ) {
 1107+ // Translate to em size so that font-size parent percentage
 1108+ // base on http://pxtoem.com/
 1109+ var emFontMap = { '6': .375, '7': .438, '8' : .5, '9': .563, '10': .625, '11':.688,
 1110+ '12':.75, '13': .813, '14': .875, '15':.938, '16':1, '17':1.063, '18': 1.125, '19': 1.888,
 1111+ '20':1.25, '21':1.313, '22':1.375, '23':1.438, '24':1.5};
 1112+ // Make sure its an int:
 1113+ options.fontsize = parseInt( options.fontsize );
 1114+ style[ "font-size" ] = ( emFontMap[ options.fontsize ] ) ?
 1115+ emFontMap[ options.fontsize ] +'em' :
 1116+ ( options.fontsize > 24 )? emFontMap[24]+'em' : emFontMap[6];
13801117 }
1381 - //Check only for time match:
1382 - var m = currentPtext.replace('--&gt;', '-->').match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/);
1383 - if (m) {
1384 - var startMs = (m[4])? (parseInt(m[4], 10) / 1000):0;
1385 - var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0;
1386 - curentCap['start']=
1387 - (parseInt(m[1], 10) * 60 * 60) +
1388 - (parseInt(m[2], 10) * 60) +
1389 - (parseInt(m[3], 10)) +
1390 - startMs;
1391 - curentCap['end']=
1392 - (parseInt(m[5], 10) * 60 * 60) +
1393 - (parseInt(m[6], 10) * 60) +
1394 - (parseInt(m[7], 10)) +
1395 - endMs;
1396 - return true;
 1118+ if( options.useGlow && options.glowBlur && options.glowColor ) {
 1119+ style["text-shadow"] = '0 0 ' + options.glowBlur + 'px ' + this.getHexColor( options.glowColor );
13971120 }
1398 - //Else content for the curentCap
1399 - if( currentPtext != '<br>' ) {
1400 - curentCap['content'] += currentPtext;
1401 - }
1402 - });
1403 - //Push last subtitle:
1404 - if( curentCap.length != 0) {
1405 - captions.push( curentCap );
1406 - }
1407 - return captions;
1408 - }
1409 - /**
1410 - * srt timed text parse handle:
1411 - * @param {String} data Srt string to be parsed
1412 - */
1413 - function parseSrt( data ) {
1414 - // Remove dos newlines
1415 - var srt = data.replace(/\r+/g, '');
14161121
1417 - // Trim white space start and end
1418 - srt = srt.replace(/^\s+|\s+$/g, '');
 1122+ return style;
 1123+ },
14191124
1420 - // Remove all html tags for security reasons
1421 - srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, '');
1422 -
1423 - // Get captions
1424 - var captions = [];
1425 - var caplist = srt.split('\n\n');
1426 - for (var i = 0; i < caplist.length; i++) {
1427 - var caption = "";
1428 - var content, start, end, s;
1429 - caption = caplist[i];
1430 - s = caption.split(/\n/);
1431 - if (s.length < 2) {
1432 - // file format error or comment lines
1433 - continue;
1434 - }
1435 - if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) {
1436 - // ignore caption number in s[0]
1437 - // parse time string
1438 - var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/);
1439 - if (m) {
1440 - start =
1441 - (parseInt(m[1], 10) * 60 * 60) +
1442 - (parseInt(m[2], 10) * 60) +
1443 - (parseInt(m[3], 10)) +
1444 - (parseInt(m[4], 10) / 1000);
1445 - end =
1446 - (parseInt(m[5], 10) * 60 * 60) +
1447 - (parseInt(m[6], 10) * 60) +
1448 - (parseInt(m[7], 10)) +
1449 - (parseInt(m[8], 10) / 1000);
1450 - } else {
1451 - // Unrecognized timestring
1452 - continue;
1453 - }
1454 - // concatenate text lines to html text
1455 - content = s.slice(2).join("<br>");
 1125+ getHexColor: function(color) {
 1126+ if( color.substr(0,2) == "0x" ) {
 1127+ return color.replace('0x', '#');
14561128 } else {
1457 - // file format error or comment lines
1458 - continue;
 1129+ color = parseInt( color );
 1130+ color = color.toString(16);
 1131+ var len = 6 - color.length;
 1132+ if( len > 0 ) {
 1133+ var pre = '';
 1134+ for( var i=0; i<len; i++) {
 1135+ pre += '0';
 1136+ }
 1137+ color = pre + color;
 1138+ }
 1139+ return '#' + color;
14591140 }
1460 - captions.push({
1461 - 'start' : start,
1462 - 'end' : end,
1463 - 'content' : content
1464 - } );
14651141 }
1466 -
1467 - return captions;
14681142 };
1469 - /**
1470 - * CMML parser handle
1471 - * @param {Mixed} data String or XML tree of CMML data to be parsed
1472 - */
1473 - function parseCMML( data ) {
1474 - var captions = [ ];
1475 - $( data ).find( 'clip' ).each( function( inx, clip ) {
1476 - var content, start, end;
1477 - // mw.log(' on clip ' + clip.id);
1478 - start = mw.npt2seconds( $( clip ).attr( 'start' ).replace( 'npt:', '' ) );
1479 - end = mw.npt2seconds( $( clip ).attr( 'end' ).replace( 'npt:', '' ) );
14801143
1481 - $( clip ).find( 'body' ).each( function( binx, bn ) {
1482 - if ( bn.textContent ) {
1483 - content = bn.textContent;
1484 - } else if ( bn.text ) {
1485 - content = bn.text;
1486 - }
1487 - } );
1488 - captions.push ( {
1489 - 'start' : start,
1490 - 'end' : end,
1491 - 'content' : content
1492 - } );
1493 - } );
1494 -
1495 - return captions;
1496 - }
1497 -
1498 - /**
1499 - * Text Providers
1500 - *
1501 - * text provider objects let you map your player to a timed text provider
1502 - * can provide discovery, and contribution push back
1503 - *
1504 -
1505 - // Will add a base class once we are serving more than just mediaWiki "commons"
1506 - // also we should segment out these files
1507 - mw.BaseTextProvider = function() {
1508 - return this.init();
1509 - }
1510 - mw.BaseTextProvider.prototype = {
1511 - init: function() {
1512 -
1513 - }
1514 - }
1515 -
1516 - */
1517 - var default_textProvider_attr = [
1518 - 'apiUrl',
1519 - 'providerId',
1520 - 'timedTextNS',
1521 - 'embedPlayer'
1522 - ];
1523 -
1524 - mw.MediaWikTrackProvider = function( options ) {
1525 - this.init( options );
1526 - };
1527 - mw.MediaWikTrackProvider.prototype = {
1528 -
1529 - // The api url:
1530 - apiUrl: null,
1531 -
1532 - // The timed text namespace
1533 - timedTextNS: null,
1534 -
1535 - /**
1536 - * @constructor
1537 - * @param {Object} options Set of options for the provider
1538 - */
1539 - init: function( options ) {
1540 - for(var i in default_textProvider_attr) {
1541 - var attr = default_textProvider_attr[ i ];
1542 - if( options[ attr ] )
1543 - this[ attr ] = options[ attr ];
1544 -
1545 - }
1546 - },
1547 -
1548 - /**
1549 - * Loads a single text source by titleKey
1550 - * @param {Object} titleKey
1551 - */
1552 - loadTitleKey: function( titleKey, callback ) {
1553 - var request = {
1554 - 'action': 'parse',
1555 - 'page': titleKey
1556 - /**
1557 - * For now we don't use cache helpers since the request is
1558 - * going over jsonp we kill any outer cache anyway
1559 - 'smaxage' : 300,
1560 - 'maxage' : 300
1561 - */
1562 - };
1563 - mw.getJSON( this.apiUrl, request, function( data ) {
1564 - if ( data && data.parse && data.parse.text['*'] ) {
1565 - callback( data.parse.text['*'] );
1566 - return;
1567 - }
1568 - mw.log("Error: could not load:" + titleKey);
1569 - callback( false );
1570 - } );
1571 - },
1572 -
1573 - /**
1574 - * Loads all available source for a given apiTitleKey
1575 - *
1576 - * @param {String} apiTitleKey For mediaWiki the apiTitleKey is the "wiki title"
1577 - */
1578 - loadSources: function( apiTitleKey, callback ) {
1579 - var request = {};
1580 - var _this = this;
1581 - this.getSourcePages( apiTitleKey, function( sourcePages ) {
1582 - if( ! sourcePages.query.allpages ) {
1583 - //Check if a shared asset
1584 - mw.log( 'no subtitle pages found');
1585 - if( callback )
1586 - callback();
1587 - return ;
1588 - }
1589 - // We have sources put them into the player
1590 - if( callback )
1591 - callback( _this.getSources( sourcePages ) );
1592 - } );
1593 - },
1594 -
1595 - /**
1596 - * Get the subtitle pages
1597 - * @param {String} titleKey Title to get subtitles for
1598 - * @param {Function} callback Function to call once NS subs are grabbed
1599 - */
1600 - getSourcePages: function( titleKey, callback ) {
1601 - var _this = this;
1602 - var request = {
1603 - 'list' : 'allpages',
1604 - 'apprefix' : unescape( titleKey ),
1605 - 'apnamespace' : this.getTimedTextNS(),
1606 - 'aplimit' : 200,
1607 - 'prop':'revisions'
1608 - /**
1609 - * For now we don't use cache helpers since the request is
1610 - * going over jsonp we kill any outer cache anyway
1611 - 'smaxage' : 300,
1612 - 'maxage' : 300
1613 - */
1614 - };
1615 - mw.getJSON( this.apiUrl, request, function( sourcePages ) {
1616 - // If "timedText" is not a valid namespace try "just" with prefix:
1617 - if ( sourcePages.error && sourcePages.error.code == 'apunknown_apnamespace' ) {
1618 - var request = {
1619 - 'list' : 'allpages',
1620 - 'apprefix' : _this.getCanonicalTimedTextNS() + ':' + _this.embedPlayer.apiTitleKey
1621 - };
1622 - mw.getJSON( _this.apiUrl, request, function( sourcePages ) {
1623 - callback( sourcePages );
1624 - } );
1625 - } else {
1626 - callback( sourcePages );
1627 - }
1628 - } );
1629 - },
1630 -
1631 - /**
1632 - * Get the sources from sourcePages data object ( api result )
1633 - * @param {Object} sourcePages Source page result object
1634 - */
1635 - getSources: function( sourcePages ) {
1636 - var _this = this;
1637 - // look for text tracks:
1638 - var foundTextTracks = false;
1639 - var sources = [];
1640 - for ( var i=0; i < sourcePages.query.allpages.length; i++ ) {
1641 -
1642 - var subPage = sourcePages.query.allpages[i];
1643 - if( !subPage || !subPage.title ){
1644 - continue;
1645 - }
1646 - var langKey = subPage.title.split( '.' );
1647 - var extension = langKey.pop();
1648 - langKey = langKey.pop();
1649 - //NOTE: we hard code the mw-srt type
1650 - // ( This is because mediaWiki srt files can have wiki-text and parsed as such )
1651 - if( extension == 'srt' ) {
1652 - extension = 'mw-srt';
1653 - }
1654 -
1655 - if ( ! _this.isSuportedLang( langKey ) ) {
1656 - mw.log( 'Error: langkey:' + langKey + ' not supported' );
1657 - } else {
1658 - sources.push( {
1659 - 'extension': extension,
1660 - 'srclang': langKey,
1661 - 'titleKey': subPage.title.replace( / /g, "_")
1662 - } );
1663 - }
1664 - }
1665 - return sources;
1666 - },
1667 -
1668 - /**
1669 - * Return the namespace ( if not encoded on the page return default 102 )
1670 - */
1671 - getTimedTextNS: function() {
1672 - if( this.timedTextNS )
1673 - return this.timedTextNS;
1674 - if ( typeof wgNamespaceIds != 'undefined' && wgNamespaceIds['timedtext'] ) {
1675 - this.timedTextNS = wgNamespaceIds['timedtext'];
1676 - }else{
1677 - //default value is 102 ( probably should store this elsewhere )
1678 - this.timedTextNS = 102;
1679 - }
1680 - return this.timedTextNS;
1681 - },
1682 -
1683 - /**
1684 - * Get the Canonical timed text namespace text
1685 - */
1686 - getCanonicalTimedTextNS: function() {
1687 - return 'TimedText';
1688 - },
1689 -
1690 - /**
1691 - * Check if the language is supported
1692 - */
1693 - isSuportedLang: function( lang_key ) {
1694 - if( mw.Language.names[ lang_key ]) {
1695 - return true;
1696 - }
1697 - return false;
1698 - }
1699 - };
1700 -
1701 - /**
1702 - * jquery timedText binding.
1703 - * Calls mw.timedText on the given selector
1704 - *
1705 - * @param {Object} options Options for the timed text menu
1706 - */
1707 - $.fn.timedText = function ( action, target ) {
1708 - mw.log('fn.timedText:: ' + action + ' t: ' + target );
1709 - var options;
1710 - if( !target ){
1711 - options = action;
1712 - }
1713 - if( typeof options == 'undefined' )
1714 - options = {};
17151144
1716 - $( this.selector ).each(function() {
1717 - var embedPlayer = $(this).get(0);
1718 -
1719 - // Setup timed text for the given player:
1720 - if( ! embedPlayer.timedText ) {
1721 - embedPlayer.timedText = new mw.TimedText( embedPlayer, options);
1722 - }
1723 -
1724 - // Show the timedText menu
1725 - if( action == 'showMenu' ) {
1726 - // Bind the menu to the target with autoShow = true
1727 - mw.log('bind menu fn.timedText');
1728 - embedPlayer.timedText.bindMenu( target, true );
1729 - }
1730 - } );
1731 - };
1732 -
1733 -
1734 - // On new embed player check if we need to add timedText
1735 - $( mw ).bind( 'EmbedPlayerNewPlayer', function( event, embedPlayer ){
1736 - if( mw.isTimedTextSupported( embedPlayer) ){
1737 - if( ! embedPlayer.timedText && mw.TimedText ) {
1738 - embedPlayer.timedText = new mw.TimedText( embedPlayer );
1739 - }
1740 - }
1741 - });
1742 -
17431145 } )( window.mediaWiki, window.jQuery );
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.php
@@ -3,22 +3,17 @@
44 // Register all the timedText modules
55 return array(
66 "mw.TimedText" => array(
7 - 'scripts' => "resources/mw.TimedText.js",
 7+ 'scripts' => array(
 8+ "resources/mw.TimedText.js",
 9+ "resources/mw.TextSource.js",
 10+ ),
811 'styles' => "resources/mw.style.TimedText.css",
912 'dependencies' => array(
1013 'mw.EmbedPlayer',
1114 'mw.Api',
1215 'mw.Language.names',
13 - 'jquery.ui.dialog',
 16+ 'jquery.ui.dialog',
1417 ),
1518 'messageFile' => 'TimedText.i18n.php',
16 - ),
17 - "mw.TimedTextEdit" => array(
18 - 'scripts' => "resources/mw.TimedTextEdit.js",
19 - 'styles' => "resources/mw.style.TimedTextEdit.css",
20 - 'dependencies' => array(
21 - 'mw.TimedText',
22 - 'jquery.ui.tabs'
23 - )
2419 )
2520 );
\ No newline at end of file

Follow-up revisions

RevisionCommit summaryAuthorDate
r105932per r104400#c26664 fixes to font size for em mapping...dale21:30, 12 December 2011
r106732per MDale's comment in CR, this isn't meant to be commented out...raindrift00:07, 20 December 2011

Comments

#Comment by Raindrift (talk | contribs)   23:32, 28 November 2011
+			//for( var i = 0; i < this.enabledSources.length ; i++ ) {
+				//var source = this.enabledSources[ i ];
+				var source = this.enabledSources[ 0 ];
+				if( source ) {
+					this.updateSourceDisplay( source, currentTime );
+				}
+			//}

Did you mean to leave these commented, or is this leftover debugging cruft that was committed by accident?

+			// 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( 
+				$('')
+					.css( this.getCaptionCss() )
+					.html( caption.content )
+			);

If the TODO here is left unfixed, does it introduce an XSS vulnerability? Is the caption sanitized elsewhere? If not, what about using wikitext for the caption instead of HTML?

+				var emFontMap = { '6': .375, '7': .438, '8' : .5, '9': .563, '10': .625, '11':.688,
+						'12':.75, '13': .813, '14': .875, '15':.938, '16':1, '17':1.063, '18': 1.125, '19': 1.888,
+						'20':1.25, '21':1.313, '22':1.375, '23':1.438, '24':1.5};

Would it make more sense to just do pointSize * .0625, then round to 3 decimal places? Also, is it necessary to account for font sizes that are not the 16px default, like when the screen is zoomed? I know it's possible to measure the height of 1em using a trick like the one here: http://stackoverflow.com/questions/739940/detect-browser-font-size but I'm not sure if that's necessary or appropriate in this context. It's hard to tell how this code is actually used from the diff. Perhaps it's not really important that it be correct in all cases.

#Comment by Mdale (talk | contribs)   21:32, 12 December 2011
  • nope .. not meant to be commented out ( follow up in r105932 )
  • No this is not an XSS because we only load caption text that has been put through the mediawiki render. But in principal it would be good to scrub that html. ( do we have a utility to do that yet? ) Added a note.
  • originally the mapping was non-linear .. but now its linear i guess so I will just use a function call.
#Comment by Raindrift (talk | contribs)   20:00, 12 December 2011

We're reviewing this in preparation for deployment next month. Please don't defer it.

Status & tagging log