Skip to main content

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

OptionTypeDefaultPurpose
namestringrequiredHuman name shown in /status and logs.
appKeystringrequiredApp key for analytics / future billing.
connectionType"process" | "viewer""process"process keeps the engine alive; viewer doesn't. Use viewer for UIs / browsers.
enginePathstring"glassout-engine.exe"Path or name on PATH.
portnumber8787Target port.
autoSpawnbooleantrueSpawn the engine if not running.
spawnElevatedbooleanfalseUAC-elevate the spawn (required for the engine to capture panels from MSFS).

Lifecycle

  • connect() reads engine.json, probes /status, optionally spawns the exe (UAC-elevated if requested), opens the WebSocket, and waits for the welcome message.
  • 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, MarkerInjectionStatus
  • Panel, PanelFrame, PanelCrop
  • Profile, ProfileSettings, Fragment, Template, TemplateItem, ConfigBundle, WindowPlacement, SavedScreen, WindowLayout
  • LicenseCredentials, 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);