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