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

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