@signageos/supra-app-launcher
v0.1.0
Published
signageOS applet for launching Android apps on the redroid runtime on Supra Server
Keywords
Readme
@signageos/supra-app-launcher
Launches an Android application in a local Redroid runtime and renders its stream in an iframe. The package provides both the default signageOS applet and a reusable browser SDK.
The launcher talks only to the co-located runtime daemon at 127.0.0.1:<daemonPort>. It ensures a runtime, polls until the configured application is ready, renders the daemon-provided loopback URL, and keeps the runtime lease alive with heartbeats.
Default applet
The applet waits for @signageos/front-applet readiness and launches from sos.config using sos.authHash as the current device identity.
| Configuration | Type | Default | Description |
| --- | --- | --- | --- |
| type | string | android | Application type. Only android is supported. |
| runtime | string | redroid | Android runtime. Only redroid is supported. |
| appId | string | none | Android package identifier to launch. |
| daemonPort | number | 8100 | Port of the co-located runtime daemon. |
A missing device identity or unsupported/missing Android configuration is rendered as an applet state instead of contacting the daemon.
Query bootstrap
Opening the applet with either type or appId in the query string starts it directly without waiting for sos.onReady(). Every query entry becomes raw launcher configuration, and identity defaults to standalone when omitted.
index.html?type=android&runtime=redroid&appId=com.example.app&daemonPort=8100&identity=device-idThis bootstrap is intended for standalone development and integration environments.
SDK
Installation and host runtime
The SDK treats @signageos/front-applet as a peer dependency. A signageOS host must import and initialize front-applet exactly once; the SDK reads the host-owned window.sos and does not bundle or import front-applet itself.
import sos from '@signageos/front-applet';
import SupraAppLauncher from '@signageos/supra-app-launcher';
await sos.onReady();
const launcher = new SupraAppLauncher();
const app = await launcher.start({
androidApp: 'com.example.app',
});
await app.stop();Fully explicit options can be used in a non-signageOS host without window.sos:
const app = await launcher.start({
androidApp: 'com.example.app',
identity: 'device-id',
display: { width: 1920, height: 1080 },
});UMD bundle
Load dist-sdk/supra-app-launcher-sdk.js in a browser. The default SDK class is exposed as SupraAppLauncher.
<script src="supra-app-launcher-sdk.js"></script>
<script>
const launcher = new SupraAppLauncher();
launcher.start({
androidApp: 'com.example.app',
identity: 'device-id',
});
</script>Options
interface SupraAppLauncherOptions {
androidApp?: string | {
appId: string;
type?: string;
runtime?: string;
};
geometry?: { x: number; y: number; width: number; height: number };
daemonPort?: number;
identity?: string;
display?: { width: number; height: number };
pollIntervalMs?: number;
heartbeatIntervalMs?: number;
heartbeatMaxTransientFailures?: number;
container?: HTMLElement;
rawConfig?: Record<string, unknown>;
onStarting?: () => void;
onRuntimeEnsuring?: () => void;
onRuntimeBooting?: () => void;
onRuntimeInstalling?: () => void;
onRuntimeReady?: () => void;
onRendering?: () => void;
onError?: (error: unknown) => void;
}| Option | Default | Description |
| --- | --- | --- |
| androidApp | raw/host configuration | Android package identifier or full application descriptor. String form uses type: android and runtime: redroid. |
| geometry | none | Iframe position and size in CSS pixels. Values must be finite; width and height must be positive. |
| daemonPort | 8100 | Local daemon port. |
| identity | host sos.authHash | Identity sent as daemon payload key authHash. |
| display | derived | Runtime display resolution in positive integer pixels. |
| pollIntervalMs | 2000 | Poll fallback delay when the daemon omits retryAfterMs. |
| heartbeatIntervalMs | 10000 | Delay between serialized heartbeat requests. |
| heartbeatMaxTransientFailures | 3 | Maximum consecutive retry budget for network and HTTP 5xx heartbeat failures. |
| container | document.body | Element that receives launcher status content and the stream iframe. |
| rawConfig | host sos.config | Raw configuration source, useful for embedded hosts and tests. |
| lifecycle callbacks | none | Notifications described below. |
pollIntervalMs, heartbeatIntervalMs, and heartbeatMaxTransientFailures must be non-negative integers. A value of 0 is valid.
Configuration precedence
Values are resolved in this order:
- Explicit
start()options. rawConfigpassed tostart().- Host-owned
window.sos(sos.configandsos.authHash). - Launcher defaults.
An explicitly empty identity falls through to raw or host identity. After all sources are resolved, an empty identity produces the visible Device identity unavailable state.
Identity is validated before the rest of launcher configuration. This preserves the default applet behavior when both identity and configuration are invalid.
Geometry and display
geometry controls iframe placement and size. The launcher uses absolute positioning when geometry is supplied.
Runtime display resolution uses this order:
- Explicit
display. - Raw
displayWidthanddisplayHeightconfiguration, including URL query parameters. - Floored
geometry.widthandgeometry.height. - Physical
window.screenwidth and height. - Browser viewport width and height.
displayWidth and displayHeight must be supplied together as positive integers. SupraScreen uses these query parameters to pass its render geometry to the nested launcher applet. The daemon payload omits display only when none of these sources yields a positive resolution.
Lifecycle and callbacks
For an immediately ready runtime, callback order is:
onStarting → onRuntimeEnsuring → onRuntimeReady → onRenderingA pending runtime adds onRuntimeBooting and/or onRuntimeInstalling after onRuntimeEnsuring; callbacks follow daemon state transitions. onError receives launcher configuration, daemon, polling, rendering, and terminal heartbeat errors. Missing identity and unsupported Android configuration are represented by visible states and do not call onError.
start() handles launcher errors, renders a stable error state, and resolves to an already-stopped app handle. It does not reject for those handled errors.
Cleanup behavior
start() resolves to an app handle with an asynchronous, idempotent stop() method. Calling start() again first stops the active launch. Stopping an older handle does not affect a newer launch.
stop() and browser pagehide cleanup:
- abort local in-flight requests;
- clear poll and heartbeat timers;
- remove page listeners;
- remove launcher rendering.
Cleanup is intentionally non-destructive. It never terminates the daemon runtime, never sends DELETE, and never uses an /android/sessions endpoint.
Dependency injection
Constructor dependencies are optional and primarily support deterministic host integration and testing:
const launcher = new SupraAppLauncher({
fetch: window.fetch.bind(window),
getRawConfig: () => window.sos.config,
getAuthHash: () => window.sos.authHash,
detectDisplayResolution: () => ({ width: 1920, height: 1080 }),
createGeneration: () => crypto.randomUUID(),
getDefaultContainer: () => document.querySelector('#launcher'),
renderStream: (url, container, geometry) => customStream,
showState: (state, title, detail, container) => renderStatus(state, title, detail),
addPageHideListener: (listener) => {
window.addEventListener('pagehide', listener, { once: true });
return () => window.removeEventListener('pagehide', listener);
},
setTimeout,
clearTimeout,
});Each dependency can be overridden independently. Production defaults use browser APIs and host window.sos.
Runtime daemon contract
The launcher uses only these requests:
POST /android/runtimes/ensure
GET /android/runtimes/{runtimeId}
POST /android/runtimes/{runtimeId}/heartbeatEnsure payload:
{
"authHash": "device-id",
"appId": "com.example.app",
"generation": "launcher-generation",
"display": { "width": 1920, "height": 1080 }
}Heartbeat payload:
{ "generation": "launcher-generation" }Only HTTP(S) render URLs on 127.0.0.1, localhost, or [::1] are accepted.
Development
Build and validation commands run on Node.js 20 or newer:
npm test # Compile and run TypeScript Node tests plus JS contract tests
npm run check-types # Type-check applet and SDK source
npm run lint # ESLint
npm run lint:prettier # Verify formatting
npm run build # Build default applet into dist/
npm run build:sdk # Build UMD SDK into dist-sdk/
npm run build:sdk:es6 # Build ES6 JavaScript and declarations into es6/
npm run escheck # Verify applet and UMD JavaScript compatibility
npm run clean-build # Clean and build every output
npm run check # Run lint, formatting, and source type checksBuild outputs:
| Directory | Contents |
| --- | --- |
| dist/ | Default applet bundle and static assets. |
| dist-sdk/ | ES5-compatible UMD SDK bundle and source map. |
| es6/ | ES6 CommonJS modules, declarations, and declaration maps. |
