source: trunk/termtv/js/player.js@ 17407

Last change on this file since 17407 was 17407, checked in by tbretz, 11 years ago
First version.
File size: 20.2 KB
Line 
1/**
2 * @class TTVPlayer
3 * @constructor
4 * @public
5 *
6 * This class implements things like loading the stream, analyzing
7 * the stream, crawling through the stream and callbacks for the control.
8 *
9 * The stream is displayed in the given canvas object.
10 *
11 * @param {Node} canvas
12 * A reference to a canvas element
13 *
14 * @param {Object} options
15 * An object with a list of options. Possible properties are
16 * <ul>
17 * <li> name: The name (or url) of the stream to be loaded
18 * <li> title: The title to be used in the title bar instead of the
19 * file name
20 * <li> font: The name or url (without extension) of the font to
21 * be loaded
22 * <li> boldfont: The name or url (without extension) of the bold font
23 * to be loaded
24 * <li> scan: if set, the player will display only 1 frame per second
25 * for a fast analysis of the stream
26 * <li> warp: a factor by which the playing is accelerated or decelerated
27 * <li> loop: restarts the stream after the defined number of
28 * milliseconds again after it finished (0: off)
29 * <li> autostart: when everything is ready, after that time, the
30 * stream will start automatically (0: no autostart)
31 * <li> onready: callback to be called when everthing is ready
32 * <li> debug: a function used to send debug messages (takes
33 * a String as argument)
34 * <li> callback: a callback providing special informations from the
35 * emulator, as the title
36 * <li> onupdate: called on each canvas update with the current
37 * position in the file and the fraction already displayed
38 * <li> oncompletion: called when the stream has been finished and will
39 * not be restarted
40 * <li> onbookmark: called when the bookmark has been set
41 * <li> onpause: called whenever the stream was paused
42 * <li> onplay: called whenever the stream starts playing
43 * <li> noskip: forces an update after each decoded chunk, otherwise
44 * only one update per 20ms (50Hz) will be displayed.
45 * <li> fontsize: if fontsize is given the cnavas-element text rendering
46 * engine is used for text rendering. This might not work on all
47 * browsers.
48 * <li> fontfamily: The font-family, if manual canvas-rendering is
49 * switched on with fontsize>0. Do not choose variable size fonts.
50 * Note that the code is waiting for the font to be loaded. So if
51 * the font cannot be loaded, it will wait forever.
52 * </ul>
53 *
54 */
55function TTVPlayer(canvas, options)
56{
57 var This = this;
58
59 // The closure compiler might have decided to rename the
60 // properties, so we 'restore' them. If the code is not
61 // compiles, this statements have no effect. If the code
62 // is compiled, this is the innermost class the user should
63 // be able to access. So outside of this class we access
64 // the object properties always by index (clear name)
65 // and inside and deeper we can access them by property.
66 options.name = options['name'];
67 options.title = options['title'];
68 options.font = options['font'];
69 options.fontsize = options['fontsize'];
70 options.fontfamily = options['fontfamily'];
71 options.boldfont = options['boldfont'];
72 options.scan = options['scan'];
73 options.warp = options['warp'];
74 options.loop = options['loop'];
75 options.autostart = options['autostart'];
76 options.onready = options['onready'];
77 options.debug = options['debug'];
78 options.callback = options['callback'];
79 options.onupdate = options['onupdate'];
80 options.oncompletion = options['oncompletion'];
81 options.onbookmark = options['onbookmark'];
82 options.onpause = options['onpause'];
83 options.onplay = options['onplay'];
84 options.noskip = options['noskip'];
85
86 This.debugStream = function(msg)
87 {
88 if (!This.options.debug)
89 return;
90
91 if (This.stream && This.stream[This.nextFrameIdx])
92 {
93 function pad(v) { return ('0'+v).slice(-2); }
94 var dat = new Date((This.stream[This.nextFrameIdx].time - This.stream[0].time)*1000);
95 options.debug(pad(dat.getUTCHours())+":"+
96 pad(dat.getUTCMinutes())+":"+
97 pad(dat.getUTCSeconds())+" "+msg);
98 }
99 else
100 options.debug("--:--:-- "+msg);
101 }
102
103 This.debug = function(msg)
104 {
105 This.debugStream("player.js: "+msg);
106 }
107
108 // ==========================================================
109
110 This.options = options;
111 if (!This.options.warp)
112 This.options.warp = 1;
113
114 var name = options.name || 'index.rec';
115
116 var xmlStream = new XMLHttpRequest();
117 xmlStream.open('GET', name, true);
118 xmlStream.setRequestHeader("Cache-Control", "no-cache");
119 xmlStream.setRequestHeader("If-Match", "*");
120 xmlStream.overrideMimeType("application/octet-stream; charset=x-user-defined");
121 //xmlStream.overrideMimeType("text/plain; charset=x-user-defined");
122 xmlStream.onload = function ()
123 {
124 // error occured during load
125 if (xmlStream.status!=200)
126 {
127 alert("ERROR[0] - HTTP request '"+name+"': "+xmlStream.statusText+" ["+xmlStream.status+"]");
128 if (options.onclose)
129 options.onclose();
130 return;
131 }
132
133 var stream = xmlStream.responseText;
134
135 // To encrypt a stream do
136 // openssl enc -aes-256-cbc -in infile -out outfile -pass pass:"YourPassphrase" -e -base64
137
138 // This might be base64 encoded
139 // If this is a plain file, most probably already one of the
140 // first few bytes does not match the base64 encoding
141 if (stream.search(/[^\x0a\x0dABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=]/)==-1)
142 {
143 // Remove carriage returns and line feeds from the stream
144 var clean = stream.replace(/[\x0A\x0C]/g, "");
145
146 // Parse as if this is a base64 encoded stream
147 var words = CryptoJS.enc.Base64.parse(clean);
148 var latin1 = CryptoJS.enc.Latin1.stringify(words);
149
150 // Check if it starts with "Salted__", then we really found an encoded stream
151 if (words.words[0] == 0x53616c74 && words.words[1] == 0x65645f5f)
152 {
153 // Ask for the passphrase and decode
154 var pw = prompt("Please enter a passphrase to decrypt the stream: ");
155 if (pw)
156 stream = CryptoJS.AES.decrypt(clean, pw).toString(CryptoJS.enc.Latin1);
157 }
158 }
159
160 try
161 {
162 // Convert the data into chunks. The advantage is that
163 // we have all the decoding done already. The disadvantage
164 // is that for a short moment we have the data twice in memory
165 // [Note: we need to step to the end anyway to get the
166 // length of the stream. Example: TtyRecParse 260ms,
167 // direct stepping: 1ms for ~5MB, ~2500chunks]
168 This.stream = new TTVDecoder(stream);
169 This.totalLength = This.stream[This.stream.length-1].time - This.stream[0].time;
170
171 }
172 catch (e)
173 {
174 alert(e);
175 if (This.options.onclose)
176 This.options.onclose();
177 return;
178 }
179
180 This.debug("GO");
181
182 // The idea with this code was to create a single array
183 // and an index table and crawl through it. Unfortunately,
184 // with the current implementation using regexps for matching,
185 // the regular exporessions would need to support the
186 // y-modifier or one has to create substrings anyways.
187 /*
188 // Decode the stream, create a continous buffer for the
189 // data create a look-up-table with index and time
190 var myStream = { };
191 myStream.index = [];
192 myStream.time = [];
193 myStream.data = "";
194
195 var n = 0;
196 var pos = 0;
197 while (1)
198 {
199 var chunk = getChunk(stream, pos);
200 if (!chunk)
201 break;
202
203 myStream.time[n] = chunk.time;
204 myStream.index[n] = myStream.data.length;
205 myStream.data += chunk.data;
206 n++;
207
208 pos = chunk.pos;
209 }
210 myStream.length = n;
211
212
213 myStream.totalLength = myStream.time[myStream.length-1] - myStream.time[0];
214 This.myStream = myStream;
215
216 var maxx, maxy;
217
218 // Scan the data stream for the size sequence
219 var re2 = new RegExp("\033\\[[8];([0-9]+);([0-9]+)t", "g");
220 while (!maxx && !maxy)
221 {
222 var rc = re2.exec(myStream.data);
223 if (!rc)
224 break;
225
226 This.debug("Found[1]: "+rc[1]+"/"+rc[2]);
227
228 maxx = parseInt(rc[2], 10);
229 maxy = parseInt(rc[1], 10);
230 }
231
232 // Scan the data stream for goto sequence if no size sequence was found
233 var re1 = new RegExp("\033\\[([0-9]+);([0-9]+)r", "g");
234 while (1)
235 {
236 var rc = re1.exec(myStream.data);
237 if (!rc)
238 break;
239
240 This.debug("Found[0]: "+rc[1]+"/"+rc[2]);
241
242 var py = parseInt(rc[2], 10);
243 if (!maxy || py>maxy)
244 {
245 This.debug("Found[0]: "+rc[1]+"/"+py);
246 maxy = py;
247 }
248 }
249
250 This.totalLength = myStream.totalLength;
251 */
252
253 /*
254 // Step through the data and try to find the sequence for
255 // the screen size, or try to get the size from goto
256 // sequences
257 var maxx;
258 var maxy;
259
260
261 var buffer = "";
262
263 var now = new Date();
264
265 var pos = 0;
266 var last;
267 while (1)
268 {
269 var chunk = getChunk(stream, pos);
270 if (!chunk)
271 break;
272
273 buffer += chunk.data;
274 pos = chunk.pos;
275 last = chunk;
276
277 while (!maxx && !maxy)
278 {
279 var rc = re2.exec(buffer);
280 if (!rc)
281 break;
282
283 This.debug("Found[1]: "+rc[1]+"/"+rc[2]);
284
285 maxx = parseInt(rc[2], 10);
286 maxy = parseInt(rc[1], 10);
287 }
288
289 if (maxx && maxy)
290 break;
291
292 while (1)
293 {
294 var rc = re1.exec(buffer);
295 if (!rc)
296 break;
297
298 This.debug("Found[0]: "+rc[1]+"/"+rc[2]);
299
300 var py = parseInt(rc[2], 10);
301 if (!maxy || py>maxy)
302 {
303 This.debug("Found[0]: "+rc[1]+"/"+py);
304 maxy = py;
305 }
306 break;
307 }
308
309 buffer = buffer.substr(-12);
310 }
311
312 // Step on to the last chunk to get the total length of the stream
313 while (1)
314 {
315 var chunk = getChunk(stream, pos);
316 if (!chunk)
317 break;
318
319 pos = chunk.pos;
320 last = chunk;
321 }
322
323 var first = getChunkHeader(stream, 0);
324 This.stream.totalLength = last.time - first.time;
325 */
326
327 var maxx, maxy;
328
329 var buffer = "";
330 for (var i=0; i<This.stream.length; i++)
331 {
332 buffer += This.stream[i].data;
333
334 var re2 = new RegExp("\033\\[[8];([0-9]+);([0-9]+)t", "g");
335 while (1)
336 {
337 var rc = re2.exec(buffer);
338 if (!rc)
339 break;
340
341 This.debug("Found[1]: "+rc[1]+"/"+rc[2]);
342
343 maxx = parseInt(rc[2], 10);
344 maxy = parseInt(rc[1], 10);
345
346 break;
347 }
348
349 if (!maxx && !maxy)
350 {
351 var re1 = new RegExp("\033\\[([0-9]+);([0-9]+)r", "g");
352 while (1)
353 {
354 var rc = re1.exec(buffer);
355 if (!rc)
356 break;
357
358 var py = parseInt(rc[2], 10);
359
360 if (!maxy || py>maxy)
361 {
362 This.debug("Found[0]: "+rc[1]+"/"+py);
363 maxy = py;
364 }
365 }
366 }
367
368 buffer = buffer.substr(-12);
369 }
370
371 This.debug("OK ["+maxx+"x"+maxy+"]");
372
373 var opts = clone(options);
374 opts.onReady = function() { This.onReadyCallback(); };
375 opts.callback = options.callback || function() { };
376 opts.debug = function(msg) { This.debugStream(msg); };
377 opts.width = maxx || 80;
378 opts.height = maxy || 24;
379 opts.autoResize = true;
380
381 This.viewer = new TTVCanvas(canvas, opts);
382 };
383 xmlStream.send(null);
384
385 // =============================================================
386
387 return this;
388}
389
390/**
391 *
392 * @private
393 *
394 * Called by the emulator when everything is loaded and ready
395 * and the stream can be started. Calls 'onready'
396 */
397TTVPlayer.prototype.onReadyCallback = function()
398{
399 // The object returned by the constructor might not yet be
400 // assigned to this.viewer although the callback has
401 // already been called
402 if (!this.viewer)
403 {
404 var This = this;
405 setTimeout(function() { This.onReadyCallback(); }, 10);
406 return;
407 }
408
409 // Take a snapshot of the initalState for easy rewind
410 this.initialState = { bookmark: this.viewer.snapshot(), index: 0 };
411
412 // call the onready-callback to signal that playing will start soon
413 if (this.options.onready)
414 {
415 var dat = new Date(this.totalLength*1000);
416 this.options.onready(dat);
417 }
418
419 // wait for a given number of milliseconds if autostart should start now
420 if (this.options.autostart>0)
421 {
422 var This = this;
423 setTimeout(function() { This.start(); }, parseInt(this.options.autostart, 10));
424 }
425}
426
427/**
428 *
429 * Called when the stream is going to start playing
430 * Signals the start by calling 'onplay'
431 *
432 * @private
433 *
434 * @param {Number} nextFrameIdx
435 * the index at which the stream should start playing
436 * (the contents must have been set properly before, see jumpBack)
437 */
438TTVPlayer.prototype.start = function(nextFrameIdx)
439{
440 var now = (new Date()).getTime()/1000;
441
442 var idx = nextFrameIdx || 0;
443
444 this.playing = true;
445 this.startTime = now;
446 this.firstFrameIdx = idx;
447 this.firstFrameTime = this.stream[idx].time;
448 //this.totalLength = this.stream[this.stream.length-1].time - this.stream[0].time;
449 this.nextFrameIdx = idx;
450 this.timeout = null;
451
452 this.nextFrame();
453
454 if (this.options.onplay)
455 this.options.onplay();
456}
457
458/**
459 *
460 * Called continously until the stream haas finished or got
461 * interrupted. Calls 'onupdate' and 'completion'
462 *
463 * @private
464 */
465TTVPlayer.prototype.nextFrame = function()
466{
467 // Get current time
468 var now = (new Date()).getTime()/1000;
469
470 // Loop as long as no action has been signaled, the stream has not
471 // yet finished and as long as we are out of sync
472 while (!this.action && this.nextFrameIdx<this.stream.length &&
473 ( this.stream[this.nextFrameIdx].time - this.firstFrameTime < (now - this.startTime)*this.options.warp || this.options.scan) )
474 {
475 //this.viewer.parseData(this.myStream.data, this.myStream.index[++this.nextFrameIdx]);
476
477 this.viewer.parser.parse(this.stream[this.nextFrameIdx++].data);
478
479 // force a screen update after each chunk if 'noskip' is set
480 if (this.options.noskip)
481 break;
482
483 // in scan mode do an update only once a second
484 if (this.options.scan && (new Date()).getTime()/1000-now>1)
485 break;
486
487 }
488
489 // The changes in the canvas and the page will be displayed only when the control
490 // is given back by setTimeout anyway, so it can be outside of the main loop
491 var force = (this.options.noskip && !this.options.scan) || this.action || this.nextFrameIdx==this.stream.length;
492
493 now = (new Date()).getTime();
494 if ((now-this.previousUpdate)>19 || force || !this.previousUpdate) // 50Hz
495 {
496 this.previousUpdate = now;
497 this.viewer.updateCanvas();
498 }
499
500 // If an interrupting action has been requested, execute it
501 if (this.action)
502 {
503 if (!this.action(true))
504 return;
505 }
506
507 // If the stream is not yet finsished, go on playing
508 if (this.nextFrameIdx<this.stream.length)
509 {
510 // Signal the position of the stream via 'onupdate'
511 if (this.options.onupdate)
512 {
513 var pos = this.stream[this.nextFrameIdx].time - this.stream[0].time;
514 var dat = new Date(pos*1000);
515 this.options.onupdate(dat, pos/this.totalLength);
516 }
517
518 var This = this;
519 setTimeout(function() { This.nextFrame(); }, 0);
520 return;
521 }
522
523 // Stream is finished, should it automatically restart?
524 if (this.options.loop)
525 {
526 var This = this;
527 this.viewer.thaw(this.initialState);
528 setTimeout(function() { This.start(0); }, this.options.loop);
529 return;
530 }
531
532 // Signal completion
533 this.timeout = null;
534 this.playing = false;
535
536 if (this.options.onupdate)
537 {
538 var dat = new Date(this.totalLength*1000);
539 this.options.onupdate(dat, 1);
540 }
541 if (this.options.oncompletion)
542 this.options.oncompletion();
543
544 var dat = new Date(new Date().getTime() - this.startTime*1000);
545
546 function pad(v) { return ('0'+v).slice(-2); }
547 this.debug("Completed ["+pad(dat.getUTCHours())+":"+pad(dat.getUTCMinutes())+":"+pad(dat.getUTCSeconds())+"]");
548}
549
550/**
551 *
552 * User function to request play or pause (depending on the current state).
553 * Calls 'onpause'.
554 *
555 * @public
556 */
557TTVPlayer.prototype.playpause = function()
558{
559 if (!this.playing)
560 {
561 if (this.nextFrameIdx==this.stream.length)
562 this.rewind();
563 else
564 this.start(this.nextFrameIdx);
565
566 return;
567 }
568
569 var This = this;
570 this.action = function()
571 {
572 This.action=null;
573 This.playing=false;
574 if (this.options.onpause)
575 this.options.onpause();
576 return false;
577 }
578
579 return false;
580}
581
582/**
583 *
584 * User function to request setting the bookmark (only one is supported)
585 * Calls 'obookmark'.
586 *
587 * @public
588 */
589TTVPlayer.prototype.setBookmark = function()
590{
591 var This = this;
592 this.action = function(wasplaying)
593 {
594 This.action = null;
595 This.bookmark = { bookmark: This.viewer.snapshot(), index: This.nextFrameIdx };
596 if (this.options.onbookmark)
597 this.options.onbookmark();
598 return true;
599 }
600
601 if (!this.playing)
602 this.action(false);
603
604
605 return false;
606}
607
608/**
609 *
610 * Resumes a stream at the given bookmark position.
611 *
612 * @private
613 *
614 * @param {Object} data
615 * contains the properties bookmark with the bookmark data and
616 * index with the corresponding index.
617 *
618 */
619TTVPlayer.prototype.resume = function(data)
620{
621 if (!data)
622 return;
623
624 var This = this;
625 this.action = function(wasplaying)
626 {
627 This.action = null;
628 This.viewer.thaw(data.bookmark);
629 This.viewer.updateCanvas();
630 if (wasplaying || data.index==0)
631 setTimeout(function() { This.start(data.index); }, 1);
632 return false;
633 }
634
635 if (!this.playing)
636 this.action(false);
637}
638
639/**
640 *
641 * User function to jump back to a previously set bookmark position
642 *
643 * @public
644 */
645TTVPlayer.prototype.jump = function()
646{
647 this.resume(this.bookmark);
648 return false;
649}
650
651/**
652 *
653 * User function to rewind to the beginning of the stream
654 *
655 * @public
656 */
657TTVPlayer.prototype.rewind = function()
658{
659 this.resume(this.initialState);
660 return false;
661}
662
663/**
664 *
665 * User function to change the warpFactor
666 *
667 * @param {Number} val
668 * The warp factor, e.g. 2 means to play two times faster, 0.5
669 * two times slower
670 *
671 * @public
672 */
673TTVPlayer.prototype.setWarpFactor = function(val)
674{
675 this.options.warp = val;
676 return false;
677}
678
679// This is a hack to avoid that the closure compiler will rename
680// these functions
681window['TTVPlayer'] = TTVPlayer;
682TTVPlayer.prototype['rewind'] = TTVPlayer.prototype.rewind;
683TTVPlayer.prototype['playpause'] = TTVPlayer.prototype.playpause;
684TTVPlayer.prototype['setBookmark'] = TTVPlayer.prototype.setBookmark;
685TTVPlayer.prototype['resume'] = TTVPlayer.prototype.resume;
686TTVPlayer.prototype['jump'] = TTVPlayer.prototype.jump;
687TTVPlayer.prototype['setWarpFactor'] = TTVPlayer.prototype.setWarpFactor;
Note: See TracBrowser for help on using the repository browser.