Skip to content
Snippets Groups Projects
tty-player.js 55.2 KiB
Newer Older
// W069 is “['x'] is better written in dot notation”, but Closure Compiler wants ['x'].
// jshint -W069
// jshint bitwise: false
// ==ClosureCompiler==
// @output_file_name tty-player.min.js
// @compilation_level ADVANCED_OPTIMIZATIONS
// ==/ClosureCompiler==
/* global MediaError, TimeRanges, Terminal, HTMLElement */
;(function() {
"use strict";

var textDecoder = "TextDecoder" in window ? new TextDecoder() : null;
/// @param {Array<number>|Uint8Array} array
function decodeUTF8(array) {
	if (array instanceof Array || !textDecoder) {
		return decodeURIComponent(Array.prototype.map.call(array, function(ord) {
			return "%" + ("0" + ord.toString(16)).substr(-2);
		}).join(""));
	} else {
		return textDecoder.decode(array);
	}
}

/// parseDataURI("data:foo/bar;base64,MTIzNA==#foo") === "1234"
/// @param {string} uri
function parseDataURI(uri) {
	// [whole uri, "base64" or undefined, data]
	var chunks = /^data:([^,]*),([^#]+)/.exec(uri);
	if (chunks === null) {
		return null;
	}
	var data = decodeURIComponent(chunks[2]);
	var mime = chunks[1].replace(/;base64$/, "");
	return [mime, mime === chunks[1] ? data : atob(data)];
}

/// @param {Array<number>|Uint8Array} array
function byteArrayToString(array) {
	// String.fromCharCode.apply can for too large values overflow the call stack.
	// Hence this, though I doubt we actually use large enough strings to worry.
	// http://stackoverflow.com/a/12713326
	var CHUNK_SIZE = 0x8000;
	var c = [];
	for (var i = 0; i < array.length; i += CHUNK_SIZE) {
		c.push(String.fromCharCode.apply(null, array["subarray" in array ? "subarray" : "slice"](i, i + CHUNK_SIZE)));
	}
	return c.join("");
}

function parseNPT(npt) {
	// Format: [npt:]([h:]mm:ss|seconds)[.subsecond]
	// I’ve decided to be lazy and allow "1:2:3.4" as well as "1:02:03.4"
	// This makes it [npt:][[h:]m:]s[.subsecond]
	var match = /^(?:npt:)?(?:(?:(\d+):)?(\d+):)?(\d+(?:\.\d+)?)$/i.exec(npt);
	return match ? (match[1] || 0) * 3600 + (match[2] || 0) * 60 + match[3] : null;
}

function classifyPosterURL(url) {
	if (!url) {
		// There is no poster.
		return {type: null};
	}
	switch (/^(?:(.*):)?/.exec(url)[1]) {
		case "npt":
			var time = parseNPT(url);
			return time ? {type: "npt", time: time} : {type: null};
		case "data":
			var data = parseDataURI(url);
			if (/^text\/plain$/i.test(data[0])) {
				return {type: "text", data: data[1]};
			}
	}
	// TODO: treat all the other possibilities as images.
	return {type: null};
}

/// @param {ArrayBuffer|Array<number>} source
function parseTTYRec(source) {
	var isArray = source instanceof Array;
	var utf8 = true;
	var dimensions = null;
	var data = [];
	var byteOffset = 0;
	var timeOffset = 0;
	var sourceLength = isArray ? source.length : source.byteLength;
	while (byteOffset < sourceLength) {
		var sec, usec, len;
		if (!isArray) {
			var header = new DataView(source, byteOffset);
			sec = header.getUint32(0, true);
			usec = header.getUint32(4, true);
			len = header.getUint32(8, true);
		} else {
			sec = source[byteOffset] +
				  (source[byteOffset + 1] << 8) +
				  (source[byteOffset + 2] << 16) +
				  (source[byteOffset + 3] << 24);
			usec = source[byteOffset + 4] +
				  (source[byteOffset + 5] << 8) +
				  (source[byteOffset + 6] << 16) +
				  (source[byteOffset + 7] << 24);
			len = source[byteOffset + 8] +
				  (source[byteOffset + 9] << 8) +
				  (source[byteOffset + 10] << 16) +
				  (source[byteOffset + 11] << 24);
		}
		var time = sec + (usec / 1000000);
		byteOffset += 12;
		var payload = isArray ? source.slice(byteOffset, byteOffset + len)
							  : new Uint8Array(source, byteOffset, len);
		payload = utf8 ? decodeUTF8(payload) : byteArrayToString(payload);
		if (byteOffset === 12) {
			// First chunk might be metadata; this is how termrec does it, for example.
			timeOffset = time;
			var metadata = /^\x1b%(G|@)\x1b\[8;([0-9]+);([0-9]+)t$/.exec(payload);
			if (metadata) {
				utf8 = metadata[1] === "G";
				dimensions = {
					rows: +metadata[2],
					cols: +metadata[3]
				};
			}
		}
		time -= timeOffset;
		byteOffset += len;
		data.push([payload, time]);
	}
	return {
		// Heuristic: if the time offset is large enough, it’s probably a timestamp.
		startDate: timeOffset >= 1e8 ? new Date(timeOffset * 1000) : null,
		dimensions: dimensions,
		data: data
	};
}

function formatTime(time) {
	var seconds = time | 0;
	var minutes = seconds / 60 | 0;
	seconds = ("0" + (seconds % 60)).substr(-2);
	if (minutes >= 60) {
		var hours = minutes / 60 | 0;
		minutes = ("0" + (minutes % 60)).substr(-2);
		return hours + ":" + minutes + ":" + seconds;
	} else {
		return minutes + ":" + seconds;
	}
}

function blankableAttributeProperty(name) {
	return {
		get: function() {
			var value = this.getAttribute(name);
			return value === null ? "" : value.trim();
		},
		set: function(value) {
			this.setAttribute(name, value);
		}
	};
}

function attributeBooleanProperty(name) {
	return {
		get: function() {
			return this.hasAttribute(name);
		},
		set: function(bool) {
			if (bool) {
				this.setAttribute(name, "");
			} else {
				this.removeAttribute(name);
			}
		}
	};
}

function invalidStateError() {
	document.createElement("video").currentTime = 1;
}

/** @const */ var NETWORK_EMPTY = 0;
/** @const */ var NETWORK_IDLE = 1;
/** @const */ var NETWORK_LOADING = 2;
/** @const */ var NETWORK_NO_SOURCE = 3;

/** @const */ var HAVE_NOTHING = 0;
/** @const */ var HAVE_METADATA = 1;
/** @const */ var HAVE_CURRENT_DATA = 2;
/** @const */ var HAVE_FUTURE_DATA = 3;
/** @const */ var HAVE_ENOUGH_DATA = 4;

// Annoyingly, with things like MediaError, one apparently can’t construct them in any way.
// I might need to do something like this.
var My = {};
// Note that the constants on MediaError are *not* on My.MediaError, though they are on instances.
My.MediaError = /** @constructor */ function(code) {
	Object.defineProperty(this, "code", {value: code});
};
My.MediaError.prototype = Object.create(MediaError.prototype);

/** @const */ var EMPTY_TIME_RANGES = document.createElement("video").played;
Loading
Loading full blame...