Skip to content

Application IFrame API

Since: TVS 6.7.0

The Application IFrame API connects a configured full-screen web application to Television Simulator (TVS). It uses window.postMessage() to provide:

  • Remote navigation, media transport, colored, and numeric commands
  • Configured application options and launch arguments
  • Current volume, mute, and power state
  • OSD presentation information such as the active title and artwork
  • Coordinated application exit and cleanup

This API uses messages beginning with TVS.Application. and protocolVersion: 1. It is separate from the channel IFrame API, which uses messages such as TVS.Ready and TVS.Options for iframe content shown by the tuner.

Firstly, make an application category. Apps are organized in the menu system of supported themes under these categories. Categories can have icons or wallpapers depending on the menu theme, but you need a name at minimum.

applicationCategories:
games: # the ID of the category; can be anything as long as it's unique within applicationCategories
name: Games

Next, add the application to the applications section and reference the category you defined above. The options object is delivered unchanged in the initialization message.

applications:
blackjack:
category: games # use the ID of the category you defined above.
title: Blackjack
src: https://games.example.com/blackjack/
options:
startingBankroll: 300
minimumBet: 5

See Interactive Applications for all configuration properties and current menu support.

Every message in both directions includes the current protocol version and application instance:

{
type: "TVS.Application.MessageName",
protocolVersion: 1,
instanceId: 42
}

Copy protocolVersion and instanceId from the current TVS.Application.Probe into every reply. An instance ID belongs only to that launch of the application. TVS rejects stale messages after the application closes or is replaced.

TVS sends TVS.Application.Probe when the iframe loads and repeats it until the application replies with TVS.Application.Ready. TVS then sends TVS.Application.Initialize.

The following example performs the handshake, validates the sender, and provides one place to send messages back to TVS:

const TVS_APPLICATION_PROTOCOL_VERSION = 1;
const allowedTvsOrigins = new Set([
"https://tvs.example.com",
"http://localhost:5173"
]);
let tvsSession = null;
function sendToTvs(type, payload = {}) {
if (!tvsSession) return;
window.parent.postMessage(
{
type,
protocolVersion: TVS_APPLICATION_PROTOCOL_VERSION,
instanceId: tvsSession.instanceId,
...payload
},
tvsSession.origin
);
}
window.addEventListener("message", (event) => {
if (
event.source !== window.parent ||
!allowedTvsOrigins.has(event.origin) ||
event.data?.protocolVersion !== TVS_APPLICATION_PROTOCOL_VERSION ||
!Number.isInteger(event.data?.instanceId)
) {
return;
}
const message = event.data;
if (message.type === "TVS.Application.Probe") {
tvsSession = {
origin: event.origin,
instanceId: message.instanceId
};
sendToTvs("TVS.Application.Ready");
return;
}
if (
!tvsSession ||
event.origin !== tvsSession.origin ||
message.instanceId !== tvsSession.instanceId
) {
return;
}
handleTvsMessage(message);
});

Replace the example origins with the origins from which your application is allowed to be embedded. Register the event listener before waiting for a probe.

Initialization is sent once the application reports that it is ready.

{
type: "TVS.Application.Initialize",
protocolVersion: 1,
instanceId: 42,
args: {},
options: {
startingBankroll: 300,
minimumBet: 5
},
audio: {
volume: 40,
muted: false,
powered: true
}
}

options contains the object from the TVS config file. args contains launch-time data supplied by the menu or another TVS component. Do not assume either value has a particular structure; validate the properties your application uses.

audio.volume is the receiver level from 0 to 50. audio.muted includes TVS mute state and temporary content muting, such as while a menu is open. audio.powered reports whether the television is on.

function handleTvsMessage(message) {
switch (message.type) {
case "TVS.Application.Initialize":
startGame(message.options);
applyAudioState(message.audio);
break;
// Other messages are handled in the examples below.
}
}

TVS sends TVS.Application.Command when the active application receives a supported remote control input.

{
type: "TVS.Application.Command",
protocolVersion: 1,
instanceId: 42,
command: "navLeft"
}

Applications can receive these commands:

ControlsCommand values
NavigationnavUp, navDown, navLeft, navRight, select, back
Media transportplay, pause, stop, fastForward, rewind
Colored controlsred, green, yellow, blue
Numeric input0 through 9, subchannelSeparator

Channel Up and Channel Down are not application commands. They remain reserved for the tuner. The first press displays an exit warning; a second Channel Up or Channel Down press within three seconds closes the application and changes channels.

