@ceralive/cerastream 2026.7.1 → 2026.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,10 +6,12 @@ engine's JSON-RPC 2.0 / NDJSON control plane over a Unix domain socket.
6
6
 
7
7
  - JSON-RPC 2.0 envelope + `hello` handshake schemas
8
8
  - The eight v1 control methods (Zod params + result schemas + inferred types)
9
- - The seven server-push event payloads (discriminated union)
9
+ - The eight server-push event payloads (discriminated union)
10
10
  - Two-tier error codes (RPC + runtime)
11
11
  - A unified engine config schema (= `start` params)
12
12
  - A `CerastreamClient` interface + `connect()` factory (UDS transport)
13
+ - Additive `client.getCapabilities()` discovery for platform, source, encoder, and
14
+ local preview availability
13
15
 
14
16
  The Zod schemas, types, and constants are the frozen wire contract; `connect()`
15
17
  drives that contract over an NDJSON/UDS transport with no native dependencies.
@@ -45,6 +47,8 @@ import { connect } from "@ceralive/cerastream";
45
47
 
46
48
  const client = await connect({ autoReconnect: true }); // hello handshake runs here
47
49
  console.log(client.hello.engine_version);
50
+ const capabilities = await client.getCapabilities();
51
+ console.log(capabilities.preview?.bound);
48
52
 
