* winks at Michael Dale and mumbles something about Title Case *
[lhc/web/wiklou.git] / js2 / mwEmbed / libTimedText / mvTextInterface.js
1
2 loadGM({
3 "mwe-select_transcript_set" : "Select layers",
4 "mwe-auto_scroll" : "auto scroll",
5 "mwe-close" : "close",
6 "mwe-improve_transcript" : "Improve",
7 "mwe-no_text_tracks_found" : "No text tracks were found",
8 "mwe-subtitles" : "$1 subtitles"
9 })
10 // text interface object (for inline display captions)
11 var mvTextInterface = function( parentEmbed ){
12 return this.init( parentEmbed );
13 }
14 mvTextInterface.prototype = {
15 text_lookahead_time:0,
16 body_ready:false,
17 default_time_range: "source", //by default just use the source don't get a time-range
18 transcript_set:null,
19 autoscroll:true,
20 add_to_end_on_this_pass:false,
21 scrollTimerId:0,
22 init:function( parentEmbed ){
23 //init a new availableTracks obj:
24 this.availableTracks={};
25 //set the parent embed object:
26 this.pe=parentEmbed;
27 //parse roe if not already done:
28 this.getTimedTextTracks();
29 },
30 //@@todo separate out data loader & data display
31 getTimedTextTracks:function(){
32 js_log("load timed text from roe: "+ this.pe.roe);
33 var _this = this;
34 var apiUrl = mwGetLocalApiUrl();
35 //if roe not yet loaded do load it:
36 if(this.pe.roe || _this.pe.wikiTitleKey ){
37 if(!this.pe.media_element.addedROEData){
38 js_log("load roe data!");
39 $j('#mv_txt_load_'+_this.pe.id).show(); //show the loading icon
40 if(_this.pe.roe){
41 do_request( _this.pe.roe, function(data)
42 {
43 //continue
44 _this.pe.media_element.addROE(data);
45 _this.getParseTimedText_rowReady();
46 });
47 }else if( _this.pe.wikiTitleKey ){
48 do_api_req({
49 'url': apiUrl,
50 'data': {
51 'list' : 'allpages',
52 'apprefix' : 'TimedText:' + _this.pe.wikiTitleKey
53 }
54 }, function( subData ) {
55 do_api_req({
56 'url': apiUrl,
57 'data': {
58 'meta' : 'siteinfo',
59 'siprop' : 'languages'
60 }
61 }, function( langDataRaw ) {
62 var langData = {};
63 var lagRaw = langDataRaw.query.languages;
64 for(var j in lagRaw){
65 langData[ lagRaw[j].code ] = lagRaw[j]['*'];
66 }
67 for(var i in subData.query.allpages){
68 var subPage = subData.query.allpages[i];
69 langKey = subPage.title.split('.');
70 langKey = langKey[ langKey.length-2 ];
71 if( !langData[ langKey] ){
72 js_log('Error: langkey:'+ langKey + ' not found');
73 }else{
74 var textElm = document.createElement('text');
75 $j(textElm).attr({
76 'category' : 'SUB',
77 'lang' : langKey,
78 'type' : "text/x-srt",
79 'title' : gM('mwe-subtitles', langData[ langKey]),
80 'src' : wgServer + wgScript + '?title=' + subPage.title + '&action=raw'
81 });
82 _this.pe.media_element.tryAddSource( textElm );
83 _this.getParseTimedText_rowReady();
84 }
85 }
86 }); //do_api_req({
87 }); //function( subData ) {
88 }
89 }else{
90 js_log('row data ready (no roe request)');
91 _this.getParseTimedText_rowReady();
92 }
93 }else{
94 if( this.pe.media_element.timedTextSources() ){
95 _this.getParseTimedText_rowReady();
96 }else{
97 js_log('no roe data or timed text sources');
98 }
99 }
100 },
101 getParseTimedText_rowReady: function (){
102 var _this = this;
103 //create timedTextObj
104 var default_found=false;
105 js_log("mv_txt_load_:SHOW mv_txt_load_");
106 $j('#mv_txt_load_'+_this.pe.id).show(); //show the loading icon
107
108 $j.each( this.pe.media_element.sources, function(inx, source){
109
110 if( typeof source.id == 'undefined' || source.id == null ){
111 source.id = 'tt_' + inx;
112 }
113 var tObj = new timedTextObj( source );
114 //make sure its a valid timed text format (we have not loaded or parsed yet) : (
115 if( tObj.lib != null ){
116 js_log('adding Track: ' + source.id + ' to ' + _this.pe.id);
117 _this.availableTracks[ source.id ] = tObj;
118 //js_log( 'is : ' + source.id + ' default: ' + source.default );
119 //display if requested:
120 if( source['default'] == "true" ){
121 //we did set at least one track by default tag
122 default_found=true;
123 js_log('do load timed text: ' + source.id );
124 _this.loadAndDisplay( source.id );
125 }else{
126 //don't load the track and don't display
127 }
128 }
129 });
130
131 //no default clip found take the first_id
132 if(!default_found){
133 $j.each( _this.availableTracks, function(inx, sourceTrack){
134 _this.loadAndDisplay( sourceTrack.id );
135 default_found=true;
136 //retun after loading first available
137 return false;
138 });
139 }
140
141 //if nothing found anywhere update the loading icon to say no tracks found
142 if(!default_found)
143 $j('#mv_txt_load_'+_this.pe.id).html( gM('mwe-no_text_tracks_found') );
144
145
146 },
147 loadAndDisplay: function ( track_id){
148 var _this = this;
149 $j('#mv_txt_load_'+_this.pe.id).show();//show the loading icon
150 _this.availableTracks[ track_id ].load(_this.default_time_range, function(){
151 $j('#mv_txt_load_'+_this.pe.id).hide();
152 _this.addTrack( track_id );
153 });
154 },
155 addTrack: function( track_id ){
156 js_log('f:displayTrack:'+ track_id);
157 var _this = this;
158 //set the display flag to true:
159 _this.availableTracks[ track_id ].display=true;
160 //setup the layout:
161 this.setup_layout();
162 js_log("SHOULD ADD: "+ track_id + ' count:' + _this.availableTracks[ track_id ].textNodes.length);
163
164 //a flag to avoid checking all clips if we know we are adding to the end:
165 _this.add_to_end_on_this_pass = false;
166
167 //run clip adding on a timed interval to not lock the browser on large srt file merges (should use worker threads)
168 var i =0;
169 var track_id = track_id;
170 var addNextClip = function(){
171 var text_clip = _this.availableTracks[ track_id ].textNodes[i];
172 _this.add_merge_text_clip(text_clip);
173 i++;
174 if(i < _this.availableTracks[ track_id ].textNodes.length){
175 setTimeout(addNextClip, 1);
176 }
177 }
178 addNextClip();
179 },
180 add_merge_text_clip: function( text_clip ){
181 var _this = this;
182 //make sure the clip does not already exist:
183 if($j('#tc_'+text_clip.id).length==0){
184 var inserted = false;
185 var text_clip_start_time = npt2seconds( text_clip.start );
186
187 var insertHTML = '<div id="tc_'+text_clip.id+'" ' +
188 'start_sec="' + text_clip_start_time + '" ' +
189 'start="'+text_clip.start+'" end="'+text_clip.end+'" class="mvtt tt_'+text_clip.type_id+'">' +
190 '<div class="mvttseek" style="top:0px;left:0px;right:0px;height:20px;font-size:small">'+
191 text_clip.start + ' to ' +text_clip.end+
192 '</div>'+
193 text_clip.body +
194 '</div>';
195 //js_log("ADDING CLIP: " + text_clip_start_time + ' html: ' + insertHTML);
196 if(!_this.add_to_end_on_this_pass){
197 $j('#mmbody_'+this.pe.id +' .mvtt').each(function(){
198 if(!inserted){
199 //js_log( npt2seconds($j(this).attr('start')) + ' > ' + text_clip_start_time);
200 if( $j(this).attr('start_sec') > text_clip_start_time){
201 inserted=true;
202 $j(this).before(insertHTML);
203 }
204 }else{
205 _this.add_to_end = true;
206 }
207 });
208 }
209 //js_log('should just add to end: '+insertHTML);
210 if(!inserted){
211 $j('#mmbody_'+this.pe.id ).append(insertHTML);
212 }
213
214 //apply the mouse over transcript seek/click functions:
215 $j(".mvttseek").click( function() {
216 _this.pe.doSeek( $j(this).parent().attr("start_sec") / _this.pe.getDuration() );
217 });
218 $j(".mvttseek").hoverIntent({
219 interval:200, //polling interval
220 timeout:200, //delay before onMouseOut
221 over:function () {
222 js_log('mvttseek: over');
223 $j(this).parent().addClass('tt_highlight');
224 //do section highlight
225 _this.pe.highlightPlaySection( {
226 'start' : $j(this).parent().attr("start"),
227 'end' : $j(this).parent().attr("end")
228 });
229 },
230 out:function () {
231 js_log('mvttseek: out');
232 $j(this).parent().removeClass('tt_highlight');
233 //de highlight section
234 _this.pe.hideHighlight();
235 }
236 }
237 );
238 }
239 },
240 setup_layout:function(){
241 //check if we have already loaded the menu/body:
242 if($j('#tt_mmenu_'+this.pe.id).length==0){
243 $j('#metaBox_'+this.pe.id).html(
244 this.getMenu() +
245 this.getBody()
246 );
247 this.doMenuBindings();
248 }
249 },
250 show:function(){
251 //setup layout if not already done:
252 this.setup_layout();
253 //display the interface if not already displayed:
254 $j('#metaBox_'+this.pe.id).fadeIn("fast");
255 //start the autoscroll timer:
256 if( this.autoscroll )
257 this.setAutoScroll();
258 },
259 close:function(){
260 //the meta box:
261 $j('#metaBox_'+this.pe.id).fadeOut('fast');
262 //the icon link:
263 $j('#metaButton_'+this.pe.id).fadeIn('fast');
264 },
265 getBody:function(){
266 return '<div id="mmbody_'+this.pe.id+'" ' +
267 'style="position:absolute;top:30px;left:0px;' +
268 'right:0px;bottom:0px;' +
269 'height:'+(this.pe.height-30)+
270 'px;overflow:auto;"><span style="display:none;" id="mv_txt_load_' + this.pe.id + '">'+
271 gM('mwe-loading_txt')+'</span>' +
272 '</div>';
273 },
274 getTsSelect:function(){
275 var _this = this;
276 js_log('getTsSelect');
277 var selHTML = '<div id="mvtsel_' + this.pe.id + '" style="position:absolute;background:#FFF;top:30px;left:0px;right:0px;bottom:0px;overflow:auto;">';
278 selHTML+='<b>' + gM('mwe-select_transcript_set') + '</b><ul>';
279 //debugger;
280 for(var i in _this.availableTracks){ //for in loop ok on object
281 var checked = ( _this.availableTracks[i].display ) ? 'checked' : '';
282 selHTML+='<li><input name="'+i+'" class="mvTsSelect" type="checkbox" ' + checked + '>'+
283 _this.availableTracks[i].getTitle() + '</li>';
284 }
285 selHTML+='</ul>' +
286 '<a href="#" onClick="document.getElementById(\'' + this.pe.id + '\').textInterface.applyTsSelect();return false;">'+gM('mwe-close')+'</a>'+
287 '</div>';
288 $j('#metaBox_'+_this.pe.id).append( selHTML );
289 },
290 applyTsSelect:function(){
291 var _this = this;
292 //update availableTracks
293 $j('#mvtsel_'+this.pe.id+' .mvTsSelect').each(function(){
294 if(this.checked){
295 var track_id = this.name;
296 //if not yet loaded now would be a good time
297 if(! _this.availableTracks[ track_id ].loaded ){
298 _this.loadAndDisplay( track_id);
299 }else{
300 _this.availableTracks[this.name].display=true;
301 //display the named class:
302 $j('#mmbody_'+_this.pe.id +' .tt_'+this.name ).fadeIn("fast");
303 }
304 }else{
305 if(_this.availableTracks[this.name].display){
306 _this.availableTracks[this.name].display=false;
307 //hide unchecked
308 $j('#mmbody_'+_this.pe.id +' .tt_'+this.name ).fadeOut("fast");
309 }
310 }
311 });
312 $j('#mvtsel_'+_this.pe.id).fadeOut("fast").remove();
313 },
314 monitor:function(){
315 _this = this;
316 //grab the time from the video object
317 var cur_time = this.pe.currentTime ;
318 if( cur_time!=0 ){
319 var search_for_range = true;
320 //check if the current transcript is already where we want:
321 if($j('#mmbody_'+this.pe.id +' .tt_scroll_highlight').length != 0){
322 var curhl = $j('#mmbody_'+this.pe.id +' .tt_scroll_highlight').get(0);
323 if(npt2seconds($j(curhl).attr('start') ) < cur_time &&
324 npt2seconds($j(curhl).attr('end') ) > cur_time){
325 /*js_log('in range of current hl: ' +
326 npt2seconds($j(curhl).attr('start')) + ' to ' + npt2seconds($j(curhl).attr('end')));
327 */
328 search_for_range = false;
329 }else{
330 search_for_range = true;
331 //remove the highlight from all:
332 $j('#mmbody_'+this.pe.id +' .tt_scroll_highlight').removeClass('tt_scroll_highlight');
333 }
334 };
335 /*js_log('search_for_range:'+search_for_range + ' for: '+ cur_time);*/
336 if( search_for_range ){
337 //search for current time: add tt_scroll_highlight to clip
338 // optimize:
339 // should do binnary search not iterative
340 // avoid jquery function calls do native loops
341 $j('#mmbody_'+this.pe.id +' .mvtt').each(function(){
342 if(npt2seconds($j(this).attr('start') ) < cur_time &&
343 npt2seconds($j(this).attr('end') ) > cur_time){
344 _this.prevTimeScroll=cur_time;
345 $j('#mmbody_'+_this.pe.id).animate({
346 scrollTop: $j(this).get(0).offsetTop
347 }, 'slow');
348 $j(this).addClass('tt_scroll_highlight');
349 //js_log('should add class to: ' + $j(this).attr('id'));
350 //done with loop
351 return false;
352 }
353 });
354 }
355 }
356 },
357 setAutoScroll:function( timer ){
358 var _this = this;
359 this.autoscroll = ( typeof timer=='undefined' )?this.autoscroll:timer;
360 if(this.autoscroll){
361 //start the timer if its not already running
362 if(!this.scrollTimerId){
363 this.scrollTimerId = setInterval('$j(\'#'+_this.pe.id+'\').get(0).textInterface.monitor()', 500);
364 }
365 //jump to the current position:
366 var cur_time = parseInt (this.pe.currentTime );
367 js_log('cur time: '+ cur_time);
368
369 _this = this;
370 var scroll_to_id='';
371 $j('#mmbody_'+this.pe.id +' .mvtt').each(function(){
372 if(cur_time > npt2seconds($j(this).attr('start')) ){
373 _this.prevTimeScroll=cur_time;
374 if( $j(this).attr('id') )
375 scroll_to_id = $j(this).attr('id');
376 }
377 });
378 if(scroll_to_id != '')
379 $j( '#mmbody_' + _this.pe.id ).animate( { scrollTop: $j('#'+scroll_to_id).position().top } , 'slow' );
380 }else{
381 //stop the timer
382 clearInterval(this.scrollTimerId);
383 this.scrollTimerId=0;
384 }
385 },
386 getMenu:function(){
387 var out='';
388 //add in loading icon:
389 var as_checked = (this.autoscroll)?'checked':'';
390 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;">' +
391 $j.btnHtml(gM('mwe-select_transcript_set'), 'tt-select', 'shuffle');
392 if(this.pe.media_element.linkback){
393 out+=' ' + $j.btnHtml(gM('mwe-improve_transcript'), 'tt-improve', 'document', {href:this.pe.media_element.linkback, target:'_new'});
394 }
395 out+='<input onClick="document.getElementById(\''+this.pe.id+'\').textInterface.setAutoScroll(this.checked);return false;" ' +
396 'type="checkbox" '+as_checked +'>'+gM('mwe-auto_scroll') + ' ' +
397 $j.btnHtml(gM('mwe-close'), 'tt-close', 'circle-close');
398 out+='</div>';
399 return out;
400 },
401 doMenuBindings:function(){
402 var _this = this;
403 var mt = '#tt_mmenu_'+ _this.pe.id;
404 $j(mt + ' .tt-close').unbind().btnBind().click(function(){
405 $j( '#' + _this.pe.id).get(0).closeTextInterface();
406 return false;
407 });
408 $j(mt + ' .tt-select').unbind().btnBind().click(function(){
409 $j( '#' + _this.pe.id).get(0).textInterface.getTsSelect();
410 return false;
411 });
412 //use hard-coded link:
413 $j(mt + ' .tt-improve').btnBind();
414 }
415 }
416
417 /* text format objects
418 * @@todo allow loading from external lib set
419 */
420 var timedTextObj = function( source ){
421 //@@todo in the future we could support timed text in oggs if they can be accessed via javascript
422 //we should be able to do a HEAD request to see if we can read transcripts from the file.
423 switch( source.mime_type ){
424 case 'text/cmml':
425 this.lib = 'CMML';
426 break;
427 case 'text/srt':
428 case 'text/x-srt':
429 this.lib = 'SRT';
430 break;
431 default:
432 js_log( source.mime_type + ' is not suported timed text fromat');
433 return ;
434 break;
435 }
436 //extend with the per-mime type lib:
437 eval('var tObj = timedText' + this.lib + ';');
438 for( var i in tObj ){
439 this[ i ] = tObj[i];
440 }
441 return this.init( source );
442 }
443
444 //base timedText object
445 timedTextObj.prototype = {
446 loaded: false,
447 lib:null,
448 display: false,
449 textNodes:new Array(),
450 init: function( source ){
451 //copy source properties
452 this.source = source;
453 this.id = source.id;
454 },
455 getTitle:function(){
456 return this.source.title;
457 },
458 getSRC:function(){
459 return this.source.src;
460 }
461 }
462
463 // Specific Timed Text formats:
464
465 timedTextCMML = {
466 load: function( range, callback ){
467 var _this = this;
468 js_log('textCMML: loading track: '+ this.src);
469
470 //:: Load transcript range ::
471
472 var pcurl = parseUri( _this.getSRC() );
473 //check for urls without time keys:
474 if( typeof pcurl.queryKey['t'] == 'undefined'){
475 //in which case just get the full time req:
476 do_request( this.getSRC(), function(data){
477 _this.doParse( data );
478 _this.loaded=true;
479 callback();
480 });
481 return ;
482 }
483 var req_time = pcurl.queryKey['t'].split('/');
484 req_time[0]=npt2seconds(req_time[0]);
485 req_time[1]=npt2seconds(req_time[1]);
486 if(req_time[1]-req_time[0]> _this.request_length){
487 //longer than 5 min will only issue a (request 5 min)
488 req_time[1] = req_time[0]+_this.request_length;
489 }
490 //set up request url:
491 url = pcurl.protocol + '://' + pcurl.authority + pcurl.path +'?';
492 $j.each(pcurl.queryKey, function(key, val){
493 if( key != 't'){
494 url+=key+'='+val+'&';
495 }else{
496 url+= 't=' + seconds2npt(req_time[0]) + '/' + seconds2npt(req_time[1]) + '&';
497 }
498 });
499 do_request( url, function(data){
500 js_log("load track clip count:" + data.getElementsByTagName('clip').length );
501 _this.doParse( data );
502 _this.loaded=true;
503 callback();
504 });
505 },
506 doParse: function(data){
507 var _this = this;
508 $j.each(data.getElementsByTagName('clip'), function(inx, clip){
509 //js_log(' on clip ' + clip.id);
510 var text_clip = {
511 start: $j(clip).attr('start').replace('npt:', ''),
512 end: $j(clip).attr('end').replace('npt:', ''),
513 type_id: _this.id,
514 id: $j(clip).attr('id')
515 }
516 $j.each( clip.getElementsByTagName('body'), function(binx, bn ){
517 if(bn.textContent){
518 text_clip.body = bn.textContent;
519 }else if(bn.text){
520 text_clip.body = bn.text;
521 }
522 });
523 _this.textNodes.push( text_clip );
524 });
525 }
526 }
527 timedTextSRT = {
528 load: function( range, callback ){
529 var _this = this;
530 js_log('textSRT: loading : '+ _this.getSRC() );
531 do_request( _this.getSRC() , function(data){
532 _this.doParse( data );
533 _this.loaded=true;
534 callback();
535 });
536 },
537 doParse:function( data ){
538 //split up the transcript chunks:
539 //strip any \r's
540 var tc = data.split(/[\r]?\n[\r]?\n/);
541 //pushing can take time
542 for(var s=0; s < tc.length ; s++) {
543 var st = tc[s].split('\n');
544 if(st.length >=2) {
545 var n = st[0];
546 var i = st[1].split(' --> ')[0].replace(/^\s+|\s+$/g,"");
547 var o = st[1].split(' --> ')[1].replace(/^\s+|\s+$/g,"");
548 var t = st[2];
549 if(st.length > 2) {
550 for(j=3; j<st.length;j++)
551 t += '\n'+st[j];
552 }
553 var text_clip = {
554 "start": i,
555 "end": o,
556 "type_id": this.id,
557 "id": this.id + '_' + n,
558 "body": t
559 }
560 this.textNodes.push( text_clip );
561 }
562 }
563 }
564 };