2 "mwe-select_transcript_set" : "Select subtitles",
3 "mwe-auto_scroll" : "auto scroll",
5 "mwe-improve_transcript" : "Improve",
6 "mwe-no_text_tracks_found" : "No text subtitles found",
7 "mwe-add-edit-subs" : "Add/edit subtitles"
9 // text interface object (for inline display captions)
10 var mvTextInterface = function( parentEmbed
) {
11 return this.init( parentEmbed
);
13 mvTextInterface
.prototype = {
14 text_lookahead_time
:0,
16 default_time_range
: "source", // by default just use the source don't get a time-range
19 add_to_end_on_this_pass
:false,
26 init:function( parentEmbed
) {
27 // init a new availableTracks obj:
28 this.availableTracks
= new Array();
29 // set the parent embed object:
30 this.pe
= parentEmbed
;
31 // parse roe if not already done:
34 // @@todo separate out data loader & data display
35 getTextTracks:function() {
36 // js_log("load timed text from roe: "+ this.pe.roe);
38 // if roe not yet loaded do load it:
39 if ( this.pe
.roe
|| _this
.pe
.wikiTitleKey
) {
40 if ( !this.pe
.media_element
.addedROEData
) {
41 $j( '#mv_txt_load_' + _this
.pe
.id
).show(); // show the loading icon
43 do_request( _this
.pe
.roe
, function( data
)
45 _this
.pe
.media_element
.addROE( data
);
46 _this
.getParseTimedText_rowReady();
48 } else if ( _this
.pe
.wikiTitleKey
) {
49 // check for a clear namespace key:
50 _this
.getTextTracksWikiTitle()
53 js_log( 'row data ready (no roe request)' );
54 _this
.getParseTimedText_rowReady();
57 if ( this.pe
.media_element
.timedTextSources() ) {
58 _this
.getParseTimedText_rowReady();
60 js_log( 'no roe data or timed text sources' );
64 getTextTracksWikiTitle:function() {
65 var apiUrl
= mw
.getLocalApiUrl();
68 var timedtext_ns
= 102;
69 if ( typeof wgNamespaceIds
!= 'undefined' && wgNamespaceIds
['timedtext'] ) {
70 timedtext_ns
= wgNamespaceIds
['timedtext'];
76 'apprefix' : _this
.pe
.wikiTitleKey
,
77 'apnamespace' : timedtext_ns
,
80 }, function( subData
) {
81 if ( subData
.error
&& subData
.error
.code
== 'apunknown_apnamespace' ) {
86 'apprefix' : 'TimedText:' + _this
.pe
.wikiTitleKey
,
88 }, function( subData
) {
89 _this
.doProcSubPages( subData
, wgServer
+ wgScriptPath
);
92 _this
.doProcSubPages( subData
, wgServer
+ wgScriptPath
);
96 doProcSubPages: function( subData
, hostPath
) {
98 // look for text tracks:
99 var foundTextTracks
= false;
101 // get all the known languages:
103 'url': hostPath
+ '/api.php',
106 'siprop' : 'languages'
108 }, function( langDataRaw
) {
110 var lagRaw
= langDataRaw
.query
.languages
;
111 for ( var j
in lagRaw
) {
112 langData
[ lagRaw
[j
].code
] = lagRaw
[j
]['*'];
114 for ( var i
in subData
.query
.allpages
) {
115 var subPage
= subData
.query
.allpages
[i
];
116 var langKey
= subPage
.title
.split( '.' );
117 var extension
= langKey
.pop();
118 langKey
= langKey
.pop();
119 if ( ! _this
.suportedMime
[ extension
] ) {
120 js_log( 'Error: unknown extension:' + extension
);
124 if ( !langData
[ langKey
] ) {
125 js_log( 'Error: langkey:' + langKey
+ ' not found' );
127 var textElm
= document
.createElement( 'text' );
128 $j( textElm
).attr( {
131 'type' : _this
.suportedMime
[ extension
],
132 'title' : langData
[ langKey
]
134 // We use the api since ?action raw on the real title has cache issues
135 $j( textElm
).attr( {
136 'apisrc' : hostPath
+ '/api.php',
137 'titleKey' : subPage
.title
139 _this
.pe
.media_element
.tryAddSource( textElm
);
140 foundTextTracks
= true;
143 // after all text loaded (or we have allready checked commons
144 if ( foundTextTracks
|| hostPath
.indexOf( 'commons.wikimedia' ) !== -1 ) {
145 // alert('calling getParseTimedText_rowReady ');
146 _this
.getParseTimedText_rowReady();
148 _this
.checkSharedRepo();
152 checkSharedRepo:function() {
154 js_log( 'checking for shared value of image' );
155 // check if its a shared repo
159 'titles': 'File:' + _this
.pe
.wikiTitleKey
,
162 }, function( data
) {
163 if ( data
.query
.pages
&& data
.query
.pages
['-1'] && data
.query
.pages
['-1'].imagerepository
== 'shared' ) {
164 js_log( 'image is shared checking commons for subtitles' );
165 // found shared repo assume commons:
167 'url': mw
.commons_api_url
,
170 'apprefix' : _this
.pe
.wikiTitleKey
,
173 }, function( data
) {
174 _this
.editlink
= 'http://commons.wikimedia.org/wiki/TimedText:' + _this
.pe
.wikiTitleKey
+ '.' + wgUserLanguage
+ '.srt';
175 _this
.doProcSubPages( data
, 'http://commons.wikimedia.org/w/' );
178 // no shared repo do normal proc
179 _this
.getParseTimedText_rowReady();
183 getParseTimedText_rowReady: function () {
185 var found_tracks
= false;
186 // create timedTextObj
187 js_log( "mv_txt_load_:SHOW mv_txt_load_" );
188 $j( '#mv_txt_load_' + _this
.pe
.id
).show(); // show the loading icon
191 if ( _this
.editlink
== '' ) {
192 if ( this.pe
.media_element
.linkback
) {
193 _this
.editlink
= this.pe
.media_element
.linkback
;
194 } else if ( this.pe
.wikiTitleKey
&& wgServer
&& wgScript
) { // check for wikiTitleKey (for edit linkback)
196 _this
.editlink
= wgServer
+ wgScript
+ '?title=TimedText:' + this.pe
.wikiTitleKey
+ '.' + wgUserLanguage
+ '.srt&action=edit';
199 $j
.each( this.pe
.media_element
.sources
, function( inx
, source
) {
200 if ( typeof source
.id
== 'undefined' || source
.id
== null ) {
201 source
.id
= 'text_' + inx
;
203 var tObj
= new timedTextObj( source
);
204 // make sure its a valid timed text format (we have not loaded or parsed yet) : (
205 if ( tObj
.lib
!= null ) {
206 _this
.availableTracks
.push( tObj
);
207 // display requested language if we can know that:
208 if ( ( typeof wgUserLanguage
!= 'undefined' && source
['lang'] == wgUserLanguage
) || source
['default'] == "true" ) {
209 // we did set at least one track by default tag
211 _this
.loadAndDisplay( _this
.availableTracks
.length
- 1 );
213 // don't load the track and don't display
218 // no default clip found take the userLanguage key if set:
219 if ( !found_tracks
) {
220 $j
.each( _this
.availableTracks
, function( inx
, source
) {
221 _this
.loadAndDisplay( inx
);
223 // return after loading first available
228 // if nothing found anywhere give the not found msg:
229 if ( !found_tracks
) {
230 $j( '#metaBox_' + _this
.pe
.id
).html( '' +
231 '<h3>' + gM( 'mwe-no_text_tracks_found' ) + '</h3>' +
232 '<a href="' + _this
.editlink
+ '">' + gM( 'mwe-add-edit-subs' ) + '</a>'
236 loadAndDisplay: function ( track_id
) {
238 $j( '#mv_txt_load_' + _this
.pe
.id
).show();// show the loading icon
239 _this
.availableTracks
[ track_id
].load( _this
.default_time_range
, function() {
240 $j( '#mv_txt_load_' + _this
.pe
.id
).hide();
241 _this
.addTrack( track_id
);
244 addTrack: function( track_id
) {
245 js_log( 'f:displayTrack:' + track_id
);
247 // set the display flag to true:
248 _this
.availableTracks
[ track_id
].display
= true;
251 js_log( "SHOULD ADD: track:" + track_id
+ ' count:' + _this
.availableTracks
[ track_id
].textNodes
.length
);
253 // a flag to avoid checking all clips if we know we are adding to the end:
254 _this
.add_to_end_on_this_pass
= false;
256 // run clip adding on a timed interval to not lock the browser on large srt file merges (should use worker threads)
258 var track_id
= track_id
;
259 var addNextClip = function() {
260 var text_clip
= _this
.availableTracks
[ track_id
].textNodes
[i
];
262 _this
.add_merge_text_clip( text_clip
, track_id
);
264 if ( i
< _this
.availableTracks
[ track_id
].textNodes
.length
) {
265 setTimeout( addNextClip
, 1 );
271 add_merge_text_clip: function( text_clip
, track_id
) {
273 // make sure the clip does not already exist:
274 if ( $j( '#tc_' + text_clip
.id
).length
== 0 ) {
275 var inserted
= false;
276 var text_clip_start_time
= npt2seconds( text_clip
.start
);
278 var insertHTML
= '<div id="tc_' + text_clip
.id
+ '" ' +
279 'start_sec="' + text_clip_start_time
+ '" ' +
280 'start="' + text_clip
.start
+ '" end="' + text_clip
.end
+ '" ' +
281 'class="mvtt track_' + track_id
+ '">' +
282 '<div class="mvttseek" style="top:0px;left:0px;right:0px;height:20px;font-size:small">' +
283 text_clip
.start
+ ' to ' + text_clip
.end
+
287 if ( !_this
.add_to_end_on_this_pass
) {
288 $j( '#mmbody_' + this.pe
.id
+ ' .mvtt' ).each( function() {
290 if ( $j( this ).attr( 'start_sec' ) > text_clip_start_time
) {
292 $j( this ).before( insertHTML
);
295 _this
.add_to_end
= true;
299 // js_log('should just add to end: '+insertHTML);
301 $j( '#mmbody_' + this.pe
.id
).append( insertHTML
);
304 // apply the mouse over transcript seek/click functions:
305 $j( ".mvttseek" ).click( function() {
306 _this
.pe
.doSeek( $j( this ).parent().attr( "start_sec" ) / _this
.pe
.getDuration() );
308 $j( ".mvttseek" ).hoverIntent( {
309 interval
:200, // polling interval
310 timeout
:200, // delay before onMouseOut
312 js_log( 'mvttseek: over' );
313 $j( this ).parent().addClass( 'tt_highlight' );
314 // do section highlight
315 _this
.pe
.highlightPlaySection( {
316 'start' : $j( this ).parent().attr( "start" ),
317 'end' : $j( this ).parent().attr( "end" )
321 js_log( 'mvttseek: out' );
322 $j( this ).parent().removeClass( 'tt_highlight' );
323 // de highlight section
324 _this
.pe
.hideHighlight();
330 setup_layout:function() {
332 // check if we have already loaded the menu/body:
333 if ( $j( '#tt_mmenu_' + this.pe
.id
).length
== 0 ) {
334 // alert( this.availableTracks.length );
335 if ( this.availableTracks
.length
!= 0 ) {
336 $j( '#metaBox_' + this.pe
.id
).html(
340 this.doMenuBindings();
345 // setup layout if not already done:
347 // display the interface if not already displayed:
348 $j( '#metaBox_' + this.pe
.id
).fadeIn( "fast" );
349 // start the autoscroll timer:
350 if ( this.autoscroll
)
351 this.setAutoScroll();
355 $j( '#metaBox_' + this.pe
.id
).fadeOut( 'fast' );
357 $j( '#metaButton_' + this.pe
.id
).fadeIn( 'fast' );
360 return '<div id="mmbody_' + this.pe
.id
+ '" ' +
361 'style="position:absolute;top:30px;left:0px;' +
362 'right:0px;bottom:0px;' +
363 'height:' + ( this.pe
.height
- 30 ) +
364 'px;overflow:auto;"><span style="display:none;" id="mv_txt_load_' + this.pe
.id
+ '">' +
365 mv_get_loading_img() + '</span>' +
368 getTsSelect:function() {
370 js_log( 'getTsSelect' );
371 var selHTML
= '<div id="mvtsel_' + this.pe
.id
+ '" style="position:absolute;background:#FFF;top:30px;left:0px;right:0px;bottom:0px;overflow:auto;">';
372 selHTML
+= '<b>' + gM( 'mwe-select_transcript_set' ) + '</b><ul>';
374 for ( var i
in _this
.availableTracks
) { // for in loop ok on object
375 var checked
= ( _this
.availableTracks
[i
].display
) ? 'checked' : '';
376 selHTML
+= '<li><input name="language" value="' + i
+ '" class="mvTsSelect" type="radio" ' + checked
+ '>' +
377 _this
.availableTracks
[i
].getTitle() + '</li>';
381 $j( '#metaBox_' + _this
.pe
.id
).append( selHTML
);
382 $j( '.mvTsSelect' ).click( function() {
383 _this
.applyTsSelect();
386 applyTsSelect:function() {
388 // update availableTracks
389 $j( '#mvtsel_' + this.pe
.id
+ ' .mvTsSelect' ).each( function() {
390 var track_id
= $j( this ).val();
391 if ( this.checked
) {
392 // if not yet loaded now would be a good time
393 if ( ! _this
.availableTracks
[ track_id
].loaded
) {
394 _this
.loadAndDisplay( track_id
);
396 _this
.availableTracks
[track_id
].display
= true;
397 // display the named class:
398 $j( '#mmbody_' + _this
.pe
.id
+ ' .track_' + track_id
).show();
401 if ( _this
.availableTracks
[track_id
].display
) {
402 _this
.availableTracks
[track_id
].display
= false;
404 $j( '#mmbody_' + _this
.pe
.id
+ ' .track_' + track_id
).hide();
408 $j( '#mvtsel_' + _this
.pe
.id
).fadeOut( "fast" ).remove();
412 // grab the time from the video object
413 var cur_time
= this.pe
.currentTime
;
414 if ( cur_time
!= 0 ) {
415 var search_for_range
= true;
416 // check if the current transcript is already where we want:
417 if ( $j( '#mmbody_' + this.pe
.id
+ ' .tt_scroll_highlight' ).length
!= 0 ) {
418 var curhl
= $j( '#mmbody_' + this.pe
.id
+ ' .tt_scroll_highlight' ).get( 0 );
419 if ( npt2seconds( $j( curhl
).attr( 'start' ) ) < cur_time
&&
420 npt2seconds( $j( curhl
).attr( 'end' ) ) > cur_time
) {
421 /*js_log('in range of current hl: ' +
422 npt2seconds($j(curhl).attr('start')) + ' to ' + npt2seconds($j(curhl).attr('end')));
424 search_for_range
= false;
426 search_for_range
= true;
427 // remove the highlight from all:
428 $j( '#mmbody_' + this.pe
.id
+ ' .tt_scroll_highlight' ).removeClass( 'tt_scroll_highlight' );
431 /*js_log('search_for_range:'+search_for_range + ' for: '+ cur_time);*/
432 if ( search_for_range
) {
433 // search for current time: add tt_scroll_highlight to clip
435 // should do binnary search not iterative
436 // avoid jquery function calls do native loops
437 $j( '#mmbody_' + this.pe
.id
+ ' .mvtt' ).each( function() {
438 if ( npt2seconds( $j( this ).attr( 'start' ) ) < cur_time
&&
439 npt2seconds( $j( this ).attr( 'end' ) ) > cur_time
) {
440 _this
.prevTimeScroll
= cur_time
;
441 $j( '#mmbody_' + _this
.pe
.id
).animate( {
442 scrollTop
: $j( this ).get( 0 ).offsetTop
444 $j( this ).addClass( 'tt_scroll_highlight' );
445 // js_log('should add class to: ' + $j(this).attr('id'));
453 setAutoScroll:function( timer
) {
455 this.autoscroll
= ( typeof timer
== 'undefined' ) ? this.autoscroll
:timer
;
456 if ( this.autoscroll
) {
457 // start the timer if its not already running
458 if ( !this.scrollTimerId
) {
459 var mvElm
= $j('#' + _this
.id
).get(0);
461 this.scrollTimerId
= setInterval( mvElm
.textInterface
.monitor(), 500 );
463 // jump to the current position:
464 var cur_time
= parseInt ( this.pe
.currentTime
);
465 js_log( 'cur time: ' + cur_time
);
468 var scroll_to_id
= '';
469 $j( '#mmbody_' + this.pe
.id
+ ' .mvtt' ).each( function() {
470 if ( cur_time
> npt2seconds( $j( this ).attr( 'start' ) ) ) {
471 _this
.prevTimeScroll
= cur_time
;
472 if ( $j( this ).attr( 'id' ) )
473 scroll_to_id
= $j( this ).attr( 'id' );
476 if ( scroll_to_id
!= '' )
477 $j( '#mmbody_' + _this
.pe
.id
).animate( { scrollTop
: $j( '#' + scroll_to_id
).position().top
} , 'slow' );
480 clearInterval( this.scrollTimerId
);
481 this.scrollTimerId
= 0;
487 // add in loading icon:
488 var as_checked
= ( this.autoscroll
) ? 'checked':'';
489 out
+= '<div id="tt_mmenu_' + this.pe
.id
+ '" class="ui-widget-header" style="font-size:.6em;position:absolute;top:0;height:30px;left:0px;right:0px;">';
490 out
+= $j
.btnHtml( gM( 'mwe-select_transcript_set' ), 'tt-select', 'shuffle' );
492 if ( _this
.editlink
!= '' )
493 out
+= ' ' + $j
.btnHtml( gM( 'mwe-improve_transcript' ), 'tt-improve' );
495 out
+= '<input class="tt-scroll" type="checkbox" ' + as_checked
+ '>' + gM( 'mwe-auto_scroll' );
497 out
+= ' ' + $j
.btnHtml( gM( 'mwe-close' ), 'tt-close', 'circle-close' );
502 doMenuBindings:function() {
504 var mt
= '#tt_mmenu_' + _this
.pe
.id
;
505 $j( mt
+ ' .tt-close' ).unbind().btnBind().click( function() {
506 $j( '#' + _this
.pe
.id
).get( 0 ).closeTextInterface();
509 $j( mt
+ ' .tt-select' ).unbind().btnBind().click( function() {
510 $j( '#' + _this
.pe
.id
).get( 0 ).textInterface
.getTsSelect();
513 $j( mt
+ ' .tt-scroll' ).click( function() {
514 _this
.setAutoScroll( this.checked
);
516 $j( mt
+ ' .tt-improve' ).unbind().btnBind().click( function() {
517 document
.location
.href
= _this
.editlink
;
522 /* text format objects
523 * @@todo allow loading from external lib set
525 var timedTextObj = function( source
) {
526 // @@todo in the future we could support timed text in oggs if they can be accessed via javascript
527 // we should be able to do a HEAD request to see if we can read transcripts from the file.
528 switch( source
.mime_type
) {
537 js_log( source
.mime_type
+ ' is not suported timed text fromat' );
541 // extend with the per-mime type lib:
542 eval( 'var tObj = timedText' + this.lib
+ ';' );
543 for ( var i
in tObj
) {
546 return this.init( source
);
549 // base timedText object
550 timedTextObj
.prototype = {
554 textNodes
:new Array(),
555 init: function( source
) {
556 // copy source properties
557 this.source
= source
;
560 getTitle:function() {
561 return this.source
.title
;
564 return this.source
.src
;
568 // Specific Timed Text formats:
571 load: function( range
, callback
) {
573 js_log( 'textCMML: loading track: ' + this.src
);
575 // :: Load transcript range ::
576 var pcurl
= mw
.parseUri( _this
.getSRC() );
577 // check for urls without time keys:
578 if ( typeof pcurl
.queryKey
['t'] == 'undefined' ) {
579 // in which case just get the full time req:
580 do_request( this.getSRC(), function( data
) {
581 _this
.doParse( data
);
588 var req_time
= pcurl
.queryKey
['t'].split( '/' );
589 req_time
[0] = npt2seconds( req_time
[0] );
590 req_time
[1] = npt2seconds( req_time
[1] );
591 if ( req_time
[1] - req_time
[0] > _this
.request_length
) {
592 // longer than 5 min will only issue a (request 5 min)
593 req_time
[1] = req_time
[0] + _this
.request_length
;
595 // set up request url:
596 url
= pcurl
.protocol
+ '://' + pcurl
.authority
+ pcurl
.path
+ '?';
597 $j
.each( pcurl
.queryKey
, function( key
, val
) {
599 url
+= key
+ '=' + val
+ '&';
601 url
+= 't=' + seconds2npt( req_time
[0] ) + '/' + seconds2npt( req_time
[1] ) + '&';
604 do_request( url
, function( data
) {
605 js_log( "load track clip count:" + data
.getElementsByTagName( 'clip' ).length
);
606 _this
.doParse( data
);
611 doParse: function( data
) {
613 $j
.each( data
.getElementsByTagName( 'clip' ), function( inx
, clip
) {
614 // js_log(' on clip ' + clip.id);
616 start
: $j( clip
).attr( 'start' ).replace( 'npt:', '' ),
617 end
: $j( clip
).attr( 'end' ).replace( 'npt:', '' ),
619 id
: $j( clip
).attr( 'id' )
621 $j
.each( clip
.getElementsByTagName( 'body' ), function( binx
, bn
) {
622 if ( bn
.textContent
) {
623 text_clip
.body
= bn
.textContent
;
624 } else if ( bn
.text
) {
625 text_clip
.body
= bn
.text
;
628 _this
.textNodes
.push( text_clip
);
633 load: function( range
, callback
) {
635 js_log( 'textSRT: loading : ' + _this
.getSRC() );
636 if ( _this
.getSRC() ) {
637 do_request( _this
.getSRC() , function( data
) {
638 _this
.doParse( data
);
642 } else if ( _this
.source
.apisrc
) {
644 'url' : _this
.source
.apisrc
,
646 'titles': _this
.source
.titleKey
,
650 }, function( data
) {
651 if ( data
&& data
.query
&& data
.query
.pages
) {
652 for ( var i
in data
.query
.pages
) {
653 var page
= data
.query
.pages
[i
];
654 if ( page
.revisions
) {
655 for ( var j
in page
.revisions
) {
656 if ( page
.revisions
[j
]['*'] ) {
657 _this
.doParse( page
.revisions
[j
]['*'] );
668 doParse:function( data
) {
669 // split up the transcript chunks:
671 var tc
= data
.split( /[\r]?\n[\r]?\n/ );
672 // pushing can take time
673 for ( var s
= 0; s
< tc
.length
; s
++ ) {
674 var st
= tc
[s
].split( '\n' );
675 if ( st
.length
>= 2 ) {
677 var i
= st
[1].split( ' --> ' )[0].replace( /^\s+|\s+$/g, "" );
678 var o
= st
[1].split( ' --> ' )[1].replace( /^\s+|\s+$/g, "" );
680 if ( st
.length
> 2 ) {
681 for ( j
= 3; j
< st
.length
; j
++ )
688 "id": this.id
+ '_' + n
,
691 this.textNodes
.push( text_clip
);