49
53
  const sub = await client.subscribeEvents({ topics: ["status", "bitrate"] }, (ev) => {
50
54
  if (ev.type === "bitrate") console.log("bitrate", ev.current_bitrate);
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type HelloResult } from "./envelope.js";
2
2
  import { type EventParams } from "./events.js";
3
- import { type ListDevicesParams, type ListDevicesResult, type PreviewSessionParams, type PreviewSessionResult, type ReloadConfigParams, type ReloadConfigResult, type SetBitrateParams, type SetBitrateResult, type StartParams, type StartResult, type StopParams, type StopResult, type SubscribeEventsParams, type SubscribeEventsResult, type SwitchInputParams, type SwitchInputResult } from "./messages.js";
3
+ import { type GetCapabilitiesResult, type ListDevicesParams, type ListDevicesResult, type PreviewSessionParams, type PreviewSessionResult, type ReloadConfigParams, type ReloadConfigResult, type SetBitrateParams, type SetBitrateResult, type StartParams, type StartResult, type StopParams, type StopResult, type SubscribeEventsParams, type SubscribeEventsResult, type SwitchInputParams, type SwitchInputResult } from "./messages.js";
4
4
  /** Options for {@link connect}. All optional — `connect({})` is valid. */
5
5
  export interface ConnectOptions {
6
6
  /** Control socket path override. Defaults to the resolved /run/cerastream/control.sock. */
@@ -77,6 +77,11 @@ export interface CerastreamClient {
77
77
  * @returns The discovered capture devices.
78
78
  */
79
79
  listDevices(params?: ListDevicesParams): Promise<ListDevicesResult>;
80
+ /**
81
+ * Read the engine's additive capability contract, including local preview
82
+ * availability. This request intentionally sits outside the frozen v1 map.
83
+ */
84
+ getCapabilities(): Promise<GetCapabilitiesResult>;
80
85
  /**
81
86
  * Subscribe to the live event stream; `handler` fires for each pushed event.
82
87
  * @param params The topics to subscribe to (absent ⇒ all topics).
package/dist/client.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { CONTROL_SOCKET_PATH, PROTOCOL_VERSION } from "./constants.js";
2
2
  import { helloResultSchema, rpcErrorSchema, rpcResponseSchema, } from "./envelope.js";
3
+ import { existsSync } from "node:fs";
3
4
  import { CerastreamConnectionError, CerastreamRpcError, CerastreamTimeoutError, } from "./errors.js";
4
5
  import { eventParamsSchema } from "./events.js";
5
6
  import { controlSocketPath } from "./paths.js";
6
- import { requestSchemas, } from "./messages.js";
7
+ import { getCapabilitiesResultSchema, requestSchemas, } from "./messages.js";
7
8
  import { LineSocket } from "./transport.js";
8
9
  const DEFAULTS = {
9
10
  requestTimeoutMs: 10_000,
@@ -49,7 +50,7 @@ class ClientImpl {
49
50
  });
50
51
  }
51
52
  catch (err) {
52
- throw new CerastreamConnectionError(`failed to connect to cerastream control socket at ${this.socketPath}`, err);
53
+ throw new CerastreamConnectionError(`failed to connect to cerastream control socket at ${this.socketPath}`, err, classifyConnectError(err, this.socketPath));
53
54
  }
54
55
  this.socket = socket;
55
56
  }
@@ -78,6 +79,9 @@ class ClientImpl {
78
79
  listDevices(params) {
79
80
  return this.call("list-devices", params);
80
81
  }
82
+ async getCapabilities() {
83
+ return getCapabilitiesResultSchema.parse(await this.rawRequest("get-capabilities", undefined));
84
+ }
81
85
  previewSession(params) {
82
86
  return this.call("preview-session", params);
83
87
  }
@@ -98,7 +102,7 @@ class ClientImpl {
98
102
  this.intentionalClose = true;
99
103
  for (const sub of this.subscriptions)
100
104
  sub.close();
101
- const conn = new CerastreamConnectionError("client closed");
105
+ const conn = new CerastreamConnectionError("client closed", undefined, "closed");
102
106
  this.rejectAllPending(conn);
103
107
  this.socket?.close();
104
108
  this.socket = undefined;
@@ -114,7 +118,7 @@ class ClientImpl {
114
118
  rawRequest(method, params) {
115
119
  const socket = this.socket;
116
120
  if (!socket) {
117
- return Promise.reject(new CerastreamConnectionError("control connection is not open"));
121
+ return Promise.reject(new CerastreamConnectionError("control connection is not open", undefined, "closed"));
118
122
  }
119
123
  const id = this.nextId++;
120
124
  const envelope = {
@@ -136,7 +140,7 @@ class ClientImpl {
136
140
  catch (err) {
137
141
  clearTimeout(timer);
138
142
  this.pending.delete(id);
139
- reject(new CerastreamConnectionError("failed to write request", err));
143
+ reject(new CerastreamConnectionError("failed to write request", err, "lost"));
140
144
  }
141
145
  });
142
146
  }
@@ -203,7 +207,7 @@ class ClientImpl {
203
207
  this.socket = undefined;
204
208
  if (this.intentionalClose)
205
209
  return;
206
- const conn = new CerastreamConnectionError("control connection lost", err);
210
+ const conn = new CerastreamConnectionError("control connection lost", err, "lost");
207
211
  this.rejectAllPending(conn);
208
212
  if (this.autoReconnect)
209
213
  void this.reconnectLoop();
@@ -260,6 +264,20 @@ function safeControlSocketPath() {
260
264
  function isObject(value) {
261
265
  return typeof value === "object" && value !== null;
262
266
  }
267
+ // Classify a connect-time transport failure into a stable machine code. Bun's
268
+ // `connect` collapses several distinct errors onto ENOENT and does not reliably
269
+ // surface ECONNREFUSED for a stale AF_UNIX socket, so an explicit errno is
270
+ // honored when present, then the socket file's presence disambiguates: a path
271
+ // that is gone is `absent` (engine not up yet), a path that still exists but
272
+ // won't accept is `refused` (a crashed/stale listener).
273
+ function classifyConnectError(err, socketPath) {
274
+ const raw = isObject(err) ? err.code : undefined;
275
+ if (raw === "ECONNREFUSED")
276
+ return "refused";
277
+ if (raw === "EACCES" || raw === "EPERM")
278
+ return "unreachable";
279
+ return existsSync(socketPath) ? "refused" : "absent";
280
+ }
263
281
  function sleep(ms) {
264
282
  return new Promise((resolve) => setTimeout(resolve, ms));
265
283
  }
@@ -8,7 +8,7 @@ export declare const PROTOCOL_VERSION: "cerastream-ipc/1";
8
8
  * additive-only within protocol major `cerastream-ipc/1` (ADR-0002 §4); this value
9
9
  * only moves when the wire schema itself does, in lockstep across both languages.
10
10
  */
11
- export declare const SCHEMA_VERSION: "0.4.0";
11
+ export declare const SCHEMA_VERSION: "0.8.0";
12
12
  /** Runtime dir holding both control + preview sockets. systemd `RuntimeDirectory=cerastream`. */
13
13
  export declare const DEFAULT_IPC_DIR: "/run/cerastream";
14
14
  /** Env override for the IPC dir (tests/dev). Defaults to {@link DEFAULT_IPC_DIR}. */
@@ -56,6 +56,14 @@ export declare const SUPPORTED_PROFILES: readonly ["balanced", "low-latency", "r
56
56
  * set or any preset's meaning changes. Mirrors the Rust `PROFILE_CATALOG_VERSION`.
57
57
  */
58
58
  export declare const PROFILE_CATALOG_VERSION: "1.0.0";
59
+ /**
60
+ * Named engine features `get-capabilities` advertises in `features`. This is the
61
+ * fail-closed negotiation contract: CeraUI sends a new out-of-schema field ONLY
62
+ * when its feature is listed, so an engine that predates a feature (and silently
63
+ * ignores the field) never applies a semantic the caller assumed. Mirrors the
64
+ * Rust `ENGINE_FEATURES`.
65
+ */
66
+ export declare const ENGINE_FEATURES: readonly ["video-passthrough"];
59
67
  /** Engine binary name (systemd-owned; CeraUI never spawns it — ADR-0005). */
60
68
  export declare const CERASTREAM_BIN: "cerastream";
61
69
  export declare const DEFAULT_MIN_BITRATE = 300;
package/dist/constants.js CHANGED
@@ -11,7 +11,7 @@ export const PROTOCOL_VERSION = "cerastream-ipc/1";
11
11
  * additive-only within protocol major `cerastream-ipc/1` (ADR-0002 §4); this value
12
12
  * only moves when the wire schema itself does, in lockstep across both languages.
13
13
  */
14
- export const SCHEMA_VERSION = "0.4.0";
14
+ export const SCHEMA_VERSION = "0.8.0";
15
15
  /** Runtime dir holding both control + preview sockets. systemd `RuntimeDirectory=cerastream`. */
16
16
  export const DEFAULT_IPC_DIR = "/run/cerastream";
17
17
  /** Env override for the IPC dir (tests/dev). Defaults to {@link DEFAULT_IPC_DIR}. */
@@ -65,6 +65,14 @@ export const SUPPORTED_PROFILES = [
65
65
  * set or any preset's meaning changes. Mirrors the Rust `PROFILE_CATALOG_VERSION`.
66
66
  */
67
67
  export const PROFILE_CATALOG_VERSION = "1.0.0";
68
+ /**
69
+ * Named engine features `get-capabilities` advertises in `features`. This is the
70
+ * fail-closed negotiation contract: CeraUI sends a new out-of-schema field ONLY
71
+ * when its feature is listed, so an engine that predates a feature (and silently
72
+ * ignores the field) never applies a semantic the caller assumed. Mirrors the
73
+ * Rust `ENGINE_FEATURES`.
74
+ */
75
+ export const ENGINE_FEATURES = ["video-passthrough"];
68
76
  /** Engine binary name (systemd-owned; CeraUI never spawns it — ADR-0005). */
69
77
  export const CERASTREAM_BIN = "cerastream";
70
78
  // ---- config defaults (mirror ceracoder, the engine being replaced) ----
package/dist/errors.d.ts CHANGED
@@ -22,15 +22,41 @@ export declare class CerastreamRpcError extends Error {
22
22
  readonly requestId: number | string | null;
23
23
  constructor(code: number, message: string, dataCode: string | undefined, requestId: number | string | null);
24
24
  }
25
+ /**
26
+ * Machine-readable classification of a {@link CerastreamConnectionError}, stable
27
+ * across releases so CeraUI's start-failure taxonomy can branch on it without
28
+ * parsing messages. Distinguishes the three connect-phase failure shapes the
29
+ * engine's systemd lifecycle produces (an absent socket during boot, a stale
30
+ * socket left by a crashed instance, an otherwise-unreachable socket) from a
31
+ * mid-session drop or an intentional client close:
32
+ *
33
+ * - `absent` — the control socket path does not exist (ENOENT): the engine
34
+ * is not running yet, or systemd has torn the old socket down
35
+ * during a restart. Maps to `engine_unavailable`/`engine_restarting`.
36
+ * - `refused` — the socket path exists but the connection was refused
37
+ * (ECONNREFUSED): a stale socket a crashed/`SIGKILL`ed engine
38
+ * left behind, or one mid-`bind` on restart. Maps to
39
+ * `engine_restarting`/`engine_unavailable`.
40
+ * - `unreachable` — any other connect-time transport failure (e.g. EACCES).
41
+ * - `lost` — an established connection dropped (socket closed/errored) or
42
+ * a write failed while a request was in flight.
43
+ * - `closed` — the client was closed, or a request was issued on a
44
+ * not-open connection.
45
+ */
46
+ export declare const CERASTREAM_CONNECT_ERROR_CODES: readonly ["absent", "refused", "unreachable", "lost", "closed"];
47
+ export type CerastreamConnectErrorCode = (typeof CERASTREAM_CONNECT_ERROR_CODES)[number];
25
48
  /**
26
49
  * Thrown when the control connection is lost (socket closed/errored) while a
27
50
  * request was in flight, or when {@link connect} cannot reach the engine and
28
- * auto-reconnect is disabled.
51
+ * auto-reconnect is disabled. The {@link code} field classifies the failure —
52
+ * absent vs refused vs unreachable vs lost vs closed — for machine routing.
29
53
  */
30
54
  export declare class CerastreamConnectionError extends Error {
55
+ /** Stable machine-readable classification of the connection failure. */
56
+ readonly code: CerastreamConnectErrorCode;
31
57
  /** The underlying transport error, when there was one. */
32
58
  readonly cause: unknown;
33
- constructor(message: string, cause?: unknown);
59
+ constructor(message: string, cause?: unknown, code?: CerastreamConnectErrorCode);
34
60
  }
35
61
  /** Thrown when a request exceeds the configured per-request timeout. */
36
62
  export declare class CerastreamTimeoutError extends Error {
package/dist/errors.js CHANGED
@@ -31,17 +31,49 @@ export class CerastreamRpcError extends Error {
31
31
  this.requestId = requestId;
32
32
  }
33
33
  }
34
+ /**
35
+ * Machine-readable classification of a {@link CerastreamConnectionError}, stable
36
+ * across releases so CeraUI's start-failure taxonomy can branch on it without
37
+ * parsing messages. Distinguishes the three connect-phase failure shapes the
38
+ * engine's systemd lifecycle produces (an absent socket during boot, a stale
39
+ * socket left by a crashed instance, an otherwise-unreachable socket) from a
40
+ * mid-session drop or an intentional client close:
41
+ *
42
+ * - `absent` — the control socket path does not exist (ENOENT): the engine
43
+ * is not running yet, or systemd has torn the old socket down
44
+ * during a restart. Maps to `engine_unavailable`/`engine_restarting`.
45
+ * - `refused` — the socket path exists but the connection was refused
46
+ * (ECONNREFUSED): a stale socket a crashed/`SIGKILL`ed engine
47
+ * left behind, or one mid-`bind` on restart. Maps to
48
+ * `engine_restarting`/`engine_unavailable`.
49
+ * - `unreachable` — any other connect-time transport failure (e.g. EACCES).
50
+ * - `lost` — an established connection dropped (socket closed/errored) or
51
+ * a write failed while a request was in flight.
52
+ * - `closed` — the client was closed, or a request was issued on a
53
+ * not-open connection.
54
+ */
55
+ export const CERASTREAM_CONNECT_ERROR_CODES = [
56
+ "absent",
57
+ "refused",
58
+ "unreachable",
59
+ "lost",
60
+ "closed",
61
+ ];
34
62
  /**
35
63
  * Thrown when the control connection is lost (socket closed/errored) while a
36
64
  * request was in flight, or when {@link connect} cannot reach the engine and
37
- * auto-reconnect is disabled.
65
+ * auto-reconnect is disabled. The {@link code} field classifies the failure —
66
+ * absent vs refused vs unreachable vs lost vs closed — for machine routing.
38
67
  */
39
68
  export class CerastreamConnectionError extends Error {
69
+ /** Stable machine-readable classification of the connection failure. */
70
+ code;
40
71
  /** The underlying transport error, when there was one. */
41
72
  cause;
42
- constructor(message, cause) {
73
+ constructor(message, cause, code = "unreachable") {
43
74
  super(message);
44
75
  this.name = "CerastreamConnectionError";
76
+ this.code = code;
45
77
  this.cause = cause;
46
78
  }
47
79
  }
package/dist/events.d.ts CHANGED
@@ -13,6 +13,11 @@ export declare const activeEncodeSchema: z.ZodObject<{
13
13
  active_input: z.ZodOptional<z.ZodString>;
14
14
  decoder: z.ZodOptional<z.ZodString>;
15
15
  input_codec: z.ZodOptional<z.ZodString>;
16
+ passthrough: z.ZodOptional<z.ZodBoolean>;
17
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
18
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
19
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
20
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
16
21
  }, z.core.$strip>;
17
22
  export type ActiveEncode = z.infer<typeof activeEncodeSchema>;
18
23
  /**
@@ -48,6 +53,11 @@ export declare const statusEventSchema: z.ZodObject<{
48
53
  active_input: z.ZodOptional<z.ZodString>;
49
54
  decoder: z.ZodOptional<z.ZodString>;
50
55
  input_codec: z.ZodOptional<z.ZodString>;
56
+ passthrough: z.ZodOptional<z.ZodBoolean>;
57
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
58
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
59
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
60
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
51
61
  }, z.core.$strip>>;
52
62
  }, z.core.$strip>;
53
63
  /** Payload of a {@link statusEventSchema} event. */
@@ -106,6 +116,14 @@ export declare const deviceEventSchema: z.ZodObject<{
106
116
  network: "network";
107
117
  }>>;
108
118
  alsa_card_id: z.ZodOptional<z.ZodString>;
119
+ product_name: z.ZodOptional<z.ZodString>;
120
+ transport: z.ZodOptional<z.ZodEnum<{
121
+ hdmi: "hdmi";
122
+ usb: "usb";
123
+ bluetooth: "bluetooth";
124
+ onboard: "onboard";
125
+ }>>;
126
+ stable_id: z.ZodOptional<z.ZodString>;
109
127
  }, z.core.$strip>;
110
128
  }, z.core.$strip>;
111
129
  /** Payload of a {@link deviceEventSchema} event. */
@@ -162,6 +180,40 @@ export declare const previewEventSchema: z.ZodObject<{
162
180
  }, z.core.$strip>;
163
181
  /** Payload of a {@link previewEventSchema} event. */
164
182
  export type PreviewEvent = z.infer<typeof previewEventSchema>;
183
+ /**
184
+ * `audio-level` event — the always-on audio level meter (ADR-0007, additive Todo
185
+ * 21). Either a real per-channel level from the current owner (the idle
186
+ * `sidecar` or the `streaming` audio leg), or `unavailable: true` + `reason` for
187
+ * a handoff gap or a degenerate `audio.mode` — never a fabricated level. A level
188
+ * carries `source`/`channels`/`rms_db`/`peak_db`/`floor_db`; an unavailable event
189
+ * carries `unavailable`/`reason` and omits the level fields. `floor_db` (`-1e6`)
190
+ * is the sentinel a consumer maps back to "silence" (JSON has no `-Infinity`).
191
+ * Independent of the preview-scoped `audio-level` WebSocket frame.
192
+ */
193
+ export declare const audioLevelEventSchema: z.ZodObject<{
194
+ type: z.ZodLiteral<"audio-level">;
195
+ seq: z.ZodNumber;
196
+ source: z.ZodOptional<z.ZodObject<{
197
+ identity: z.ZodOptional<z.ZodString>;
198
+ owner: z.ZodEnum<{
199
+ streaming: "streaming";
200
+ sidecar: "sidecar";
201
+ }>;
202
+ }, z.core.$strip>>;
203
+ channels: z.ZodOptional<z.ZodNumber>;
204
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
205
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
206
+ floor_db: z.ZodOptional<z.ZodNumber>;
207
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
208
+ reason: z.ZodOptional<z.ZodEnum<{
209
+ device_busy: "device_busy";
210
+ no_device: "no_device";
211
+ mode_none: "mode_none";
212
+ handoff: "handoff";
213
+ }>>;
214
+ }, z.core.$strip>;
215
+ /** Payload of a {@link audioLevelEventSchema} event. */
216
+ export type AudioLevelEvent = z.infer<typeof audioLevelEventSchema>;
165
217
  /** Discriminated union of every v1 event payload (the inner `params`). */
166
218
  export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
167
219
  type: z.ZodLiteral<"status">;
@@ -186,6 +238,11 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
186
238
  active_input: z.ZodOptional<z.ZodString>;
187
239
  decoder: z.ZodOptional<z.ZodString>;
188
240
  input_codec: z.ZodOptional<z.ZodString>;
241
+ passthrough: z.ZodOptional<z.ZodBoolean>;
242
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
243
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
244
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
245
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
189
246
  }, z.core.$strip>>;
190
247
  }, z.core.$strip>, z.ZodObject<{
191
248
  type: z.ZodLiteral<"switch">;
@@ -232,6 +289,14 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
232
289
  network: "network";
233
290
  }>>;
234
291
  alsa_card_id: z.ZodOptional<z.ZodString>;
292
+ product_name: z.ZodOptional<z.ZodString>;
293
+ transport: z.ZodOptional<z.ZodEnum<{
294
+ hdmi: "hdmi";
295
+ usb: "usb";
296
+ bluetooth: "bluetooth";
297
+ onboard: "onboard";
298
+ }>>;
299
+ stable_id: z.ZodOptional<z.ZodString>;
235
300
  }, z.core.$strip>;
236
301
  }, z.core.$strip>, z.ZodObject<{
237
302
  type: z.ZodLiteral<"bitrate">;
@@ -266,6 +331,27 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
266
331
  seq: z.ZodNumber;
267
332
  session_id: z.ZodString;
268
333
  phase: z.ZodString;
334
+ }, z.core.$strip>, z.ZodObject<{
335
+ type: z.ZodLiteral<"audio-level">;
336
+ seq: z.ZodNumber;
337
+ source: z.ZodOptional<z.ZodObject<{
338
+ identity: z.ZodOptional<z.ZodString>;
339
+ owner: z.ZodEnum<{
340
+ streaming: "streaming";
341
+ sidecar: "sidecar";
342
+ }>;
343
+ }, z.core.$strip>>;
344
+ channels: z.ZodOptional<z.ZodNumber>;
345
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
346
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
347
+ floor_db: z.ZodOptional<z.ZodNumber>;
348
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
349
+ reason: z.ZodOptional<z.ZodEnum<{
350
+ device_busy: "device_busy";
351
+ no_device: "no_device";
352
+ mode_none: "mode_none";
353
+ handoff: "handoff";
354
+ }>>;
269
355
  }, z.core.$strip>], "type">;
