'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
*
* - name: The name (or url) of the stream to be loaded
*
- title: The title to be used in the title bar instead of the
* file name
*
- font: The name or url (without extension) of the font to
* be loaded
*
- boldfont: The name or url (without extension) of the bold font
* to be loaded
*
- scan: if set, the player will display only 1 frame per second
* for a fast analysis of the stream
*
- warp: a factor by which the playing is accelerated or decelerated
*
- loop: restarts the stream after the defined number of
* milliseconds again after it finished (0: off)
*
- autostart: when everything is ready, after that time, the
* stream will start automatically (0: no autostart)
*
- onready: callback to be called when everthing is ready
*
- debug: a function used to send debug messages (takes
* a String as argument)
*
- callback: a callback providing special informations from the
* emulator, as the title
*
- onupdate: called on each canvas update with the current
* position in the file and the fraction already displayed
*
- oncompletion: called when the stream has been finished and will
* not be restarted
*
- onbookmark: called when the bookmark has been set
*
- onpause: called whenever the stream was paused
*
- onplay: called whenever the stream starts playing
*
- noskip: forces an update after each decoded chunk, otherwise
* only one update per 20ms (50Hz) will be displayed.
*
- fontsize: if fontsize is given the cnavas-element text rendering
* engine is used for text rendering. This might not work on all
* browsers.
*
- fontfamily: The font-family, if manual canvas-rendering is
* switched on with fontsize>0. Do not choose variable size fonts.
* Note that the code is waiting for the font to be loaded. So if
* the font cannot be loaded, it will wait forever.
*
*
*/
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