@ceralive/cerastream 2026.7.2 → 2026.7.4

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,12 +6,14 @@ 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 nine 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
13
  - Additive `client.getCapabilities()` discovery for platform, source, encoder, and
14
14
  local preview availability
15
+ - Additive `client.changeConfig()` — reconfigure the live session (resolution,
16
+ framerate, codec, pipeline, source) as one transaction with typed rollback
15
17
 
16
18
  The Zod schemas, types, and constants are the frozen wire contract; `connect()`
17
19
  drives that contract over an NDJSON/UDS transport with no native dependencies.
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 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";
3
+ import { type ChangeConfigParams, type ChangeConfigResult, 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. */
@@ -82,6 +82,20 @@ export interface CerastreamClient {
82
82
  * availability. This request intentionally sits outside the frozen v1 map.
83
83
  */
84
84
  getCapabilities(): Promise<GetCapabilitiesResult>;
85
+ /**
86
+ * Reconfigure the LIVE session as one transaction (resolution, framerate,
87
+ * codec, pipeline, source). Also sits outside the frozen v1 map.
88
+ *
89
+ * Resolves for every phase the transaction reached — including
90
+ * `rollback_failed`, an honest terminal outcome. Callers must branch on
91
+ * `result.phase`, not on whether the promise settled. The engine's declared
92
+ * worst-case transaction bound is 65 000 ms, so size any caller-side deadline
93
+ * from that, not from the default per-request timeout.
94
+ * @param params A delta of the live config; absent field ⇒ keep live value.
95
+ * @throws {CerastreamRpcError} ONLY when the transaction never started (not
96
+ * streaming, a concurrent change, or an empty/invalid delta).
97
+ */
98
+ changeConfig(params: ChangeConfigParams): Promise<ChangeConfigResult>;
85
99
  /**
86
100
  * Subscribe to the live event stream; `handler` fires for each pushed event.
87
101
  * @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 { getCapabilitiesResultSchema, requestSchemas, } from "./messages.js";
7
+ import { changeConfigParamsSchema, changeConfigResultSchema, 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
  }
@@ -81,6 +82,9 @@ class ClientImpl {
81
82
  async getCapabilities() {
82
83
  return getCapabilitiesResultSchema.parse(await this.rawRequest("get-capabilities", undefined));
83
84
  }
85
+ async changeConfig(params) {
86
+ return changeConfigResultSchema.parse(await this.rawRequest("change-config", changeConfigParamsSchema.parse(params)));
87
+ }
84
88
  previewSession(params) {
85
89
  return this.call("preview-session", params);
86
90
  }
@@ -101,7 +105,7 @@ class ClientImpl {
101
105
  this.intentionalClose = true;
102
106
  for (const sub of this.subscriptions)
103
107
  sub.close();
104
- const conn = new CerastreamConnectionError("client closed");
108
+ const conn = new CerastreamConnectionError("client closed", undefined, "closed");
105
109
  this.rejectAllPending(conn);
106
110
  this.socket?.close();
107
111
  this.socket = undefined;
@@ -117,7 +121,7 @@ class ClientImpl {
117
121
  rawRequest(method, params) {
118
122
  const socket = this.socket;
119
123
  if (!socket) {
120
- return Promise.reject(new CerastreamConnectionError("control connection is not open"));
124
+ return Promise.reject(new CerastreamConnectionError("control connection is not open", undefined, "closed"));
121
125
  }
122
126
  const id = this.nextId++;
123
127
  const envelope = {
@@ -139,7 +143,7 @@ class ClientImpl {
139
143
  catch (err) {
140
144
  clearTimeout(timer);
141
145
  this.pending.delete(id);
142
- reject(new CerastreamConnectionError("failed to write request", err));
146
+ reject(new CerastreamConnectionError("failed to write request", err, "lost"));
143
147
  }
144
148
  });
145
149
  }
@@ -206,7 +210,7 @@ class ClientImpl {
206
210
  this.socket = undefined;
207
211
  if (this.intentionalClose)
208
212
  return;
209
- const conn = new CerastreamConnectionError("control connection lost", err);
213
+ const conn = new CerastreamConnectionError("control connection lost", err, "lost");
210
214
  this.rejectAllPending(conn);
211
215
  if (this.autoReconnect)
212
216
  void this.reconnectLoop();
@@ -263,6 +267,20 @@ function safeControlSocketPath() {
263
267
  function isObject(value) {
264
268
  return typeof value === "object" && value !== null;
265
269
  }
270
+ // Classify a connect-time transport failure into a stable machine code. Bun's
271
+ // `connect` collapses several distinct errors onto ENOENT and does not reliably
272
+ // surface ECONNREFUSED for a stale AF_UNIX socket, so an explicit errno is
273
+ // honored when present, then the socket file's presence disambiguates: a path
274
+ // that is gone is `absent` (engine not up yet), a path that still exists but
275
+ // won't accept is `refused` (a crashed/stale listener).
276
+ function classifyConnectError(err, socketPath) {
277
+ const raw = isObject(err) ? err.code : undefined;
278
+ if (raw === "ECONNREFUSED")
279
+ return "refused";
280
+ if (raw === "EACCES" || raw === "EPERM")
281
+ return "unreachable";
282
+ return existsSync(socketPath) ? "refused" : "absent";
283
+ }
266
284
  function sleep(ms) {
267
285
  return new Promise((resolve) => setTimeout(resolve, ms));
268
286
  }
@@ -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.10.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.10.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,16 @@ 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>;
127
+ physical_group_id: z.ZodOptional<z.ZodString>;
128
+ hardware_serial: z.ZodOptional<z.ZodString>;
109
129
  }, z.core.$strip>;
110
130
  }, z.core.$strip>;
111
131
  /** Payload of a {@link deviceEventSchema} event. */
@@ -162,6 +182,62 @@ export declare const previewEventSchema: z.ZodObject<{
162
182
  }, z.core.$strip>;
163
183
  /** Payload of a {@link previewEventSchema} event. */
164
184
  export type PreviewEvent = z.infer<typeof previewEventSchema>;
185
+ /**
186
+ * `audio-level` event — the always-on audio level meter (ADR-0007, additive Todo
187
+ * 21). Either a real per-channel level from the current owner (the idle
188
+ * `sidecar` or the `streaming` audio leg), or `unavailable: true` + `reason` for
189
+ * a handoff gap or a degenerate `audio.mode` — never a fabricated level. A level
190
+ * carries `source`/`channels`/`rms_db`/`peak_db`/`floor_db`; an unavailable event
191
+ * carries `unavailable`/`reason` and omits the level fields. `floor_db` (`-1e6`)
192
+ * is the sentinel a consumer maps back to "silence" (JSON has no `-Infinity`).
193
+ * Independent of the preview-scoped `audio-level` WebSocket frame.
194
+ */
195
+ export declare const audioLevelEventSchema: z.ZodObject<{
196
+ type: z.ZodLiteral<"audio-level">;
197
+ seq: z.ZodNumber;
198
+ source: z.ZodOptional<z.ZodObject<{
199
+ identity: z.ZodOptional<z.ZodString>;
200
+ owner: z.ZodEnum<{
201
+ streaming: "streaming";
202
+ sidecar: "sidecar";
203
+ }>;
204
+ }, z.core.$strip>>;
205
+ channels: z.ZodOptional<z.ZodNumber>;
206
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
207
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
208
+ floor_db: z.ZodOptional<z.ZodNumber>;
209
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
210
+ reason: z.ZodOptional<z.ZodEnum<{
211
+ device_busy: "device_busy";
212
+ no_device: "no_device";
213
+ mode_none: "mode_none";
214
+ handoff: "handoff";
215
+ }>>;
216
+ }, z.core.$strip>;
217
+ /** Payload of a {@link audioLevelEventSchema} event. */
218
+ export type AudioLevelEvent = z.infer<typeof audioLevelEventSchema>;
219
+ /**
220
+ * `config-change` event — one phase of a `change-config` transaction (0.10.0,
221
+ * additive Todo 9). Exactly one `applying` at entry, then exactly ONE terminal
222
+ * phase for the same `attempt_id` (which the RPC result also echoes, so a caller
223
+ * that missed an event can still correlate). A terminal phase is published only
224
+ * after the outcome gate resolved, so `applied` means "PLAYING **and** frames
225
+ * actually advancing", never merely "PLAYING".
226
+ */
227
+ export declare const configChangeEventSchema: z.ZodObject<{
228
+ type: z.ZodLiteral<"config-change">;
229
+ seq: z.ZodNumber;
230
+ attempt_id: z.ZodString;
231
+ phase: z.ZodEnum<{
232
+ applying: "applying";
233
+ applied: "applied";
234
+ reverted: "reverted";
235
+ rollback_failed: "rollback_failed";
236
+ }>;
237
+ reason: z.ZodOptional<z.ZodString>;
238
+ }, z.core.$strip>;
239
+ /** Payload of a {@link configChangeEventSchema} event. */
240
+ export type ConfigChangeEvent = z.infer<typeof configChangeEventSchema>;
165
241
  /** Discriminated union of every v1 event payload (the inner `params`). */
166
242
  export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
167
243
  type: z.ZodLiteral<"status">;
@@ -186,6 +262,11 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
186
262
  active_input: z.ZodOptional<z.ZodString>;
187
263
  decoder: z.ZodOptional<z.ZodString>;
188
264
  input_codec: z.ZodOptional<z.ZodString>;
265
+ passthrough: z.ZodOptional<z.ZodBoolean>;
266
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
267
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
268
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
269
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
189
270
  }, z.core.$strip>>;
190
271
  }, z.core.$strip>, z.ZodObject<{
191
272
  type: z.ZodLiteral<"switch">;
@@ -232,6 +313,16 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
232
313
  network: "network";
233
314
  }>>;