270
356
  export type EventParams = z.infer<typeof eventParamsSchema>;
271
357
  /** Full event envelope: { jsonrpc, method:"event", params } with typed params. */
@@ -295,6 +381,11 @@ export declare const cerastreamEventSchema: z.ZodObject<{
295
381
  active_input: z.ZodOptional<z.ZodString>;
296
382
  decoder: z.ZodOptional<z.ZodString>;
297
383
  input_codec: z.ZodOptional<z.ZodString>;
384
+ passthrough: z.ZodOptional<z.ZodBoolean>;
385
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
386
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
387
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
388
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
298
389
  }, z.core.$strip>>;
299
390
  }, z.core.$strip>, z.ZodObject<{
300
391
  type: z.ZodLiteral<"switch">;
@@ -341,6 +432,14 @@ export declare const cerastreamEventSchema: z.ZodObject<{
341
432
  network: "network";
342
433
  }>>;
343
434
  alsa_card_id: z.ZodOptional<z.ZodString>;
435
+ product_name: z.ZodOptional<z.ZodString>;
436
+ transport: z.ZodOptional<z.ZodEnum<{
437
+ hdmi: "hdmi";
438
+ usb: "usb";
439
+ bluetooth: "bluetooth";
440
+ onboard: "onboard";
441
+ }>>;
442
+ stable_id: z.ZodOptional<z.ZodString>;
344
443
  }, z.core.$strip>;
