iangilham.com

By on

code interactive-tv javascript

Now that Glastonbury and Wimbledon are over with no major technical failures, it feels like a good time to talk about the interactive TV application I built for the events. For the last few years the BBC has been fielding an application that allows the audience to switch between different hidden video streams on broadcast TV. We always aimed to equally support all the broadcast digital platforms (terrestrial, satellite and cable) but for operational reasons this is the first time we’ve had a complete offering on the cable platform. I’d like to share some of what I learned developing software for cable TV in the UK.

For the last few months I’ve been working on a client application for the UK’s aging cable television platform. For the purpose of this article, I’m talking about the older boxes operated by Virgin Media, not the TiVo branded boxes. These set-top boxes run the Liberate TV Navigator middleware, which is essentially just a really old web browser with a few TV-specific extensions.

The set-top box browser supports a variant of HTML 3.2 and a very early JavaScript interpreter. It is largely based on Netscape Communicator 4.76 and was never updated to more modern web standards. The Liberate middleware (now owned by SeaChange did see at least a few revisions, but these never made it into consumer products in the UK.

There are a number of challenges to working with older browsers like this. Here’s a brief list of some of the things the browser cannot do:

The document is a fixed-size rectangle of 720 by 576 pixels. The web page can embed and scale the TV’s video stream, but the complete document must be statically sized to fit within those dimensions. Since there are no advanced layout controls, the design is restricted to hard-coded HTML tables with fixed size rows and columns. The tables can’t be nested more than a few layers deep either, as that leads to serious performance issues on most of the older devices.

JavaScript can be used to get a reference to tables and fish out their elements. There is some limited ability to modify the contents by changing text, foreground and background colours (including transparent to hide them completely), but there is no way to dynamically add elements to the DOM or resize existing table cells, so each document is restricted to showing and hiding elements defined statically, and changing foreground and background colours.

The absence of object and function literals in JavaScript makes writing object-oriented code tricky. The only mechanism that works in the Liberate browser is to add named functions as methods on a constructor function’s prototype. This is more complete than functional programming support however, which has too many missing features to be workable.

var tblMain = document.tblMain;

function Foo() {
	this.name = "foo";
}

function foo_hello() {
	tblMain.rows[0].cells[0].write("Hello, " + this.name);
}
Foo.prototype.hello = foo_hello;

The above example assumes a document containing a table element with the attribute name="tblMain".

The limited syntax support creates issues when assembling a content pipeline for the website as well. Typical minification tools like uglifier and Closure Compiler tend to change object and array declarations to use object literals (= new Array() vs =[]), so they have to be used carefully to avoid breaking the scripts in the browser.

The browser can fetch resources from a broadcast carousel file system (known as in-band data) or via HTTP over the DOCSIS return path (out-of-band). The Cable operator places restrictions on what URLs can be retrieved for safety, so the network can be thought of as an Intranet, though out-of-band data is not immune to man-in-the-middle attacks.

Drawing a user interface

A simple user interface can be drawn with basic HTML 3.2 tables.

<html>
	<head>
		<title>test application</title>
		<script>
		// your scripts here
		</script>
	</head>
	<body onload="onLoad()" onunload="onUnload()" background="tv://">
		<table name="uiRoot"
				width="720" height="576"
				cellspacing="0" cellpadding="0" border="0">
			<!-- header row with borders -->
			<tr>
				<td width="50" height="30"></td>
				<td width="620"></td>
				<td width="50"></td>
			</tr>
			<!-- content row with borders -->
			<tr>
				<td width="50" height="30"></td>
				<!-- content area -->
				<td width="620" bgcolor="white">
					<font size="5" color="black">&nbsp;</font>
				</td>
				<td width="50"></td>
			</tr>
		</table>
	</body>
</html>

In this example, the content area is a table cell containing text. The cell can be manipulated as in the following code.

function hideContent() {
	window.uiRoot.rows[1].cells[1].bgcolor = "transparent";
	window.uiRoot.rows[1].cells[1].write(true, "");
}

function showContent() {
	window.uiRoot.rows[1].cells[1].bgcolor = "white";
	window.uiRoot.rows[1].cells[1].write(true, "Hello world!");
}

Similar methods are used to implement a navigable menu, with a highlight colour for the current row’s background. This would require the hard-coded HTML table to contain enough rows to represent the full menu, and some functions to navigate up and down, and highlight, show or hide each row.

When making multiple UI changes, it can be beneficial to lock the UI layer then allow the changes to draw when finished. This can be done at the level of the table or sub-table element in the HTML layer.

// lock UI
window.uiRoot.drawingDisabled = true;

// make some UI changes

// unlock UI
window.uiRoot.drawingDisabled = false;

Handling user input

Input handling is old-school on Liberate. Input events call the window.onkeyout function and provide the opportunity to decode navigator.input.curKey and run any required behaviours. Keys are represented by raw numbers, so a map of names to key codes can be useful.

function KeyMap() {
	this.HELP = 779;
	this.INFO = 775;
	this.RED = 2048;
	this.GREEN = 2049;
	this.YELLOW = 2050;
	this.BLUE = 2051;
	this.TV = 2053;
	this.PAGEUP = 1284;
	this.PAGEDOWN = 1285;
	this.VOLUP = 1026;
	this.VOLDOWN = 1027;
	this.MUTE = 1036;
	this.OK = 13;
	this.UP = 1280;
	this.DOWN = 1281;
	this.LEFT = 1282;
	this.RIGHT = 1283;
	this.BACK = 1288;
	this.N0 = 48;
	this.N1 = 49;
	this.N2 = 50;
	this.N3 = 51;
	this.N4 = 52;
	this.N5 = 53;
	this.N6 = 54;
	this.N7 = 55;
	this.N8 = 56;
	this.N9 = 57;
}
var K = new KeyMap();

The basic layout of an input handler function is below. This follows the previous example, toggling display of the UI’s ‘hello world’ box.

function doKeyout() {
	var key = navigator.input.curKey;

	// let the user alter volume/mute
	if (key ==K.VOLUP || key == K.VOLDOWN || key == K.MUTE) {
		return true; // invokes STB default behaviour
	}

	// handle application input
	if (key == K.OK) {
		// togle the UI content
		if (window.uiRoot.rows[1].cells[1].bgcolor == "transparent") {
			showContent();
		} else {
			hideContent();
		}
	}
	return false;
}
window.onkeyout = doKeyout;

Fetching resources with NetRequest

While the Liberate TV Navigator browser can’t parse JSON, it can fetch an in-band JavaScript file and eval it. This enables applications to use a configuration file in the programming language or to be fed a declaration of a string to be parsed. A common practice is to broadcast the basic application in-band and have it look up any changeable configuration data from either a separate ‘dynamic’ carousel (updating whenever the content changes) or over the network. Fortunately, both methods of fetching a file are modeled by the NetRequest object in the JavaScript API. In the below example, configNROnStart, configNROnCancel, configNROnError, configNROnLoad, and configNROnDone are functions called as event handlers.

function loadConfig() {
	configNR = new NetRequest();
	configNR.url = "http://www.example.com/config.js";
	configNR.cache = false;
	configNR.onstart = configNROnStart;
	configNR.oncancel = configNROnCancel;
	configNR.onerror = configNROnError;
	configNR.onload = configNROnLoad; // do something with response body
	configNR.ondone = configNROnDone;

	menuDataNR.fetch();
}

Dynamically updating resources at runtime

Another useful feature is timers. These work like in any older browser – calling var t = setTimeout("doSomething()", 5000); will trigger the string doSomething() to be evaluated asynchronously after 5 seconds. Calling clearTimeout(t) cancels it. The only caveat with timers is that the first argument must be a string to eval; it does not accept a function.

var world = new World();
var t = setTimout("world.hello()", 5000);
clearTimeout(t);

Together, NetRequests and Timers enable us to poll for an updating resource on an interval.

var configNROnloadCallback;

function configNROnLoad(event) {
	configNROnloadCallback(event.textData);
}

function loadConfig(callback) {
	configNROnloadCallback = callback;
	configNR = new NetRequest();
	configNR.url = "http://example.com/config.js";
	configNR.cache = false;
	configNR.onstart = configNROnStart;
	configNR.oncancel = configNROnCancel;
	configNR.onerror = configNROnError;
	configNR.onload = configNROnLoad;
	configNR.ondone = configNROnDone;

	menuDataNR.fetch();
}

var configReloadTimer = setTimeout(
	"reloadConfig();", 15000);

function reloadConfig() {
	loadConfig(loadConfig);
}

function loadConfig(config) {
	// TODO: do something with config

	// reset the timer
	configReloadTimer = setTimeout(
		"reloadConfig();", 15000);
}

Conclusion

That concludes our whirlwind tour of the Liberate platform. I have demonstrated the major interactive software building blocks on non-Tivo cable TV boxes in the UK. We’re now doing more complex things with the platform, such as tuning to TV channels and switching out alternative audio tracks, while periodically fetching configuration updates and updating on-screen assets.

I deliberately haven’t discussed much of the back-end infrastructure that bridges the gap from a web server to the broadcast transport stream. There is a large stack of legacy proprietary software behind the Liberate platform, compiling resources into transport streams and generating announcements so the set-top-box knows what content to run and make available to applications. The exact nature of the back-end is a bit more sensitive than the software environment and capabilities described above.