234
315
  alsa_card_id: z.ZodOptional<z.ZodString>;
316
+ product_name: z.ZodOptional<z.ZodString>;
317
+ transport: z.ZodOptional<z.ZodEnum<{
318
+ hdmi: "hdmi";
319
+ usb: "usb";
320
+ bluetooth: "bluetooth";
321
+ onboard: "onboard";
322
+ }>>;
323
+ stable_id: z.ZodOptional<z.ZodString>;
324
+ physical_group_id: z.ZodOptional<z.ZodString>;
325
+ hardware_serial: z.ZodOptional<z.ZodString>;
235
326
  }, z.core.$strip>;
236
327
  }, z.core.$strip>, z.ZodObject<{
237
328
  type: z.ZodLiteral<"bitrate">;
@@ -266,6 +357,38 @@ export declare const eventParamsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
266
357
  seq: z.ZodNumber;
267
358
  session_id: z.ZodString;
268
359
  phase: z.ZodString;
360
+ }, z.core.$strip>, z.ZodObject<{
361
+ type: z.ZodLiteral<"audio-level">;
362
+ seq: z.ZodNumber;
363
+ source: z.ZodOptional<z.ZodObject<{
364
+ identity: z.ZodOptional<z.ZodString>;
365
+ owner: z.ZodEnum<{
366
+ streaming: "streaming";
367
+ sidecar: "sidecar";
368
+ }>;
369
+ }, z.core.$strip>>;
370
+ channels: z.ZodOptional<z.ZodNumber>;
371
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
372
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
373
+ floor_db: z.ZodOptional<z.ZodNumber>;
374
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
375
+ reason: z.ZodOptional<z.ZodEnum<{
376
+ device_busy: "device_busy";
377
+ no_device: "no_device";
378
+ mode_none: "mode_none";
379
+ handoff: "handoff";
380
+ }>>;
381
+ }, z.core.$strip>, z.ZodObject<{
382
+ type: z.ZodLiteral<"config-change">;
383
+ seq: z.ZodNumber;
384
+ attempt_id: z.ZodString;
385
+ phase: z.ZodEnum<{
386
+ applying: "applying";
387
+ applied: "applied";
388
+ reverted: "reverted";
389
+ rollback_failed: "rollback_failed";
390
+ }>;
391
+ reason: z.ZodOptional<z.ZodString>;
269
392
  }, z.core.$strip>], "type">;
270
393
  export type EventParams = z.infer<typeof eventParamsSchema>;
271
394
  /** Full event envelope: { jsonrpc, method:"event", params } with typed params. */