345
444
  }, z.core.$strip>, z.ZodObject<{
346
445
  type: z.ZodLiteral<"bitrate">;
@@ -375,11 +474,33 @@ export declare const cerastreamEventSchema: z.ZodObject<{
375
474
  seq: z.ZodNumber;
376
475
  session_id: z.ZodString;
377
476
  phase: z.ZodString;
477
+ }, z.core.$strip>, z.ZodObject<{
478
+ type: z.ZodLiteral<"audio-level">;
479
+ seq: z.ZodNumber;
480
+ source: z.ZodOptional<z.ZodObject<{
481
+ identity: z.ZodOptional<z.ZodString>;
482
+ owner: z.ZodEnum<{
483
+ streaming: "streaming";
484
+ sidecar: "sidecar";
485
+ }>;
486
+ }, z.core.$strip>>;
487
+ channels: z.ZodOptional<z.ZodNumber>;
488
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
489
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
490
+ floor_db: z.ZodOptional<z.ZodNumber>;
491
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
492
+ reason: z.ZodOptional<z.ZodEnum<{
493
+ device_busy: "device_busy";
494
+ no_device: "no_device";
495
+ mode_none: "mode_none";
496
+ handoff: "handoff";
497
+ }>>;
378
498
  }, z.core.$strip>], "type">;
379
499
  }, z.core.$strip>;
380
500
  export type CerastreamEvent = z.infer<typeof cerastreamEventSchema>;
381
- /** The seven event topics (schema.md "Events" table) — count-assertion source. */
382
- export declare const EVENT_TOPICS: readonly ["status", "switch", "device", "bitrate", "srt-stats", "error", "preview"];
501
+ /** The eight event topics (schema.md "Events" table) — count-assertion source.
502
+ * `audio-level` is additive (Todo 21) and appended last, preserving order. */
503
+ export declare const EVENT_TOPICS: readonly ["status", "switch", "device", "bitrate", "srt-stats", "error", "preview", "audio-level"];
383
504
  export type EventTopicName = (typeof EVENT_TOPICS)[number];
384
505
  /** topic → payload Zod schema. Tests assert every topic is represented here. */
385
506
  export declare const eventSchemas: {
@@ -406,6 +527,11 @@ export declare const eventSchemas: {
406
527
  active_input: z.ZodOptional<z.ZodString>;
407
528
  decoder: z.ZodOptional<z.ZodString>;
408
529
  input_codec: z.ZodOptional<z.ZodString>;
530
+ passthrough: z.ZodOptional<z.ZodBoolean>;
531
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
532
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
533
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
534
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
409
535
  }, z.core.$strip>>;
410
536
  }, z.core.$strip>;
411
537
  readonly switch: z.ZodObject<{
@@ -454,6 +580,14 @@ export declare const eventSchemas: {
454
580
  network: "network";
455
581
  }>>;
456
582
  alsa_card_id: z.ZodOptional<z.ZodString>;
583
+ product_name: z.ZodOptional<z.ZodString>;
584
+ transport: z.ZodOptional<z.ZodEnum<{
585
+ hdmi: "hdmi";
586
+ usb: "usb";
587
+ bluetooth: "bluetooth";
588
+ onboard: "onboard";
589
+ }>>;
590
+ stable_id: z.ZodOptional<z.ZodString>;
457
591
  }, z.core.$strip>;
458
592
  }, z.core.$strip>;
459
593
  readonly bitrate: z.ZodObject<{
@@ -493,4 +627,26 @@ export declare const eventSchemas: {
493
627
  session_id: z.ZodString;
494
628
  phase: z.ZodString;
495
629
  }, z.core.$strip>;
630
+ readonly "audio-level": z.ZodObject<{
631
+ type: z.ZodLiteral<"audio-level">;
632
+ seq: z.ZodNumber;
633
+ source: z.ZodOptional<z.ZodObject<{
634
+ identity: z.ZodOptional<z.ZodString>;
635
+ owner: z.ZodEnum<{
636
+ streaming: "streaming";
637
+ sidecar: "sidecar";
638
+ }>;
639
+ }, z.core.$strip>>;
640
+ channels: z.ZodOptional<z.ZodNumber>;
641
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
642
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
643
+ floor_db: z.ZodOptional<z.ZodNumber>;
644
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
645
+ reason: z.ZodOptional<z.ZodEnum<{
646
+ device_busy: "device_busy";
647
+ no_device: "no_device";
648
+ mode_none: "mode_none";
649
+ handoff: "handoff";
650
+ }>>;
651
+ }, z.core.$strip>;
496
652
  };
