'use strict'; /** * @class TTVPlayer * @constructor * @public * * This class implements things like loading the stream, analyzing * the stream, crawling through the stream and callbacks for the control. * * The stream is displayed in the given canvas object. * * @param {Node} canvas * A reference to a canvas element * * @param {Object} options * An object with a list of options. Possible properties are * * */ function TTVPlayer(canvas, options) { var This = this; // The closure compiler might have decided to rename the // properties, so we 'restore' them. If the code is not // compiles, this statements have no effect. If the code // is compiled, this is the innermost class the user should // be able to access. So outside of this class we access // the object properties always by index (clear name) // and inside and deeper we can access them by property. options.name = options['name']; options.title = options['title']; options.font = options['font']; options.fontsize = options['fontsize']; options.fontfamily = options['fontfamily']; options.boldfont = options['boldfont']; options.scan = options['scan']; options.warp = options['warp']; options.loop = options['loop']; options.autostart = options['autostart']; options.onready = options['onready']; options.debug = options['debug']; options.callback = options['callback']; options.onupdate = options['onupdate']; options.oncompletion = options['oncompletion']; options.onbookmark = options['onbookmark']; options.onpause = options['onpause']; options.onplay = options['onplay']; options.noskip = options['noskip']; This.debugStream = function(msg) { function pad(v) { return ('0'+v).slice(-2); } if (!This.options.debug) return; if (This.stream && This.stream[This.nextFrameIdx]) { var dat = new Date((This.stream[This.nextFrameIdx].time - This.stream[0].time)*1000); options.debug(pad(dat.getUTCHours())+":"+ pad(dat.getUTCMinutes())+":"+ pad(dat.getUTCSeconds())+" "+msg); } else options.debug("--:--:-- "+msg); } This.debug = function(msg) { This.debugStream("player.js: "+msg); } // ========================================================== This.options = options; if (!This.options.warp) This.options.warp = 1; var name = options.name || 'index.rec'; var xmlStream = new XMLHttpRequest(); xmlStream.open('GET', name, true); xmlStream.setRequestHeader("Cache-Control", "no-cache"); xmlStream.setRequestHeader("If-Match", "*"); xmlStream.overrideMimeType("application/octet-stream; charset=x-user-defined"); //xmlStream.overrideMimeType("text/plain; charset=x-user-defined"); xmlStream.onload = function () { // error occured during load if (xmlStream.status!=200) { alert("ERROR[0] - HTTP request '"+name+"': "+xmlStream.statusText+" ["+xmlStream.status+"]"); if (options.onclose) options.onclose(); return; } var stream = xmlStream.responseText; // To encrypt a stream do // openssl enc -aes-256-cbc -in infile -out outfile -pass pass:"YourPassphrase" -e -base64 // This might be base64 encoded // If this is a plain file, most probably already one of the // first few bytes does not match the base64 encoding if (stream.search(/[^\x0a\x0dABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=]/)==-1) { // Remove carriage returns and line feeds from the stream var clean = stream.replace(/[\x0A\x0C]/g, ""); // Parse as if this is a base64 encoded stream var words = CryptoJS.enc.Base64.parse(clean); var latin1 = CryptoJS.enc.Latin1.stringify(words); // Check if it starts with "Salted__", then we really found an encoded stream if (words.words[0] == 0x53616c74 && words.words[1] == 0x65645f5f) { // Ask for the passphrase and decode var pw = prompt("Please enter a passphrase to decrypt the stream: "); if (pw) stream = CryptoJS.AES.decrypt(clean, pw).toString(CryptoJS.enc.Latin1); } } try { // Convert the data into chunks. The advantage is that // we have all the decoding done already. The disadvantage // is that for a short moment we have the data twice in memory // [Note: we need to step to the end anyway to get the // length of the stream. Example: TtyRecParse 260ms, // direct stepping: 1ms for ~5MB, ~2500chunks] This.stream = new TTVDecoder(stream); This.totalLength = This.stream[This.stream.length-1].time - This.stream[0].time; } catch (e) { alert(e); if (This.options.onclose) This.options.onclose(); return; } This.debug("GO"); // The idea with this code was to create a single array // and an index table and crawl through it. Unfortunately, // with the current implementation using regexps for matching, // the regular exporessions would need to support the // y-modifier or one has to create substrings anyways. /* // Decode the stream, create a continous buffer for the // data create a look-up-table with index and time var myStream = { }; myStream.index = []; myStream.time = []; myStream.data = ""; var n = 0; var pos = 0; while (1) { var chunk = getChunk(stream, pos); if (!chunk) break; myStream.time[n] = chunk.time; myStream.index[n] = myStream.data.length; myStream.data += chunk.data; n++; pos = chunk.pos; } myStream.length = n; myStream.totalLength = myStream.time[myStream.length-1] - myStream.time[0]; This.myStream = myStream; var maxx, maxy; // Scan the data stream for the size sequence var re2 = new RegExp("\033\\[[8];([0-9]+);([0-9]+)t", "g"); while (!maxx && !maxy) { var rc = re2.exec(myStream.data); if (!rc) break; This.debug("Found[1]: "+rc[1]+"/"+rc[2]); maxx = parseInt(rc[2], 10); maxy = parseInt(rc[1], 10); } // Scan the data stream for goto sequence if no size sequence was found var re1 = new RegExp("\033\\[([0-9]+);([0-9]+)r", "g"); while (1) { var rc = re1.exec(myStream.data); if (!rc) break; This.debug("Found[0]: "+rc[1]+"/"+rc[2]); var py = parseInt(rc[2], 10); if (!maxy || py>maxy) { This.debug("Found[0]: "+rc[1]+"/"+py); maxy = py; } } This.totalLength = myStream.totalLength; */ /* // Step through the data and try to find the sequence for // the screen size, or try to get the size from goto // sequences var maxx; var maxy; var buffer = ""; var now = new Date(); var pos = 0; var last; while (1) { var chunk = getChunk(stream, pos); if (!chunk) break; buffer += chunk.data; pos = chunk.pos; last = chunk; while (!maxx && !maxy) { var rc = re2.exec(buffer); if (!rc) break; This.debug("Found[1]: "+rc[1]+"/"+rc[2]); maxx = parseInt(rc[2], 10); maxy = parseInt(rc[1], 10); } if (maxx && maxy) break; while (1) { var rc = re1.exec(buffer); if (!rc) break; This.debug("Found[0]: "+rc[1]+"/"+rc[2]); var py = parseInt(rc[2], 10); if (!maxy || py>maxy) { This.debug("Found[0]: "+rc[1]+"/"+py); maxy = py; } break; } buffer = buffer.substr(-12); } // Step on to the last chunk to get the total length of the stream while (1) { var chunk = getChunk(stream, pos); if (!chunk) break; pos = chunk.pos; last = chunk; } var first = getChunkHeader(stream, 0); This.stream.totalLength = last.time - first.time; */ var maxx, maxy; var buffer = ""; for (var i=0; imaxy) { This.debug("Found[0]: "+rc[1]+"/"+py); maxy = py; } } } buffer = buffer.substr(-12); } This.debug("OK ["+maxx+"x"+maxy+"]"); var opts = clone(options); opts.onReady = function() { This.onReadyCallback(); }; opts.callback = options.callback || function() { }; opts.debug = function(msg) { This.debugStream(msg); }; opts.width = maxx || 80; opts.height = maxy || 24; opts.autoResize = true; This.viewer = new TTVCanvas(canvas, opts); }; xmlStream.send(null); // ============================================================= return this; } /** * * @private * * Called by the emulator when everything is loaded and ready * and the stream can be started. Calls 'onready' */ TTVPlayer.prototype.onReadyCallback = function() { // The object returned by the constructor might not yet be // assigned to this.viewer although the callback has // already been called if (!this.viewer) { var This = this; setTimeout(function() { This.onReadyCallback(); }, 10); return; } // Take a snapshot of the initalState for easy rewind this.initialState = { bookmark: this.viewer.snapshot(), index: 0 }; // call the onready-callback to signal that playing will start soon if (this.options.onready) { var dat = new Date(this.totalLength*1000); this.options.onready(dat); } // wait for a given number of milliseconds if autostart should start now if (this.options.autostart>0) { var This = this; setTimeout(function() { This.start(); }, parseInt(this.options.autostart, 10)); } } /** * * Called when the stream is going to start playing * Signals the start by calling 'onplay' * * @private * * @param {Number} nextFrameIdx * the index at which the stream should start playing * (the contents must have been set properly before, see jumpBack) */ TTVPlayer.prototype.start = function(nextFrameIdx) { var now = (new Date()).getTime()/1000; var idx = nextFrameIdx || 0; this.playing = true; this.startTime = now; this.firstFrameIdx = idx; this.firstFrameTime = this.stream[idx].time; //this.totalLength = this.stream[this.stream.length-1].time - this.stream[0].time; this.nextFrameIdx = idx; this.timeout = null; this.nextFrame(); if (this.options.onplay) this.options.onplay(); } /** * * Called continously until the stream haas finished or got * interrupted. Calls 'onupdate' and 'completion' * * @private */ TTVPlayer.prototype.nextFrame = function() { // Get current time var now = (new Date()).getTime()/1000; // Loop as long as no action has been signaled, the stream has not // yet finished and as long as we are out of sync while (!this.action && this.nextFrameIdx1) break; } // The changes in the canvas and the page will be displayed only when the control // is given back by setTimeout anyway, so it can be outside of the main loop var force = (this.options.noskip && !this.options.scan) || this.action || this.nextFrameIdx==this.stream.length; now = (new Date()).getTime(); if ((now-this.previousUpdate)>19 || force || !this.previousUpdate) // 50Hz { this.previousUpdate = now; this.viewer.updateCanvas(); } // If an interrupting action has been requested, execute it if (this.action) { if (!this.action(true)) return; } // If the stream is not yet finsished, go on playing if (this.nextFrameIdx