@@ -295,6 +418,11 @@ export declare const cerastreamEventSchema: z.ZodObject<{
295
418
  active_input: z.ZodOptional<z.ZodString>;
296
419
  decoder: z.ZodOptional<z.ZodString>;
297
420
  input_codec: z.ZodOptional<z.ZodString>;
421
+ passthrough: z.ZodOptional<z.ZodBoolean>;
422
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
423
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
424
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
425
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
298
426
  }, z.core.$strip>>;
299
427
  }, z.core.$strip>, z.ZodObject<{
300
428
  type: z.ZodLiteral<"switch">;
@@ -341,6 +469,16 @@ export declare const cerastreamEventSchema: z.ZodObject<{
341
469
  network: "network";
342
470
  }>>;
343
471
  alsa_card_id: z.ZodOptional<z.ZodString>;
472
+ product_name: z.ZodOptional<z.ZodString>;
473
+ transport: z.ZodOptional<z.ZodEnum<{
474
+ hdmi: "hdmi";
475
+ usb: "usb";
476
+ bluetooth: "bluetooth";
477
+ onboard: "onboard";
478
+ }>>;
479
+ stable_id: z.ZodOptional<z.ZodString>;
480
+ physical_group_id: z.ZodOptional<z.ZodString>;
481
+ hardware_serial: z.ZodOptional<z.ZodString>;
344
482
  }, z.core.$strip>;
345
483
  }, z.core.$strip>, z.ZodObject<{
346
484
  type: z.ZodLiteral<"bitrate">;
@@ -375,11 +513,45 @@ export declare const cerastreamEventSchema: z.ZodObject<{
375
513
  seq: z.ZodNumber;
376
514
  session_id: z.ZodString;
377
515
  phase: z.ZodString;
516
+ }, z.core.$strip>, z.ZodObject<{
517
+ type: z.ZodLiteral<"audio-level">;
518
+ seq: z.ZodNumber;
519
+ source: z.ZodOptional<z.ZodObject<{
520
+ identity: z.ZodOptional<z.ZodString>;
521
+ owner: z.ZodEnum<{
522
+ streaming: "streaming";
523
+ sidecar: "sidecar";
524
+ }>;
525
+ }, z.core.$strip>>;
526
+ channels: z.ZodOptional<z.ZodNumber>;
527
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
528
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
529
+ floor_db: z.ZodOptional<z.ZodNumber>;
530
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
531
+ reason: z.ZodOptional<z.ZodEnum<{
532
+ device_busy: "device_busy";
533
+ no_device: "no_device";
534
+ mode_none: "mode_none";
535
+ handoff: "handoff";
536
+ }>>;
537
+ }, z.core.$strip>, z.ZodObject<{
538
+ type: z.ZodLiteral<"config-change">;
539
+ seq: z.ZodNumber;
540
+ attempt_id: z.ZodString;
541
+ phase: z.ZodEnum<{
542
+ applying: "applying";
543
+ applied: "applied";
544
+ reverted: "reverted";
545
+ rollback_failed: "rollback_failed";
546
+ }>;
547
+ reason: z.ZodOptional<z.ZodString>;
378
548
  }, z.core.$strip>], "type">;
379
549
  }, z.core.$strip>;
380
550
  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"];
551
+ /** The nine event topics (schema.md "Events" table) — count-assertion source.
552
+ * `audio-level` (Todo 21) and `config-change` (Todo 9) are additive and appended
553
+ * last, preserving the order of every prior topic. */
554
+ export declare const EVENT_TOPICS: readonly ["status", "switch", "device", "bitrate", "srt-stats", "error", "preview", "audio-level", "config-change"];
383
555
  export type EventTopicName = (typeof EVENT_TOPICS)[number];
384
556
  /** topic → payload Zod schema. Tests assert every topic is represented here. */
385
557
  export declare const eventSchemas: {
@@ -406,6 +578,11 @@ export declare const eventSchemas: {
406
578
  active_input: z.ZodOptional<z.ZodString>;
407
579
  decoder: z.ZodOptional<z.ZodString>;
408
580
  input_codec: z.ZodOptional<z.ZodString>;
581
+ passthrough: z.ZodOptional<z.ZodBoolean>;
582
+ gop_probe_outcome: z.ZodOptional<z.ZodString>;
583
+ gop_probe_ms: z.ZodOptional<z.ZodNumber>;
584
+ frames_emitted: z.ZodOptional<z.ZodNumber>;
585
+ pipeline_playing: z.ZodOptional<z.ZodBoolean>;
409
586
  }, z.core.$strip>>;
410
587
  }, z.core.$strip>;
411
588
  readonly switch: z.ZodObject<{
@@ -454,6 +631,16 @@ export declare const eventSchemas: {
454
631
  network: "network";
455
632
  }>>;
456
633
  alsa_card_id: z.ZodOptional<z.ZodString>;
634
+ product_name: z.ZodOptional<z.ZodString>;
635
+ transport: z.ZodOptional<z.ZodEnum<{
636
+ hdmi: "hdmi";
637
+ usb: "usb";
638
+ bluetooth: "bluetooth";
639
+ onboard: "onboard";
640
+ }>>;
641
+ stable_id: z.ZodOptional<z.ZodString>;
642
+ physical_group_id: z.ZodOptional<z.ZodString>;
643
+ hardware_serial: z.ZodOptional<z.ZodString>;
457
644
  }, z.core.$strip>;
458
645
  }, z.core.$strip>;
459
646
  readonly bitrate: z.ZodObject<{
@@ -493,4 +680,38 @@ export declare const eventSchemas: {
493
680
  session_id: z.ZodString;
494
681
  phase: z.ZodString;
495
682
  }, z.core.$strip>;
683
+ readonly "audio-level": z.ZodObject<{
684
+ type: z.ZodLiteral<"audio-level">;
685
+ seq: z.ZodNumber;
686
+ source: z.ZodOptional<z.ZodObject<{
687
+ identity: z.ZodOptional<z.ZodString>;
688
+ owner: z.ZodEnum<{
689
+ streaming: "streaming";
690
+ sidecar: "sidecar";
691
+ }>;
692
+ }, z.core.$strip>>;
693
+ channels: z.ZodOptional<z.ZodNumber>;
694
+ rms_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
695
+ peak_db: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
696
+ floor_db: z.ZodOptional<z.ZodNumber>;
697
+ unavailable: z.ZodOptional<z.ZodLiteral<true>>;
698
+ reason: z.ZodOptional<z.ZodEnum<{
699
+ device_busy: "device_busy";
700
+ no_device: "no_device";
701
+ mode_none: "mode_none";
702
+ handoff: "handoff";
703
+ }>>;
704
+ }, z.core.$strip>;
705
+ readonly "config-change": z.ZodObject<{
706
+ type: z.ZodLiteral<"config-change">;
707
+ seq: z.ZodNumber;
708
+ attempt_id: z.ZodString;
709
+ phase: z.ZodEnum<{
710
+ applying: "applying";
711
+ applied: "applied";
712
+ reverted: "reverted";
713
+ rollback_failed: "rollback_failed";
714
+ }>;
715
+ reason: z.ZodOptional<z.ZodString>;
716
+ }, z.core.$strip>;
496
717
  };