package/dist/events.js CHANGED
@@ -19,6 +19,11 @@ export const activeEncodeSchema = z.object({
19
19
  active_input: z.string().optional(), // active input id feeding the encode
20
20
  decoder: z.string().optional(), // runtime-selected decode element when transcoding
21
21
  input_codec: z.string().optional(), // incoming pre-decode codec token: "h264"/"h265"/"mjpeg"
22
+ passthrough: z.boolean().optional(), // 0.5.0: true when a same-codec passthrough graph is live (no decode/re-encode)
23
+ gop_probe_outcome: z.string().optional(), // 0.5.0: GOP preflight outcome: "eligible"|"ineligible"|"unknown"|"skipped"
24
+ gop_probe_ms: z.number().int().nonnegative().optional(), // 0.5.0: GOP preflight duration in ms when a probe ran
25
+ frames_emitted: z.number().int().nonnegative().optional(), // 0.7.0: monotonic egress-buffer counter; advances across status heartbeats while frames flow
26
+ pipeline_playing: z.boolean().optional(), // 0.7.0: pipeline is in GStreamer PLAYING (false during an in-process reconnect)
22
27
  });
23
28
  /**
24
29
  * `status` event — stream state change + heartbeat. The trailing fields are
@@ -96,6 +101,32 @@ export const previewEventSchema = z.object({
96
101
  session_id: z.string(),
97
102
  phase: z.string(),
98
103
  });
104
+ /**
105
+ * `audio-level` event — the always-on audio level meter (ADR-0007, additive Todo
106
+ * 21). Either a real per-channel level from the current owner (the idle
107
+ * `sidecar` or the `streaming` audio leg), or `unavailable: true` + `reason` for
108
+ * a handoff gap or a degenerate `audio.mode` — never a fabricated level. A level
109
+ * carries `source`/`channels`/`rms_db`/`peak_db`/`floor_db`; an unavailable event
110
+ * carries `unavailable`/`reason` and omits the level fields. `floor_db` (`-1e6`)
111
+ * is the sentinel a consumer maps back to "silence" (JSON has no `-Infinity`).
112
+ * Independent of the preview-scoped `audio-level` WebSocket frame.
113
+ */
114
+ export const audioLevelEventSchema = z.object({
115
+ type: z.literal("audio-level"),
116
+ seq,
117
+ source: z
118
+ .object({
119
+ identity: z.string().optional(), // reboot-stable device id (Todo 20 stable_id)
120
+ owner: z.enum(["sidecar", "streaming"]),
121
+ })
122
+ .optional(),
123
+ channels: z.number().int().nonnegative().optional(),
124
+ rms_db: z.array(z.number()).optional(),
125
+ peak_db: z.array(z.number()).optional(),
126
+ floor_db: z.number().optional(),
127
+ unavailable: z.literal(true).optional(),
128
+ reason: z.enum(["device_busy", "no_device", "mode_none", "handoff"]).optional(),
129
+ });
99
130
  /** Discriminated union of every v1 event payload (the inner `params`). */
