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