package/dist/events.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { processErrorCodeSchema, processErrorSourceSchema, } from "./errors.js";
3
- import { captureDeviceSchema, inputModeSchema, mediaClassSchema, streamStateSchema, } from "./types.js";
3
+ import { captureDeviceSchema, configChangePhaseSchema, inputModeSchema, mediaClassSchema, streamStateSchema, } from "./types.js";
4
4
  // Server → client `event` notifications, delivered after `subscribe-events`
5
5
  // (schema.md "Events"). Each event is the inner `params` of an rpcEventSchema:
6
6
  // { type, seq, … }. `type` discriminates; `seq` is a per-type monotonic counter.
@@ -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,47 @@ 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
+ });
130
+ /**
131
+ * `config-change` event — one phase of a `change-config` transaction (0.10.0,
132
+ * additive Todo 9). Exactly one `applying` at entry, then exactly ONE terminal
133
+ * phase for the same `attempt_id` (which the RPC result also echoes, so a caller
134
+ * that missed an event can still correlate). A terminal phase is published only
135
+ * after the outcome gate resolved, so `applied` means "PLAYING **and** frames
136
+ * actually advancing", never merely "PLAYING".
137
+ */
138
+ export const configChangeEventSchema = z.object({
139
+ type: z.literal("config-change"),
140
+ seq,
141
+ attempt_id: z.string(),
142
+ phase: configChangePhaseSchema,
143
+ reason: z.string().optional(), // absent on applying/applied; "teardown_timeout" marks the supervisor escalation
144
+ });
99
145
  /** Discriminated union of every v1 event payload (the inner `params`). */