100
131
  export const eventParamsSchema = z.discriminatedUnion("type", [
101
132
  statusEventSchema,
@@ -105,6 +136,7 @@ export const eventParamsSchema = z.discriminatedUnion("type", [
105
136
  srtStatsEventSchema,
106
137
  runtimeErrorEventSchema,
107
138
  previewEventSchema,
139
+ audioLevelEventSchema,
108
140
  ]);
109
141
  /** Full event envelope: { jsonrpc, method:"event", params } with typed params. */
110
142
  export const cerastreamEventSchema = z.object({
@@ -112,7 +144,8 @@ export const cerastreamEventSchema = z.object({
112
144
  method: z.literal("event"),
113
145
  params: eventParamsSchema,
114
146
  });
115
- /** The seven event topics (schema.md "Events" table) — count-assertion source. */
147
+ /** The eight event topics (schema.md "Events" table) — count-assertion source.
148
+ * `audio-level` is additive (Todo 21) and appended last, preserving order. */
116
149
  export const EVENT_TOPICS = [
117
150
  "status",
118
151
  "switch",
@@ -121,6 +154,7 @@ export const EVENT_TOPICS = [
121
154
  "srt-stats",
122
155
  "error",
123
156
  "preview",
157
+ "audio-level",
124
158
  ];
125
159
  /** topic → payload Zod schema. Tests assert every topic is represented here. */
126
160
  export const eventSchemas = {
@@ -131,4 +165,5 @@ export const eventSchemas = {
131
165
  "srt-stats": srtStatsEventSchema,
132
166
  error: runtimeErrorEventSchema,
133
167
  preview: previewEventSchema,
168
+ "audio-level": audioLevelEventSchema,
134
169
  };
@@ -28,10 +28,20 @@ export declare const startParamsSchema: z.ZodObject<{
28
28
  resolution: z.ZodOptional<z.ZodString>;
29
29
  framerate: z.ZodOptional<z.ZodNumber>;
30
30
  audio: z.ZodOptional<z.ZodObject<{
31
+ mode: z.ZodOptional<z.ZodEnum<{
32
+ default: "default";
33
+ none: "none";
34
+ device: "device";
35
+ }>>;
31
36
  device: z.ZodOptional<z.ZodString>;
32
37
  codec: z.ZodOptional<z.ZodString>;
33
38
  delay_ms: z.ZodOptional<z.ZodNumber>;
34
39
  }, z.core.$strip>>;
40
+ video_passthrough: z.ZodOptional<z.ZodEnum<{
41
+ auto: "auto";
42
+ force: "force";
43
+ off: "off";
44
+ }>>;
35
45
  }, z.core.$strip>;
36
46
  export type StartParams = z.infer<typeof startParamsSchema>;
37
47
  export declare const startResultSchema: z.ZodObject<{
@@ -95,6 +105,11 @@ export declare const reloadConfigResultSchema: z.ZodObject<{
95
105
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
96
106
  }, z.core.$strip>>;
97
107
  }, z.core.$strip>;
108
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
109
+ encoder: "encoder";
110
+ "source-fixed": "source-fixed";
111
+ }>>;
112
+ reason: z.ZodOptional<z.ZodString>;
98
113
  }, z.core.$strip>;
99
114
  export type ReloadConfigResult = z.infer<typeof reloadConfigResultSchema>;
100
115
  export declare const setBitrateParamsSchema: z.ZodObject<{
@@ -105,6 +120,11 @@ export declare const setBitrateResultSchema: z.ZodObject<{
105
120
  applied: z.ZodObject<{
106
121
  max_bitrate: z.ZodNumber;
107
122
  }, z.core.$strip>;
123
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
124
+ encoder: "encoder";
125
+ "source-fixed": "source-fixed";
126
+ }>>;
127
+ reason: z.ZodOptional<z.ZodString>;
108
128
  }, z.core.$strip>;
109
129
  export type SetBitrateResult = z.infer<typeof setBitrateResultSchema>;
110
130
  export declare const switchInputParamsSchema: z.ZodObject<{
@@ -172,6 +192,14 @@ export declare const listDevicesResultSchema: z.ZodObject<{
172
192
  network: "network";
173
193
  }>>;
174
194
  alsa_card_id: z.ZodOptional<z.ZodString>;
195
+ product_name: z.ZodOptional<z.ZodString>;
196
+ transport: z.ZodOptional<z.ZodEnum<{
197
+ hdmi: "hdmi";
198
+ usb: "usb";
199
+ bluetooth: "bluetooth";
200
+ onboard: "onboard";
201
+ }>>;
202
+ stable_id: z.ZodOptional<z.ZodString>;
175
203
  }, z.core.$strip>>;
176
204
  }, z.core.$strip>;
177
205
  export type ListDevicesResult = z.infer<typeof listDevicesResultSchema>;
@@ -183,6 +211,7 @@ export declare const eventTopicSchema: z.ZodEnum<{
183
211
  switch: "switch";
184
212
  "srt-stats": "srt-stats";
185
213
  preview: "preview";
214
+ "audio-level": "audio-level";
186
215
  }>;
187
216
  export type EventTopic = z.infer<typeof eventTopicSchema>;
188
217
  export declare const subscribeEventsParamsSchema: z.ZodObject<{
@@ -194,6 +223,7 @@ export declare const subscribeEventsParamsSchema: z.ZodObject<{
194
223
  switch: "switch";
195
224
  "srt-stats": "srt-stats";
196
225
  preview: "preview";
226
+ "audio-level": "audio-level";
197
227
  }>>>;
198
228
  }, z.core.$strip>;
199
229
  export type SubscribeEventsParams = z.infer<typeof subscribeEventsParamsSchema>;
@@ -206,6 +236,7 @@ export declare const subscribeEventsResultSchema: z.ZodObject<{
206
236
  switch: "switch";
207
237
  "srt-stats": "srt-stats";
208
238
  preview: "preview";
239
+ "audio-level": "audio-level";
209
240
  }>>;
210
241
  }, z.core.$strip>;
211
242
  export type SubscribeEventsResult = z.infer<typeof subscribeEventsResultSchema>;
@@ -274,6 +305,16 @@ export declare const platformCapsSchema: z.ZodObject<{
274
305
  supports_h265: z.ZodBoolean;
275
306
  hardware_accelerated: z.ZodBoolean;
276
307
  max_resolution: z.ZodString;
308
+ hardware_kind: z.ZodOptional<z.ZodEnum<{
309
+ rk3588: "rk3588";
310
+ jetson: "jetson";
311
+ n100: "n100";
312
+ generic: "generic";
313
+ }>>;
314
+ source: z.ZodOptional<z.ZodEnum<{
315
+ detected: "detected";
316
+ override: "override";
317
+ }>>;
277
318
  }, z.core.$strip>;
278
319
  export type PlatformCaps = z.infer<typeof platformCapsSchema>;
279
320
  export declare const previewAvailabilitySchema: z.ZodObject<{
@@ -287,6 +328,16 @@ export declare const getCapabilitiesResultSchema: z.ZodObject<{
287
328
  supports_h265: z.ZodBoolean;
288
329
  hardware_accelerated: z.ZodBoolean;
289
330
  max_resolution: z.ZodString;
331
+ hardware_kind: z.ZodOptional<z.ZodEnum<{
332
+ rk3588: "rk3588";
333
+ jetson: "jetson";
334
+ n100: "n100";
335
+ generic: "generic";
336
+ }>>;
337
+ source: z.ZodOptional<z.ZodEnum<{
338
+ detected: "detected";
339
+ override: "override";
340
+ }>>;
290
341
  }, z.core.$strip>;
291
342
  encoder: z.ZodObject<{
292
343
  codecs: z.ZodArray<z.ZodString>;
@@ -319,6 +370,7 @@ export declare const getCapabilitiesResultSchema: z.ZodObject<{
319
370
  bound: z.ZodBoolean;
320
371
  }, z.core.$strip>>;
321
372
  network_embedded_audio: z.ZodOptional<z.ZodBoolean>;
373
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
322
374
  }, z.core.$strip>;
323
375
  export type GetCapabilitiesResult = z.infer<typeof getCapabilitiesResultSchema>;
324
376
  /** The eight v1 control methods (the literal JSON-RPC `method` strings). */
@@ -371,10 +423,20 @@ export declare const requestSchemas: {
371
423
  resolution: z.ZodOptional<z.ZodString>;
372
424
  framerate: z.ZodOptional<z.ZodNumber>;
373
425
  audio: z.ZodOptional<z.ZodObject<{
426
+ mode: z.ZodOptional<z.ZodEnum<{
427
+ default: "default";
428
+ none: "none";
429
+ device: "device";
430
+ }>>;
374
431
  device: z.ZodOptional<z.ZodString>;
375
432
  codec: z.ZodOptional<z.ZodString>;
376
433
  delay_ms: z.ZodOptional<z.ZodNumber>;
377
434
  }, z.core.$strip>>;
435
+ video_passthrough: z.ZodOptional<z.ZodEnum<{
436
+ auto: "auto";
437
+ force: "force";
438
+ off: "off";
439
+ }>>;
378
440
  }, z.core.$strip>;
379
441
  readonly result: z.ZodObject<{
380
442
  session_id: z.ZodString;
@@ -437,6 +499,11 @@ export declare const requestSchemas: {
437
499
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
438
500
  }, z.core.$strip>>;
439
501
  }, z.core.$strip>;
502
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
503
+ encoder: "encoder";
504
+ "source-fixed": "source-fixed";
505
+ }>>;
506
+ reason: z.ZodOptional<z.ZodString>;
440
507
  }, z.core.$strip>;
441
508
  };
442
509
  readonly "set-bitrate": {
@@ -447,6 +514,11 @@ export declare const requestSchemas: {
447
514
  applied: z.ZodObject<{
448
515
  max_bitrate: z.ZodNumber;
449
516
  }, z.core.$strip>;
517
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
518
+ encoder: "encoder";
519
+ "source-fixed": "source-fixed";
520
+ }>>;
521
+ reason: z.ZodOptional<z.ZodString>;
450
522
  }, z.core.$strip>;
451
523
  };
452
524
  readonly "switch-input": {
@@ -498,6 +570,14 @@ export declare const requestSchemas: {
498
570
  network: "network";
499
571
  }>>;
500
572
  alsa_card_id: z.ZodOptional<z.ZodString>;
573
+ product_name: z.ZodOptional<z.ZodString>;
574
+ transport: z.ZodOptional<z.ZodEnum<{
575
+ hdmi: "hdmi";
576
+ usb: "usb";
577
+ bluetooth: "bluetooth";
578
+ onboard: "onboard";
579
+ }>>;
580
+ stable_id: z.ZodOptional<z.ZodString>;
501
581
  }, z.core.$strip>>;
502
582
  }, z.core.$strip>;
503
583
  };
@@ -511,6 +591,7 @@ export declare const requestSchemas: {
511
591
  switch: "switch";
512
592
  "srt-stats": "srt-stats";
513
593
  preview: "preview";
594
+ "audio-level": "audio-level";
514
595
  }>>>;
515
596
  }, z.core.$strip>;