function handleCommand(command) {
switch (command) {
case "navLeft":
moveSelection(-1, 0);
break;
case "navRight":
moveSelection(1, 0);
break;
case "navUp":
moveSelection(0, -1);
break;
case "navDown":
moveSelection(0, 1);
break;
case "select":
activateSelection();
break;
case "back":
if (canCloseDialog()) {
closeDialog();
} else {
requestExit();
}
break;
}
}
function handleTvsMessage(message) {
switch (message.type) {
case "TVS.Application.Initialize":
startGame(message.options);
applyAudioState(message.audio);
break;
case "TVS.Application.Command":
handleCommand(message.command);
break;
}
}

The iframe application receives commands only after it completes the handshake. Once ready, it receives Back like any other command. If the application has no internal screen to close, it should send TVS.Application.RequestExit so Back retains its expected behavior.

After initialization, TVS sends TVS.Application.AudioState whenever receiver volume, mute, or power state changes.

function applyAudioState(audio) {
const normalizedVolume = Math.max(0, Math.min(1, audio.volume / 50));
gameAudio.volume = audio.muted || !audio.powered ? 0 : normalizedVolume;
}
function handleTvsMessage(message) {
switch (message.type) {
case "TVS.Application.Initialize":
startGame(message.options);
applyAudioState(message.audio);
break;
case "TVS.Application.Command":
handleCommand(message.command);
break;
case "TVS.Application.AudioState":
applyAudioState(message.audio);
break;
}
}

Send TVS.Application.SetPresentation to expose the application’s current state to compatible tuner and information OSDs.

sendToTvs("TVS.Application.SetPresentation", {
presentation: {
name: "Blackjack",
icon: "https://games.example.com/blackjack/icon.png",
title: "Player turn",
subtitle: "Bankroll: $285",
summary: "Dealer shows the six of hearts.",
audio: {
hasData: true,
title: "Casino Theme",
artist: "Example Band",
album: "Game Music",
coverArt: "https://games.example.com/blackjack/album.png",
stationName: "Blackjack"
}
}
});

All presentation properties are optional. name, icon, title, subtitle, and summary must be strings when present. audio may be null or an object containing an optional boolean hasData plus the optional string properties stationName, title, artist, album, and coverArt. Compatible information OSDs use it for now-playing details.

Send presentation: null to clear previously published information.

Use an absolute URL for remote artwork so it resolves independently of the TVS installation URL.

An application can ask TVS to close it:

function requestExit() {
sendToTvs("TVS.Application.RequestExit");
}

An application closed this way is treated like a user exit. If it was opened from a menu, TVS restores that menu to its previous page and selection.

Before an application closes or is replaced, TVS may send TVS.Application.PrepareExit with one of these reasons:

ReasonMeaning
userThe user or application requested exit
quitThe user permanently closed the saved application
channel-changeThe tuner is changing channels
replacedAnother application is taking over
launch-failedThe application could not finish launching

Pause media and persist any short-lived state, then reply with TVS.Application.ExitReady.

async function prepareForExit(reason) {
pauseAllMedia();
try {
await saveSessionState(reason);
} finally {
sendToTvs("TVS.Application.ExitReady");
}
}

Add the message to the main handler:

case "TVS.Application.PrepareExit":
void prepareForExit(message.reason);
break;

TVS waits only briefly for cleanup before completing the exit, so do not start a long-running operation in response.

TVS remembers the application’s ID and launch arguments for the dedicated APP input, but protocol version 1 does not include a host-side checkpoint API for private iframe state. Save any state that must survive a page refresh within the application itself.

This example combines the handshake, initialization, remote input, audio state, OSD presentation, and exit lifecycle in a small application shell.

<p id="status">Waiting for TVS…</p>
<script>
const protocolVersion = 1;
const allowedTvsOrigins = new Set(["https://tvs.example.com"]);
const status = document.querySelector("#status");
let session = null;
function send(type, payload = {}) {
if (!session) return;
parent.postMessage(
{ type, protocolVersion, instanceId: session.instanceId, ...payload },
session.origin
);
}
addEventListener("message", (event) => {
const message = event.data;
if (
event.source !== parent ||
!allowedTvsOrigins.has(event.origin) ||
message?.protocolVersion !== protocolVersion ||
!Number.isInteger(message?.instanceId)
) return;
if (message.type === "TVS.Application.Probe") {
session = { origin: event.origin, instanceId: message.instanceId };
send("TVS.Application.Ready");
return;
}
if (!session || message.instanceId !== session.instanceId) return;
switch (message.type) {
case "TVS.Application.Initialize":
status.textContent = `Ready with ${JSON.stringify(message.options)}`;
send("TVS.Application.SetPresentation", {
presentation: { name: "Example App", title: "Ready" }
});
break;
case "TVS.Application.Command":
status.textContent = `Command: ${message.command}`;
if (message.command === "back") send("TVS.Application.RequestExit");
break;
case "TVS.Application.AudioState":
document.documentElement.dataset.muted = String(message.audio.muted);
break;
case "TVS.Application.PrepareExit":
send("TVS.Application.ExitReady");
break;
}
});
</script>