100
146
  export const eventParamsSchema = z.discriminatedUnion("type", [
101
147
  statusEventSchema,
@@ -105,6 +151,8 @@ export const eventParamsSchema = z.discriminatedUnion("type", [
105
151
  srtStatsEventSchema,
106
152
  runtimeErrorEventSchema,
107
153
  previewEventSchema,
154
+ audioLevelEventSchema,
155
+ configChangeEventSchema,
108
156
  ]);
109
157
  /** Full event envelope: { jsonrpc, method:"event", params } with typed params. */
110
158
  export const cerastreamEventSchema = z.object({
@@ -112,7 +160,9 @@ export const cerastreamEventSchema = z.object({
112
160
  method: z.literal("event"),
113
161
  params: eventParamsSchema,
114
162
  });
115
- /** The seven event topics (schema.md "Events" table) — count-assertion source. */
163
+ /** The nine event topics (schema.md "Events" table) — count-assertion source.
164
+ * `audio-level` (Todo 21) and `config-change` (Todo 9) are additive and appended
165
+ * last, preserving the order of every prior topic. */
116
166
  export const EVENT_TOPICS = [
117
167
  "status",
118
168
  "switch",
@@ -121,6 +171,8 @@ export const EVENT_TOPICS = [
121
171
  "srt-stats",
122
172
  "error",
123
173
  "preview",
174
+ "audio-level",
175
+ "config-change",
124
176
  ];
125
177
  /** topic → payload Zod schema. Tests assert every topic is represented here. */
126
178
  export const eventSchemas = {
@@ -131,4 +183,6 @@ export const eventSchemas = {
131
183
  "srt-stats": srtStatsEventSchema,
132
184
  error: runtimeErrorEventSchema,
133
185
  preview: previewEventSchema,
186
+ "audio-level": audioLevelEventSchema,
187
+ "config-change": configChangeEventSchema,
134
188
  };
@@ -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<{
@@ -73,6 +83,7 @@ export declare const reloadConfigParamsSchema: z.ZodObject<{
73
83
  audio: z.ZodOptional<z.ZodObject<{
74
84
  delay_ms: z.ZodOptional<z.ZodNumber>;
75
85
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
86
+ meter_device: z.ZodOptional<z.ZodNullable<z.ZodString>>;
76
87
  }, z.core.$strip>>;
77
88
  }, z.core.$strip>;
78
89
  export type ReloadConfigParams = z.infer<typeof reloadConfigParamsSchema>;
@@ -93,8 +104,14 @@ export declare const reloadConfigResultSchema: z.ZodObject<{
93
104
  audio: z.ZodOptional<z.ZodObject<{
94
105
  delay_ms: z.ZodOptional<z.ZodNumber>;
95
106
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
107
+ meter_device: z.ZodOptional<z.ZodNullable<z.ZodString>>;
96
108
  }, z.core.$strip>>;
97
109
  }, z.core.$strip>;
110
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
111
+ encoder: "encoder";
112
+ "source-fixed": "source-fixed";
113
+ }>>;
114
+ reason: z.ZodOptional<z.ZodString>;
98
115
  }, z.core.$strip>;
99
116
  export type ReloadConfigResult = z.infer<typeof reloadConfigResultSchema>;
100
117
  export declare const setBitrateParamsSchema: z.ZodObject<{
@@ -105,6 +122,11 @@ export declare const setBitrateResultSchema: z.ZodObject<{
105
122
  applied: z.ZodObject<{
106
123
  max_bitrate: z.ZodNumber;
107
124
  }, z.core.$strip>;
125
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
126
+ encoder: "encoder";
127
+ "source-fixed": "source-fixed";
128
+ }>>;
129
+ reason: z.ZodOptional<z.ZodString>;
108
130
  }, z.core.$strip>;
109
131
  export type SetBitrateResult = z.infer<typeof setBitrateResultSchema>;
110
132
  export declare const switchInputParamsSchema: z.ZodObject<{
@@ -172,6 +194,16 @@ export declare const listDevicesResultSchema: z.ZodObject<{
172
194
  network: "network";
173
195
  }>>;
174
196
  alsa_card_id: z.ZodOptional<z.ZodString>;
197
+ product_name: z.ZodOptional<z.ZodString>;
198
+ transport: z.ZodOptional<z.ZodEnum<{
199
+ hdmi: "hdmi";
200
+ usb: "usb";
201
+ bluetooth: "bluetooth";
202
+ onboard: "onboard";
203
+ }>>;
204
+ stable_id: z.ZodOptional<z.ZodString>;
205
+ physical_group_id: z.ZodOptional<z.ZodString>;
206
+ hardware_serial: z.ZodOptional<z.ZodString>;
175
207
  }, z.core.$strip>>;
176
208
  }, z.core.$strip>;
177
209
  export type ListDevicesResult = z.infer<typeof listDevicesResultSchema>;
@@ -183,6 +215,8 @@ export declare const eventTopicSchema: z.ZodEnum<{
183
215
  switch: "switch";
184
216
  "srt-stats": "srt-stats";
185
217
  preview: "preview";
218
+ "audio-level": "audio-level";
219
+ "config-change": "config-change";
186
220
  }>;
187
221
  export type EventTopic = z.infer<typeof eventTopicSchema>;
188
222
  export declare const subscribeEventsParamsSchema: z.ZodObject<{
@@ -194,6 +228,8 @@ export declare const subscribeEventsParamsSchema: z.ZodObject<{
194
228
  switch: "switch";
195
229
  "srt-stats": "srt-stats";
196
230
  preview: "preview";
231
+ "audio-level": "audio-level";
232
+ "config-change": "config-change";
197
233
  }>>>;
198
234
  }, z.core.$strip>;
199
235
  export type SubscribeEventsParams = z.infer<typeof subscribeEventsParamsSchema>;
@@ -206,6 +242,8 @@ export declare const subscribeEventsResultSchema: z.ZodObject<{
206
242
  switch: "switch";
207
243
  "srt-stats": "srt-stats";
208
244
  preview: "preview";
245
+ "audio-level": "audio-level";
246
+ "config-change": "config-change";
209
247
  }>>;
210
248
  }, z.core.$strip>;
211
249
  export type SubscribeEventsResult = z.infer<typeof subscribeEventsResultSchema>;
@@ -274,6 +312,16 @@ export declare const platformCapsSchema: z.ZodObject<{
274
312
  supports_h265: z.ZodBoolean;
275
313
  hardware_accelerated: z.ZodBoolean;
276
314
  max_resolution: z.ZodString;
315
+ hardware_kind: z.ZodOptional<z.ZodEnum<{
316
+ rk3588: "rk3588";
317
+ jetson: "jetson";
318
+ n100: "n100";
319
+ generic: "generic";
320
+ }>>;
321
+ source: z.ZodOptional<z.ZodEnum<{
322
+ detected: "detected";
323
+ override: "override";
324
+ }>>;
277
325
  }, z.core.$strip>;
278
326
  export type PlatformCaps = z.infer<typeof platformCapsSchema>;
279
327
  export declare const previewAvailabilitySchema: z.ZodObject<{
@@ -287,6 +335,16 @@ export declare const getCapabilitiesResultSchema: z.ZodObject<{
287
335
  supports_h265: z.ZodBoolean;
288
336
  hardware_accelerated: z.ZodBoolean;
289
337
  max_resolution: z.ZodString;
338
+ hardware_kind: z.ZodOptional<z.ZodEnum<{
339
+ rk3588: "rk3588";
340
+ jetson: "jetson";
341
+ n100: "n100";
342
+ generic: "generic";
343
+ }>>;
344
+ source: z.ZodOptional<z.ZodEnum<{
345
+ detected: "detected";
346
+ override: "override";
347
+ }>>;
290
348
  }, z.core.$strip>;
291
349
  encoder: z.ZodObject<{
292
350
  codecs: z.ZodArray<z.ZodString>;
@@ -319,8 +377,45 @@ export declare const getCapabilitiesResultSchema: z.ZodObject<{
319
377
  bound: z.ZodBoolean;
320
378
  }, z.core.$strip>>;
321
379
  network_embedded_audio: z.ZodOptional<z.ZodBoolean>;
380
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
322
381
  }, z.core.$strip>;
323
382
  export type GetCapabilitiesResult = z.infer<typeof getCapabilitiesResultSchema>;
383
+ export declare const changeConfigParamsSchema: z.ZodObject<{
384
+ pipeline: z.ZodOptional<z.ZodString>;
385
+ resolution: z.ZodOptional<z.ZodString>;
386
+ framerate: z.ZodOptional<z.ZodNumber>;
387
+ codec: z.ZodOptional<z.ZodEnum<{
388
+ h264: "h264";
389
+ h265: "h265";
390
+ }>>;
391
+ input_id: z.ZodOptional<z.ZodString>;
392
+ }, z.core.$strip>;
393
+ export type ChangeConfigParams = z.infer<typeof changeConfigParamsSchema>;
394
+ export declare const changeConfigResultSchema: z.ZodObject<{
395
+ attempt_id: z.ZodString;
396
+ phase: z.ZodEnum<{
397
+ applying: "applying";
398
+ applied: "applied";
399
+ reverted: "reverted";
400
+ rollback_failed: "rollback_failed";
401
+ }>;
402
+ state: z.ZodEnum<{
403
+ idle: "idle";
404
+ starting: "starting";
405
+ streaming: "streaming";
406
+ stopping: "stopping";
407
+ }>;
408
+ reason: z.ZodOptional<z.ZodString>;
409
+ }, z.core.$strip>;
410
+ export type ChangeConfigResult = z.infer<typeof changeConfigResultSchema>;
411
+ /**
412
+ * The `reason` a `rollback_failed` carries when a teardown deadline overran.
413
+ * A TERMINAL supervisor escalation, not an ordinary failure: the engine could not
414
+ * prove the old session released its capture devices within the bound, so it
415
+ * refuses to build a second session that would race it. Mirrors the Rust
416
+ * `REASON_TEARDOWN_TIMEOUT`; consumers render it distinctly.
417
+ */
418
+ export declare const REASON_TEARDOWN_TIMEOUT: "teardown_timeout";
324
419
  /** The eight v1 control methods (the literal JSON-RPC `method` strings). */
325
420
  export declare const V1_METHODS: readonly ["start", "stop", "reload-config", "set-bitrate", "switch-input", "list-devices", "subscribe-events", "preview-session"];
326
421
  export type V1Method = (typeof V1_METHODS)[number];
@@ -371,10 +466,20 @@ export declare const requestSchemas: {
371
466
  resolution: z.ZodOptional<z.ZodString>;
372
467
  framerate: z.ZodOptional<z.ZodNumber>;
373
468
  audio: z.ZodOptional<z.ZodObject<{
469
+ mode: z.ZodOptional<z.ZodEnum<{
470
+ default: "default";
471
+ none: "none";
472
+ device: "device";
473
+ }>>;
374
474
  device: z.ZodOptional<z.ZodString>;
375
475
  codec: z.ZodOptional<z.ZodString>;
376
476
  delay_ms: z.ZodOptional<z.ZodNumber>;
377
477
  }, z.core.$strip>>;
478
+ video_passthrough: z.ZodOptional<z.ZodEnum<{
479
+ auto: "auto";
480
+ force: "force";
481
+ off: "off";
482
+ }>>;
378
483
  }, z.core.$strip>;
379
484
  readonly result: z.ZodObject<{
380
485
  session_id: z.ZodString;
@@ -416,6 +521,7 @@ export declare const requestSchemas: {
416
521
  audio: z.ZodOptional<z.ZodObject<{
417
522
  delay_ms: z.ZodOptional<z.ZodNumber>;
418
523
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
524
+ meter_device: z.ZodOptional<z.ZodNullable<z.ZodString>>;
419
525
  }, z.core.$strip>>;
420
526
  }, z.core.$strip>;
421
527
  readonly result: z.ZodObject<{
@@ -435,8 +541,14 @@ export declare const requestSchemas: {
435
541
  audio: z.ZodOptional<z.ZodObject<{
436
542
  delay_ms: z.ZodOptional<z.ZodNumber>;
437
543
  delay_ms_signed: z.ZodOptional<z.ZodNumber>;
544
+ meter_device: z.ZodOptional<z.ZodNullable<z.ZodString>>;
438
545
  }, z.core.$strip>>;
439
546
  }, z.core.$strip>;
547
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
548
+ encoder: "encoder";
549
+ "source-fixed": "source-fixed";
550
+ }>>;
551
+ reason: z.ZodOptional<z.ZodString>;
440
552
  }, z.core.$strip>;
441
553
  };
442
554
  readonly "set-bitrate": {
@@ -447,6 +559,11 @@ export declare const requestSchemas: {
447
559
  applied: z.ZodObject<{
448
560
  max_bitrate: z.ZodNumber;
449
561
  }, z.core.$strip>;
562
+ bitrate_control: z.ZodOptional<z.ZodEnum<{
563
+ encoder: "encoder";
564
+ "source-fixed": "source-fixed";
565
+ }>>;
566
+ reason: z.ZodOptional<z.ZodString>;
450
567
  }, z.core.$strip>;
451
568
  };
452
569
  readonly "switch-input": {
@@ -498,6 +615,16 @@ export declare const requestSchemas: {
498
615
  network: "network";
499
616
  }>>;
500
617
  alsa_card_id: z.ZodOptional<z.ZodString>;
618
+ product_name: z.ZodOptional<z.ZodString>;
619
+ transport: z.ZodOptional<z.ZodEnum<{
620
+ hdmi: "hdmi";
621
+ usb: "usb";
622
+ bluetooth: "bluetooth";
623
+ onboard: "onboard";
624
+ }>>;
625
+ stable_id: z.ZodOptional<z.ZodString>;
626
+ physical_group_id: z.ZodOptional<z.ZodString>;
627
+ hardware_serial: z.ZodOptional<z.ZodString>;
501
628
  }, z.core.$strip>>;
502
629
  }, z.core.$strip>;
503
630
  };
@@ -511,6 +638,8 @@ export declare const requestSchemas: {
511
638
  switch: "switch";
512
639
  "srt-stats": "srt-stats";
513
640
  preview: "preview";
641
+ "audio-level": "audio-level";
642
+ "config-change": "config-change";
514
643
  }>>>;
515
644
  }, z.core.$strip>;
