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