516
597
  readonly result: z.ZodObject<{
@@ -522,6 +603,7 @@ export declare const requestSchemas: {
522
603
  switch: "switch";
523
604
  "srt-stats": "srt-stats";
524
605
  preview: "preview";
606
+ "audio-level": "audio-level";
525
607
  }>>;
526
608
  }, z.core.$strip>;
527
609
  };
package/dist/messages.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { AUDIO_DELAY_MAX_MS } from "./constants.js";
3
3
  import { helloParamsSchema, helloResultSchema, } from "./envelope.js";
4
- import { balancerAlgorithmSchema, captureDeviceSchema, cerastreamConfigSchema, inputModeSchema, mediaClassSchema, previewTierSchema, streamStateSchema, } from "./types.js";
4
+ import { balancerAlgorithmSchema, bitrateControlSchema, captureDeviceSchema, cerastreamConfigSchema, inputModeSchema, mediaClassSchema, previewTierSchema, streamStateSchema, } from "./types.js";
5
5
  // The eight v1 control methods (schema.md "v1 messages"). Each is matched
6
6
  // byte-for-byte to schema.md: the JSON shape on the wire is the contract.
7
7
  // ---- 1. start ----
@@ -44,6 +44,10 @@ export const reloadConfigParamsSchema = z.object({
44
44
  });
45
45
  export const reloadConfigResultSchema = z.object({
46
46
  applied: reloadConfigParamsSchema, // post-clamp values actually applied
47
+ // additive (0.5.0): a bitrate-bearing reload during passthrough is NOT applied —
48
+ // "source-fixed" + reason "passthrough" report that. Absent on a transcode reload.
49
+ bitrate_control: bitrateControlSchema.optional(),
50
+ reason: z.string().optional(),
47
51
  });
48
52
  // ---- 4. set-bitrate ----