516
645
  readonly result: z.ZodObject<{
@@ -522,6 +651,8 @@ export declare const requestSchemas: {
522
651
  switch: "switch";
523
652
  "srt-stats": "srt-stats";
524
653
  preview: "preview";
654
+ "audio-level": "audio-level";
655
+ "config-change": "config-change";
525
656
  }>>;
526
657
  }, z.core.$strip>;
527
658
  };
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, configChangePhaseSchema, inputModeSchema, mediaClassSchema, previewTierSchema, streamStateSchema, videoCodecSchema, } 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 ----
@@ -39,11 +39,19 @@ export const reloadConfigParamsSchema = z.object({
39
39
  .object({
40
40
  delay_ms: z.number().int().min(0).max(AUDIO_DELAY_MAX_MS).optional(), // legacy unsigned; kept for 0.3.0 callers
41
41
  delay_ms_signed: z.number().int().optional(), // signed sibling; clamped at apply, so unbounded
42
+ // additive (0.9.0): idle-meter card preference — absent leaves it unchanged,
43
+ // `null` restores the engine's auto-pick, `hw:CARD=…` prefers that card
44
+ // (a preference, not a pin: a card that never delivers is still demoted)
45
+ meter_device: z.string().nullable().optional(),
42
46
  })
43
47
  .optional(),
44
48
  });
45
49
  export const reloadConfigResultSchema = z.object({
46
50
  applied: reloadConfigParamsSchema, // post-clamp values actually applied
51
+ // additive (0.5.0): a bitrate-bearing reload during passthrough is NOT applied —
52
+ // "source-fixed" + reason "passthrough" report that. Absent on a transcode reload.
53
+ bitrate_control: bitrateControlSchema.optional(),
54
+ reason: z.string().optional(),
47
55
  });
48
56
  // ---- 4. set-bitrate ----
