SDK — glassout-client
glassout-client is the TypeScript SDK for talking to the engine. If you're
writing an MSFS add-on, a home-cockpit controller, a browser dashboard, or
anything else that needs panels, this is what you reach for.
Getting the SDK
glassout-client is not yet published to npm. The package will land on the
public registry once the API has stabilised and a few more integrators have
kicked the tyres.
In the meantime, if you'd like to build against it, reach out (see the contact
note at the top of this page) and I'll share the SDK files directly so you can
drop them into your project. Once it goes live on npm, a regular
npm install glassout-client (or bun add glassout-client) is all you'll need —
the import paths and API documented here won't change.
Minimal example
import { GlassOutClient } from "glassout-client";
const engine = new GlassOutClient({
name: "My MSFS App",
appKey: "my-app-key",
});
await engine.connect();
engine.on("panels", (panels) => {
console.log(
"Found panels:",
panels.map((p) => p.name),
);
});
engine.on("frame", (frame) => {
// frame.data is a self-contained JPEG — feed it to any decoder
});
engine.subscribePanel("PFD_Captain");
Options
| Option | Type | Default | Purpose |
|---|---|---|---|
name | string | required | Human name shown in /status and logs. |
appKey | string | required | App key for analytics / future billing. |
connectionType | "process" | "viewer" | "process" | process keeps the engine alive; viewer doesn't. Use viewer for UIs / browsers. |
enginePath | string | "glassout-engine.exe" | Path or name on PATH. |
port | number | 8787 | Target port. |
autoSpawn | boolean | true | Spawn the engine if not running. |
spawnElevated | boolean | false | UAC-elevate the spawn (required for the engine to capture panels from MSFS). |
Lifecycle
connect()readsengine.json, probes/status, optionally spawns the exe (UAC-elevated if requested), opens the WebSocket, and waits for thewelcomemessage.disconnect()closes the WebSocket. The engine keeps running until the last process client leaves.- Auto-reconnect is always on: exponential backoff 100 ms → 5 s; panel subscriptions are replayed after every reconnect.
Events
engine.on("connected", () => {});
engine.on("disconnected", () => {});
engine.on("reconnecting", (info) => {
/* { attempt, nextDelayMs, nextAttemptAt } */
});
engine.on("reconnected", () => {});
engine.on("state", (state: EngineState | null) => {});
engine.on("panels", (panels: Panel[]) => {});
engine.on("configs", (configs: ConfigBundle[]) => {});
engine.on("frame", (frame: PanelFrame) => {});
Subscriptions
engine.subscribePanel("PFD_Captain");
engine.subscribePanel("ND_Captain", { x: 0, y: 0, w: 768, h: 768 });
engine.subscribePanel("MFD", undefined, 30); // cap to 30 fps
engine.unsubscribePanel("PFD_Captain");
Signature: subscribePanel(panelId, crop?, targetFps?).
Passing a crop rect is a layout hint — the engine still streams the full
panel, but your viewer can render only that region. Useful for
fragment-style viewers cropping a single instrument out of a larger panel.
Pass targetFps (10–60, default 30) to cap this client's frame rate. The
engine ticks at the max across all subscribers, so lowering it on a weak
link doesn't penalise other clients watching the same panel.
Adaptive quality is automatic. Every client is placed on a discrete JPEG quality ladder (Q40 / Q60 / Q75 / Q85) and moved up or down based on the headroom of its own WebSocket link. You don't pick a quality — a phone on weak WiFi settles at a lower bucket, a wired PC stays at the top. The server stress-tests each link continuously and reacts inside 1–2 seconds.
Clicks
The SDK doesn't expose a click method directly — clicks are forwarded by the
HTML viewer at /panel/{id} (and /canvas, /instance/...). If you need to
synthesise a click from your own code, send the raw panel.input frame
yourself. Coordinates are in the source panel's native pixel space and
the engine forwards via the Coherent CDP. See the
WebSocket protocol
for the message shape.
Pushing profile bundles
process clients can push their profile/fragment/template bundles to the
engine so that /instance/... viewer URLs resolve live and other clients
see the bundle in configs:
engine.pushConfig("My Cockpit App", profiles);
The engine stores bundles in memory only and broadcasts the aggregated
list as configs on every change. The SDK re-pushes the last bundle
automatically after a reconnect.
License operations
const result = await engine.activateLicense("XXXX-XXXX-XXXX-XXXX-XXXX");
if (result.ok && result.credentials) {
// Persist result.credentials yourself — engine is stateless
await engine.validateLicense(result.credentials);
}
The engine doesn't store credentials. Your app is responsible for keeping
the returned { licenseKey, deviceToken } pair safe and re-validating on
each run via engine.validateLicense(credentials). engine.deactivateLicense(credentials)
releases the activation slot on the remote server.
Wire latency
The SDK runs an NTP-style time.sync exchange on connect (5 samples, 200 ms
apart) and re-syncs every 60 s. The estimated offset is exposed at
engine.engineOffsetMs:
engine.on("frame", (frame) => {
if (engine.engineOffsetMs !== null) {
const wireMs = Date.now() - (frame.engineTimestampMs + engine.engineOffsetMs);
// wireMs ≈ end-to-end latency from MSFS readback to JS handler
}
});
Decoding frames
Frames arrive as binary WebSocket messages. The SDK parses the header for
you and emits a PanelFrame whose data field holds standard JPEG bytes —
every frame is self-contained, so decode and render directly.
engine.on("frame", async (frame) => {
const bitmap = await createImageBitmap(
new Blob([frame.data], { type: "image/jpeg" }),
);
ctx.drawImage(bitmap, 0, 0);
bitmap.close();
});
In a browser, you don't need any of this — just use the engine's HTML viewer URLs directly (see the HTTP API).
Types
The SDK re-exports all protocol types:
EngineState,MsfsConnectionStatus,SimConnectStatus,DllStatus,EncoderStatus,DirectXInfo,MarkerInjectionInfo,MarkerInjectionStatusPanel,PanelFrame,PanelCropProfile,ProfileSettings,Fragment,Template,TemplateItem,ConfigBundle,WindowPlacement,SavedScreen,WindowLayoutLicenseCredentials,LicenseActivateResponse,LicenseValidateResponse,LicenseResponse- URL builders:
buildPanelUrl,buildFragmentConfigUrl,buildTemplateConfigUrl,buildFragmentInstanceUrl,buildTemplateInstanceUrl,buildCanvasConfigEntries,canvasEntriesToQueryString
Discovery helpers
If you want to implement the discovery logic yourself:
import { discoverEngine, probeEnginePort, spawnEngine } from "glassout-client";
const existing = await discoverEngine();
if (!existing) await spawnEngine("glassout-engine.exe", 8787);