49
53
  export const setBitrateParamsSchema = z.object({
@@ -51,6 +55,10 @@ export const setBitrateParamsSchema = z.object({
51
55
  });
52
56
  export const setBitrateResultSchema = z.object({
53
57
  applied: z.object({ max_bitrate: z.number().int() }),
58
+ // additive (0.5.0). On passthrough there is no encoder to drive, so a request is
59
+ // NOT applied: "source-fixed" with reason "passthrough". Absent/"encoder" ⇒ applied.
60
+ bitrate_control: bitrateControlSchema.optional(),
61
+ reason: z.string().optional(),
54
62
  });
55
63
  // ---- 5. switch-input ----
56
64
  export const switchInputParamsSchema = z.object({
@@ -91,6 +99,7 @@ export const eventTopicSchema = z.enum([
91
99
  "srt-stats",
92
100
  "error",
93
101
  "preview",
102
+ "audio-level",
94
103
  ]);
95
104
  export const subscribeEventsParamsSchema = z.object({
96
105
  topics: z.array(eventTopicSchema).optional(), // default: all topics
@@ -154,6 +163,10 @@ export const platformCapsSchema = z.object({
154
163
  supports_h265: z.boolean(),
155
164
  hardware_accelerated: z.boolean(),
156
165
  max_resolution: z.string(),
166
+ // additive: the engine's resolved hardware kind and how it was resolved.
167
+ // Optional so a pre-field engine (which omits both) still parses.
168
+ hardware_kind: z.enum(["rk3588", "jetson", "n100", "generic"]).optional(),
169
+ source: z.enum(["detected", "override"]).optional(),
157
170
  });
158
171
  // Preview-server availability (0.4.0, additive). Lets the UI tell an unbound /
159
172
  // port-conflicted preview (enabled:true, bound:false) from a down engine.
@@ -173,6 +186,7 @@ export const getCapabilitiesResultSchema = z.object({
173
186
  profile_catalog_version: z.string().optional(), // semver of the supported_profiles catalog
174
187
  preview: previewAvailabilitySchema.optional(), // preview-server availability (unbound vs down)
175
188
  network_embedded_audio: z.boolean().optional(), // engine routes network-ingest embedded audio to the mux
189
+ features: z.array(z.string()).optional(), // named engine features (e.g. "video-passthrough") for fail-closed negotiation
176
190
  });
177
191
  // ---- method registry (count-assertion source of truth) ----
178
192
  /** The eight v1 control methods (the literal JSON-RPC `method` strings). */
package/dist/types.d.ts CHANGED
@@ -32,6 +32,17 @@ export declare const videoCodecSchema: z.ZodEnum<{
32
32
  h265: "h265";
33
33
  }>;
34
34
  export type VideoCodec = z.infer<typeof videoCodecSchema>;
35
+ export declare const videoPassthroughSchema: z.ZodEnum<{
36
+ auto: "auto";
37
+ force: "force";
38
+ off: "off";
39
+ }>;
40
+ export type VideoPassthrough = z.infer<typeof videoPassthroughSchema>;
41
+ export declare const bitrateControlSchema: z.ZodEnum<{
42
+ encoder: "encoder";
43
+ "source-fixed": "source-fixed";
44
+ }>;
45
+ export type BitrateControl = z.infer<typeof bitrateControlSchema>;
35
46
  export declare const captureDeviceKindSchema: z.ZodEnum<{
36
47
  audio: "audio";
37
48
  hdmi: "hdmi";
@@ -43,6 +54,13 @@ export declare const captureDeviceKindSchema: z.ZodEnum<{
43
54
  network: "network";
44
55
  }>;
45
56
  export type CaptureDeviceKind = z.infer<typeof captureDeviceKindSchema>;
57
+ export declare const deviceTransportSchema: z.ZodEnum<{
58
+ hdmi: "hdmi";
59
+ usb: "usb";
60
+ bluetooth: "bluetooth";
61
+ onboard: "onboard";
62
+ }>;
63
+ export type DeviceTransport = z.infer<typeof deviceTransportSchema>;
46
64
  /** SRT transport config. Mirrors schema.md `start.srt` exactly. */
47
65
  export declare const srtConfigSchema: z.ZodObject<{
48
66
  host: z.ZodString;
@@ -73,7 +91,18 @@ export type BitrateConfig = z.infer<typeof bitrateConfigSchema>;
73
91
  * later, negative = earlier); it is clamped to ±AUDIO_DELAY_MAX_MS when applied,
74
92
  * never rejected — so the schema does NOT bound it.
75
93
  */
94
+ export declare const audioModeSchema: z.ZodEnum<{
95
+ default: "default";
96
+ none: "none";
97
+ device: "device";
98
+ }>;
99
+ export type AudioMode = z.infer<typeof audioModeSchema>;
76
100
  export declare const audioConfigSchema: z.ZodObject<{
101
+ mode: z.ZodOptional<z.ZodEnum<{
102
+ default: "default";
103
+ none: "none";
104
+ device: "device";
105
+ }>>;
77
106
  device: z.ZodOptional<z.ZodString>;
78
107
  codec: z.ZodOptional<z.ZodString>;
79
108
  delay_ms: z.ZodOptional<z.ZodNumber>;
@@ -114,10 +143,20 @@ export declare const cerastreamConfigSchema: z.ZodObject<{
114
143
  resolution: z.ZodOptional<z.ZodString>;
115
144
  framerate: z.ZodOptional<z.ZodNumber>;
116
145
  audio: z.ZodOptional<z.ZodObject<{
146
+ mode: z.ZodOptional<z.ZodEnum<{
147
+ default: "default";
148
+ none: "none";
149
+ device: "device";
150
+ }>>;
117
151
  device: z.ZodOptional<z.ZodString>;
118
152
  codec: z.ZodOptional<z.ZodString>;
119
153
  delay_ms: z.ZodOptional<z.ZodNumber>;
120
154
  }, z.core.$strip>>;
155
+ video_passthrough: z.ZodOptional<z.ZodEnum<{
156
+ auto: "auto";
157
+ force: "force";
158
+ off: "off";
159
+ }>>;
121
160
  }, z.core.$strip>;
122
161
  export type CerastreamConfig = z.infer<typeof cerastreamConfigSchema>;
123
162
  export type PartialCerastreamConfig = z.input<typeof cerastreamConfigSchema>;
@@ -159,6 +198,14 @@ export declare const captureDeviceSchema: z.ZodObject<{
159
198
  network: "network";
160
199
  }>>;
161
200
  alsa_card_id: z.ZodOptional<z.ZodString>;
201
+ product_name: z.ZodOptional<z.ZodString>;
202
+ transport: z.ZodOptional<z.ZodEnum<{
203
+ hdmi: "hdmi";
204
+ usb: "usb";
205
+ bluetooth: "bluetooth";
206
+ onboard: "onboard";
207
+ }>>;
208
+ stable_id: z.ZodOptional<z.ZodString>;
162
209
  }, z.core.$strip>;
163
210
  export type CaptureDevice = z.infer<typeof captureDeviceSchema>;
164
211
  export declare const srtStatsSchema: z.ZodObject<{
package/dist/types.js CHANGED
@@ -18,6 +18,16 @@ export const previewTierSchema = z.enum(["webcodecs", "webrtc"]);
18
18
  // platform default. Wire values are the codec id strings the encoder caps also
19
19
  // advertise ("h264"/"h265") — never an encoder element name.
20
20
  export const videoCodecSchema = z.enum(["h264", "h265"]);
21
+ // Same-codec passthrough policy (0.5.0, additive). Absent ⇒ "auto". "auto" =
22
+ // passthrough only when adaptive bitrate is inactive and eligibility holds;
23
+ // "force" = passthrough whenever eligible (typed start failure otherwise);
24
+ // "off" = always transcode. See schema.md and the engine's layer-C matrix.
25
+ export const videoPassthroughSchema = z.enum(["auto", "force", "off"]);
26
+ // Which surface owns the running bitrate, reported additively on a set-bitrate /
27
+ // reload-config response (0.5.0). "encoder" = applied; "source-fixed" = a
28
+ // passthrough graph fixes the bitrate at the source, so the request was NOT
29
+ // applied (paired with reason "passthrough").
30
+ export const bitrateControlSchema = z.enum(["encoder", "source-fixed"]);
21
31
  // Engine-typed capture-device family (0.4.0, additive). CeraUI groups a device
22
32
  // by the engine's resolved kind (a UVC/USB dongle is uvc_h264/mjpeg, never
23
33
  // mislabeled HDMI). Absent ⇒ CeraUI falls back to a bus/name heuristic.
@@ -31,6 +41,17 @@ export const captureDeviceKindSchema = z.enum([
31
41
  "test",
32
42
  "network",
33
43
  ]);
44
+ // How a capture device is physically attached (Todo 20, additive). The transport
45
+ // tag CeraUI renders beside the product name (e.g. "RØDE NT-USB · USB"). Distinct
46
+ // from captureDeviceKind: an HDMI-to-USB dongle is kind:uvc_h264 but transport:usb,
47
+ // while the SoC's dedicated HDMI-RX port is transport:hdmi. Absent on a legacy
48
+ // producer or when no hint identifies a transport.
49
+ export const deviceTransportSchema = z.enum([
50
+ "usb",
51
+ "hdmi",
52
+ "bluetooth",
53
+ "onboard",
54
+ ]);
34
55
  // ---- config sub-schemas (canonical; reused by `start` + the unified config) ----
35
56
  /** SRT transport config. Mirrors schema.md `start.srt` exactly. */
36
57
  export const srtConfigSchema = z.object({
@@ -67,7 +88,15 @@ export const bitrateConfigSchema = z
67
88
  * later, negative = earlier); it is clamped to ±AUDIO_DELAY_MAX_MS when applied,
68
89
  * never rejected — so the schema does NOT bound it.
69
90
  */
91
+ // How the program audio is sourced (0.6.0, additive). "none" = no audio branch
92
+ // (video-only TS); "default" = embedded audio for a network source, the selected
93
+ // device or a silent track for a capture source; "device" = the selected ALSA
94
+ // leg (requires `device`). Absent ⇒ legacy inference from `device`/source kind,
95
+ // so a pre-0.6.0 caller keeps working. Replaces leaking pseudo-source strings
96
+ // (e.g. "No audio") into `device`.
97
+ export const audioModeSchema = z.enum(["none", "default", "device"]);
70
98
  export const audioConfigSchema = z.object({
99
+ mode: audioModeSchema.optional(),
71
100
  device: z.string().optional(), // ALSA capture device id; absent ⇒ test-tone fallback
72
101
  codec: z.string().optional(), // audio encoder codec id (e.g. "aac", "opus")
73
102
  delay_ms: z.number().int().optional(), // signed A/V-sync delay (clamped at apply)
@@ -87,6 +116,7 @@ export const cerastreamConfigSchema = z.object({
87
116
  resolution: z.string().optional(), // additive (0.4.0): "WxH" pixel form (never a UI token)
88
117
  framerate: z.number().optional(), // additive (0.4.0): fps as a number, e.g. 29.97
89
118
  audio: audioConfigSchema.optional(), // additive (0.4.0): audio device/codec/signed delay
119
+ video_passthrough: videoPassthroughSchema.optional(), // additive (0.5.0): auto|force|off; absent ⇒ auto
90
120
  });
91
121
  // convenience defaults for building a config client-side
92
122
  export const DEFAULT_BITRATE_CONFIG = {
@@ -110,6 +140,9 @@ export const captureDeviceSchema = z.object({
110
140
  caps: z.array(captureCapSchema).optional(),
111
141
  kind: captureDeviceKindSchema.optional(), // additive (0.4.0): engine-typed device family; absent on legacy producers
112
142
  alsa_card_id: z.string().optional(), // additive: ALSA card id for media_class:audio devices only; absent on video + legacy producers
143
+ product_name: z.string().optional(), // additive (Todo 20): real product name, deduped with a #N suffix when shared; absent ⇒ use display_name
144
+ transport: deviceTransportSchema.optional(), // additive (Todo 20): how the device is attached; absent on legacy producers
145
+ stable_id: z.string().optional(), // additive (Todo 20): reboot-stable hardware identity, distinct from input_id/device_path
113
146
  });
114
147
  // ---- transport telemetry (srt-stats event payload) ----
115
148
  export const srtStatsSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceralive/cerastream",
3
- "version": "2026.7.1",
3
+ "version": "2026.7.3",
4
4
  "description": "Type-safe TypeScript schema + IPC client for the cerastream streaming engine (JSON-RPC 2.0 / NDJSON over a Unix domain socket).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",