49
57
  export const setBitrateParamsSchema = z.object({
@@ -51,6 +59,10 @@ export const setBitrateParamsSchema = z.object({
51
59
  });
52
60
  export const setBitrateResultSchema = z.object({
53
61
  applied: z.object({ max_bitrate: z.number().int() }),
62
+ // additive (0.5.0). On passthrough there is no encoder to drive, so a request is
63
+ // NOT applied: "source-fixed" with reason "passthrough". Absent/"encoder" ⇒ applied.
64
+ bitrate_control: bitrateControlSchema.optional(),
65
+ reason: z.string().optional(),
54
66
  });
55
67
  // ---- 5. switch-input ----
56
68
  export const switchInputParamsSchema = z.object({
@@ -91,6 +103,8 @@ export const eventTopicSchema = z.enum([
91
103
  "srt-stats",
92
104
  "error",
93
105
  "preview",
106
+ "audio-level",
107
+ "config-change",
94
108
  ]);
95
109
  export const subscribeEventsParamsSchema = z.object({
96
110
  topics: z.array(eventTopicSchema).optional(), // default: all topics
@@ -154,6 +168,10 @@ export const platformCapsSchema = z.object({
154
168
  supports_h265: z.boolean(),
155
169
  hardware_accelerated: z.boolean(),
156
170
  max_resolution: z.string(),
171
+ // additive: the engine's resolved hardware kind and how it was resolved.
172
+ // Optional so a pre-field engine (which omits both) still parses.
173
+ hardware_kind: z.enum(["rk3588", "jetson", "n100", "generic"]).optional(),
174
+ source: z.enum(["detected", "override"]).optional(),
157
175
  });
158
176
  // Preview-server availability (0.4.0, additive). Lets the UI tell an unbound /
159
177
  // port-conflicted preview (enabled:true, bound:false) from a down engine.
@@ -173,7 +191,44 @@ export const getCapabilitiesResultSchema = z.object({
173
191
  profile_catalog_version: z.string().optional(), // semver of the supported_profiles catalog
174
192
  preview: previewAvailabilitySchema.optional(), // preview-server availability (unbound vs down)
175
193
  network_embedded_audio: z.boolean().optional(), // engine routes network-ingest embedded audio to the mux
194
+ features: z.array(z.string()).optional(), // named engine features (e.g. "video-passthrough") for fail-closed negotiation
195
+ });
196
+ // ---- 10. change-config (0.10.0, additive) ----
197
+ // Deliberately absent from V1_METHODS / requestSchemas, like get-capabilities and
198
+ // switch-audio, so the frozen eight-method contract count stays eight. Params are
199
+ // a DELTA of startParamsSchema: absent ⇒ keep the live value. An empty delta is
200
+ // refused with `cerastream.params.invalid` — a no-op that restarted the pipeline
201
+ // would be the worst possible outcome. `input_id` rides the transaction, so a
202
+ // source change renegotiates caps instead of going through `switch-input`.
203
+ export const changeConfigParamsSchema = z
204
+ .object({
205
+ pipeline: z.string().optional(),
206
+ resolution: z.string().optional(), // "WxH" pixels, e.g. "3840x2160"
207
+ framerate: z.number().optional(),
208
+ codec: videoCodecSchema.optional(),
209
+ input_id: z.string().optional(),
210
+ })
211
+ .refine((delta) => Object.keys(delta).length > 0, {
212
+ message: "change-config delta must carry at least one field",
213
+ });
214
+ // `Ok` for every phase the transaction actually reached — INCLUDING
215
+ // `rollback_failed`, an honest terminal outcome rather than an RPC fault. A
216
+ // Tier-1 error means the transaction never started, so a caller can tell
217
+ // "nothing happened" from "something happened and here is what".
218
+ export const changeConfigResultSchema = z.object({
219
+ attempt_id: z.string(), // correlates with every config-change event of the attempt
220
+ phase: configChangePhaseSchema, // the TERMINAL phase reached
221
+ state: streamStateSchema, // streaming for applied/reverted, idle for rollback_failed
222
+ reason: z.string().optional(), // machine-stable cause on a non-applied phase
176
223
  });
224
+ /**
225
+ * The `reason` a `rollback_failed` carries when a teardown deadline overran.
226
+ * A TERMINAL supervisor escalation, not an ordinary failure: the engine could not
227
+ * prove the old session released its capture devices within the bound, so it
228
+ * refuses to build a second session that would race it. Mirrors the Rust
229
+ * `REASON_TEARDOWN_TIMEOUT`; consumers render it distinctly.
230
+ */
231
+ export const REASON_TEARDOWN_TIMEOUT = "teardown_timeout";
177
232
  // ---- method registry (count-assertion source of truth) ----
178
233
  /** The eight v1 control methods (the literal JSON-RPC `method` strings). */
179
234
  export const V1_METHODS = [
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,20 @@ 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>;
64
+ export declare const configChangePhaseSchema: z.ZodEnum<{
65
+ applying: "applying";
66
+ applied: "applied";
67
+ reverted: "reverted";
68
+ rollback_failed: "rollback_failed";
69
+ }>;
70
+ export type ConfigChangePhase = z.infer<typeof configChangePhaseSchema>;
46
71
  /** SRT transport config. Mirrors schema.md `start.srt` exactly. */
47
72
  export declare const srtConfigSchema: z.ZodObject<{
48
73
  host: z.ZodString;
@@ -73,7 +98,18 @@ export type BitrateConfig = z.infer<typeof bitrateConfigSchema>;
73
98
  * later, negative = earlier); it is clamped to ±AUDIO_DELAY_MAX_MS when applied,
74
99
  * never rejected — so the schema does NOT bound it.
75
100
  */
101
+ export declare const audioModeSchema: z.ZodEnum<{
102
+ default: "default";
103
+ none: "none";
104
+ device: "device";
105
+ }>;
106
+ export type AudioMode = z.infer<typeof audioModeSchema>;
76
107
  export declare const audioConfigSchema: z.ZodObject<{
108
+ mode: z.ZodOptional<z.ZodEnum<{
109
+ default: "default";
110
+ none: "none";
111
+ device: "device";
112
+ }>>;
77
113
  device: z.ZodOptional<z.ZodString>;
78
114
  codec: z.ZodOptional<z.ZodString>;
79
115
  delay_ms: z.ZodOptional<z.ZodNumber>;
@@ -114,10 +150,20 @@ export declare const cerastreamConfigSchema: z.ZodObject<{
114
150
  resolution: z.ZodOptional<z.ZodString>;
115
151
  framerate: z.ZodOptional<z.ZodNumber>;
116
152
  audio: z.ZodOptional<z.ZodObject<{
153
+ mode: z.ZodOptional<z.ZodEnum<{
154
+ default: "default";
155
+ none: "none";
156
+ device: "device";
157
+ }>>;
117
158
  device: z.ZodOptional<z.ZodString>;
118
159
  codec: z.ZodOptional<z.ZodString>;
119
160
  delay_ms: z.ZodOptional<z.ZodNumber>;
120
161
  }, z.core.$strip>>;
162
+ video_passthrough: z.ZodOptional<z.ZodEnum<{
163
+ auto: "auto";
164
+ force: "force";
165
+ off: "off";
166
+ }>>;
121
167
  }, z.core.$strip>;
122
168
  export type CerastreamConfig = z.infer<typeof cerastreamConfigSchema>;
123
169
  export type PartialCerastreamConfig = z.input<typeof cerastreamConfigSchema>;
@@ -159,6 +205,16 @@ export declare const captureDeviceSchema: z.ZodObject<{
159
205
  network: "network";
160
206
  }>>;
161
207
  alsa_card_id: z.ZodOptional<z.ZodString>;
208
+ product_name: z.ZodOptional<z.ZodString>;
209
+ transport: z.ZodOptional<z.ZodEnum<{
210
+ hdmi: "hdmi";
211
+ usb: "usb";
212
+ bluetooth: "bluetooth";
213
+ onboard: "onboard";
214
+ }>>;
215
+ stable_id: z.ZodOptional<z.ZodString>;
216
+ physical_group_id: z.ZodOptional<z.ZodString>;
217
+ hardware_serial: z.ZodOptional<z.ZodString>;
162
218
  }, z.core.$strip>;
163
219
  export type CaptureDevice = z.infer<typeof captureDeviceSchema>;
164
220
  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,29 @@ 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
+ ]);
55
+ // Phase of one `change-config` transaction (0.10.0, additive). `applying` is
56
+ // published once at entry, then exactly ONE terminal phase for the same
57
+ // attempt_id: `applied` (the new config satisfied the outcome gate),
58
+ // `reverted` (it did not and the single known-good rollback attempt did), or
59
+ // `rollback_failed` (no rollback was possible, or the one attempt also failed —
60
+ // the engine is idle, which is the truthful terminal state).
61
+ export const configChangePhaseSchema = z.enum([
62
+ "applying",
63
+ "applied",
64
+ "reverted",
65
+ "rollback_failed",
66
+ ]);
34
67
  // ---- config sub-schemas (canonical; reused by `start` + the unified config) ----
35
68
  /** SRT transport config. Mirrors schema.md `start.srt` exactly. */
36
69
  export const srtConfigSchema = z.object({
@@ -67,7 +100,15 @@ export const bitrateConfigSchema = z
67
100
  * later, negative = earlier); it is clamped to ±AUDIO_DELAY_MAX_MS when applied,
68
101
  * never rejected — so the schema does NOT bound it.
69
102
  */
103
+ // How the program audio is sourced (0.6.0, additive). "none" = no audio branch
104
+ // (video-only TS); "default" = embedded audio for a network source, the selected
105
+ // device or a silent track for a capture source; "device" = the selected ALSA
106
+ // leg (requires `device`). Absent ⇒ legacy inference from `device`/source kind,
107
+ // so a pre-0.6.0 caller keeps working. Replaces leaking pseudo-source strings
108
+ // (e.g. "No audio") into `device`.
109
+ export const audioModeSchema = z.enum(["none", "default", "device"]);
70
110
  export const audioConfigSchema = z.object({
111
+ mode: audioModeSchema.optional(),
71
112
  device: z.string().optional(), // ALSA capture device id; absent ⇒ test-tone fallback
72
113
  codec: z.string().optional(), // audio encoder codec id (e.g. "aac", "opus")
73
114
  delay_ms: z.number().int().optional(), // signed A/V-sync delay (clamped at apply)
@@ -87,6 +128,7 @@ export const cerastreamConfigSchema = z.object({
87
128
  resolution: z.string().optional(), // additive (0.4.0): "WxH" pixel form (never a UI token)
88
129
  framerate: z.number().optional(), // additive (0.4.0): fps as a number, e.g. 29.97
89
130
  audio: audioConfigSchema.optional(), // additive (0.4.0): audio device/codec/signed delay
131
+ video_passthrough: videoPassthroughSchema.optional(), // additive (0.5.0): auto|force|off; absent ⇒ auto
90
132
  });
91
133
  // convenience defaults for building a config client-side
92
134
  export const DEFAULT_BITRATE_CONFIG = {
@@ -110,6 +152,11 @@ export const captureDeviceSchema = z.object({
110
152
  caps: z.array(captureCapSchema).optional(),
111
153
  kind: captureDeviceKindSchema.optional(), // additive (0.4.0): engine-typed device family; absent on legacy producers
112
154
  alsa_card_id: z.string().optional(), // additive: ALSA card id for media_class:audio devices only; absent on video + legacy producers
155
+ product_name: z.string().optional(), // additive (Todo 20): real product name, deduped with a #N suffix when shared; absent ⇒ use display_name
156
+ transport: deviceTransportSchema.optional(), // additive (Todo 20): how the device is attached; absent on legacy producers
157
+ stable_id: z.string().optional(), // additive (Todo 20): reboot-stable hardware identity, distinct from input_id/device_path
158
+ physical_group_id: z.string().optional(), // additive (0.10.0, ADR-0008): `usb:<topology-token>` shared by one physical device's rows; absent on non-USB + legacy producers, and an absent group NEVER matches
159
+ hardware_serial: z.string().optional(), // additive (0.10.0, ADR-0008): USB serial, DIAGNOSTIC ONLY — vendors ship placeholder serials, so never match/group/select on it
113
160
  });
114
161
  // ---- transport telemetry (srt-stats event payload) ----
115
162
  export const srtStatsSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceralive/cerastream",
3
- "version": "2026.7.2",
3
+ "version": "2026.7.4",
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",