@camstack/types 1.2.41 → 1.2.43

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.
Files changed (43) hide show
  1. package/dist/addon.js +1 -1
  2. package/dist/addon.mjs +1 -1
  3. package/dist/canonical-hash-7nfBbEqR.mjs +35 -0
  4. package/dist/canonical-hash-BcZHRHIx.js +40 -0
  5. package/dist/cap-call-context.d.ts +26 -0
  6. package/dist/capabilities/index.d.ts +5 -5
  7. package/dist/capabilities/notification-rules.cap.d.ts +41 -0
  8. package/dist/capabilities/pipeline-analytics.cap.d.ts +92 -4
  9. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +149 -0
  10. package/dist/capabilities/pipeline-runner.cap.d.ts +119 -1
  11. package/dist/capabilities/platform-probe.cap.d.ts +3 -3
  12. package/dist/capabilities/privacy-mask.cap.d.ts +69 -9
  13. package/dist/capabilities/recording.cap.d.ts +30 -0
  14. package/dist/capabilities/snapshot.cap.d.ts +1 -1
  15. package/dist/capabilities/stream-broker.cap.d.ts +304 -0
  16. package/dist/capabilities/stream-params.cap.d.ts +8 -4
  17. package/dist/device/device-profile.d.ts +12 -4
  18. package/dist/device/system-mirror.d.ts +11 -0
  19. package/dist/encode-profile.d.ts +2 -0
  20. package/dist/ffmpeg/encode-defaults.d.ts +107 -0
  21. package/dist/ffmpeg/hwaccel.d.ts +98 -0
  22. package/dist/ffmpeg/invocation.d.ts +348 -0
  23. package/dist/ffmpeg/process.d.ts +135 -0
  24. package/dist/ffmpeg/sharing-key.d.ts +91 -0
  25. package/dist/generated/addon-api.d.ts +92 -4
  26. package/dist/generated/device-proxy.d.ts +2 -2
  27. package/dist/generated/method-access-map.d.ts +1 -1
  28. package/dist/generated/system-proxy.d.ts +2 -2
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +2069 -42
  31. package/dist/index.mjs +2006 -43
  32. package/dist/interfaces/camera-switches.d.ts +375 -0
  33. package/dist/interfaces/inference-engine.d.ts +24 -3
  34. package/dist/interfaces/ops-log.d.ts +4 -0
  35. package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
  36. package/dist/node.d.ts +2 -0
  37. package/dist/node.js +270 -36
  38. package/dist/node.mjs +269 -36
  39. package/dist/pipeline/native-lease.d.ts +150 -0
  40. package/dist/{sleep-DTce7-ch.js → sleep-Bx9IIoT0.js} +43 -1
  41. package/dist/{sleep-CXimb854.mjs → sleep-DtstvzWm.mjs} +38 -2
  42. package/dist/utils/addon-id.d.ts +30 -0
  43. package/package.json +1 -1
@@ -279,6 +279,181 @@ export declare const RtpSourceSchema: z.ZodObject<{
279
279
  pipelineKey: z.ZodString;
280
280
  }, z.core.$strip>;
281
281
  export type RtpSource = z.infer<typeof RtpSourceSchema>;
282
+ /**
283
+ * The encode request — **structured and serialisable, with NO raw-flag escape
284
+ * hatch.** This is deliberate and it is the one lesson taken from
285
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
286
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
287
+ * adding a flag silently forks the shared child, and two consumers that mean
288
+ * the same thing but spell it differently never share. Here every knob is a
289
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
290
+ * never an opaque array.
291
+ *
292
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
293
+ * The operator-facing derived-stream transform editor still has them — that is
294
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
295
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
296
+ */
297
+ export declare const EgressEncodeSchema: z.ZodObject<{
298
+ audio: z.ZodUnion<readonly [z.ZodLiteral<"passthrough">, z.ZodObject<{
299
+ codec: z.ZodEnum<{
300
+ copy: "copy";
301
+ opus: "opus";
302
+ aac: "aac";
303
+ pcmu: "pcmu";
304
+ pcma: "pcma";
305
+ }>;
306
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
307
+ sampleRateHz: z.ZodOptional<z.ZodNumber>;
308
+ channels: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
309
+ }, z.core.$strip>]>;
310
+ video: z.ZodObject<{
311
+ codec: z.ZodEnum<{
312
+ h264: "h264";
313
+ h265: "h265";
314
+ copy: "copy";
315
+ }>;
316
+ profile: z.ZodOptional<z.ZodEnum<{
317
+ high: "high";
318
+ baseline: "baseline";
319
+ main: "main";
320
+ }>>;
321
+ level: z.ZodOptional<z.ZodString>;
322
+ width: z.ZodOptional<z.ZodNumber>;
323
+ height: z.ZodOptional<z.ZodNumber>;
324
+ fps: z.ZodOptional<z.ZodNumber>;
325
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
326
+ gopFrames: z.ZodOptional<z.ZodNumber>;
327
+ bf: z.ZodOptional<z.ZodNumber>;
328
+ preset: z.ZodOptional<z.ZodEnum<{
329
+ fast: "fast";
330
+ ultrafast: "ultrafast";
331
+ superfast: "superfast";
332
+ veryfast: "veryfast";
333
+ faster: "faster";
334
+ medium: "medium";
335
+ }>>;
336
+ tune: z.ZodOptional<z.ZodEnum<{
337
+ zerolatency: "zerolatency";
338
+ film: "film";
339
+ animation: "animation";
340
+ }>>;
341
+ }, z.core.$strip>;
342
+ }, z.core.$strip>;
343
+ export type EgressEncode = z.infer<typeof EgressEncodeSchema>;
344
+ /**
345
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
346
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
347
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
348
+ * an Echo). Named rather than numeric so the INTENT survives.
349
+ */
350
+ export declare const EgressRateControlSchema: z.ZodEnum<{
351
+ tight: "tight";
352
+ relaxed: "relaxed";
353
+ }>;
354
+ export type EgressRateControl = z.infer<typeof EgressRateControlSchema>;
355
+ export declare const EgressTranscodeRequestSchema: z.ZodObject<{
356
+ deviceId: z.ZodNumber;
357
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
358
+ kind: z.ZodLiteral<"profile">;
359
+ profile: z.ZodEnum<{
360
+ high: "high";
361
+ mid: "mid";
362
+ low: "low";
363
+ }>;
364
+ }, z.core.$strip>, z.ZodObject<{
365
+ kind: z.ZodLiteral<"cam-stream">;
366
+ camStreamId: z.ZodString;
367
+ }, z.core.$strip>], "kind">;
368
+ encode: z.ZodObject<{
369
+ audio: z.ZodUnion<readonly [z.ZodLiteral<"passthrough">, z.ZodObject<{
370
+ codec: z.ZodEnum<{
371
+ copy: "copy";
372
+ opus: "opus";
373
+ aac: "aac";
374
+ pcmu: "pcmu";
375
+ pcma: "pcma";
376
+ }>;
377
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
378
+ sampleRateHz: z.ZodOptional<z.ZodNumber>;
379
+ channels: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
380
+ }, z.core.$strip>]>;
381
+ video: z.ZodObject<{
382
+ codec: z.ZodEnum<{
383
+ h264: "h264";
384
+ h265: "h265";
385
+ copy: "copy";
386
+ }>;
387
+ profile: z.ZodOptional<z.ZodEnum<{
388
+ high: "high";
389
+ baseline: "baseline";
390
+ main: "main";
391
+ }>>;
392
+ level: z.ZodOptional<z.ZodString>;
393
+ width: z.ZodOptional<z.ZodNumber>;
394
+ height: z.ZodOptional<z.ZodNumber>;
395
+ fps: z.ZodOptional<z.ZodNumber>;
396
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
397
+ gopFrames: z.ZodOptional<z.ZodNumber>;
398
+ bf: z.ZodOptional<z.ZodNumber>;
399
+ preset: z.ZodOptional<z.ZodEnum<{
400
+ fast: "fast";
401
+ ultrafast: "ultrafast";
402
+ superfast: "superfast";
403
+ veryfast: "veryfast";
404
+ faster: "faster";
405
+ medium: "medium";
406
+ }>>;
407
+ tune: z.ZodOptional<z.ZodEnum<{
408
+ zerolatency: "zerolatency";
409
+ film: "film";
410
+ animation: "animation";
411
+ }>>;
412
+ }, z.core.$strip>;
413
+ }, z.core.$strip>;
414
+ rateControl: z.ZodOptional<z.ZodEnum<{
415
+ tight: "tight";
416
+ relaxed: "relaxed";
417
+ }>>;
418
+ bitstreamFilter: z.ZodOptional<z.ZodEnum<{
419
+ dump_extra: "dump_extra";
420
+ h264_mp4toannexb: "h264_mp4toannexb";
421
+ hevc_mp4toannexb: "hevc_mp4toannexb";
422
+ }>>;
423
+ publishLocally: z.ZodOptional<z.ZodBoolean>;
424
+ pixelFormat: z.ZodOptional<z.ZodEnum<{
425
+ yuv420p: "yuv420p";
426
+ nv12: "nv12";
427
+ }>>;
428
+ decodeHwAccel: z.ZodOptional<z.ZodEnum<{
429
+ none: "none";
430
+ auto: "auto";
431
+ videotoolbox: "videotoolbox";
432
+ vaapi: "vaapi";
433
+ qsv: "qsv";
434
+ cuda: "cuda";
435
+ }>>;
436
+ hostname: z.ZodOptional<z.ZodString>;
437
+ tag: z.ZodOptional<z.ZodString>;
438
+ }, z.core.$strip>;
439
+ export type EgressTranscodeRequest = z.infer<typeof EgressTranscodeRequestSchema>;
440
+ export declare const EgressTranscodeSchema: z.ZodObject<{
441
+ url: z.ZodString;
442
+ pipelineKey: z.ZodString;
443
+ videoCodec: z.ZodEnum<{
444
+ H264: "H264";
445
+ H265: "H265";
446
+ }>;
447
+ resolution: z.ZodObject<{
448
+ width: z.ZodNumber;
449
+ height: z.ZodNumber;
450
+ }, z.core.$strip>;
451
+ transcoded: z.ZodBoolean;
452
+ encoder: z.ZodString;
453
+ decodeHwAccel: z.ZodNullable<z.ZodString>;
454
+ camStreamId: z.ZodNullable<z.ZodString>;
455
+ }, z.core.$strip>;
456
+ export type EgressTranscode = z.infer<typeof EgressTranscodeSchema>;
282
457
  export declare const streamBrokerCapability: {
283
458
  readonly name: "stream-broker";
284
459
  readonly scope: "system";
@@ -321,6 +496,7 @@ export declare const streamBrokerCapability: {
321
496
  baseline: "baseline";
322
497
  main: "main";
323
498
  }>>;
499
+ level: z.ZodOptional<z.ZodString>;
324
500
  width: z.ZodOptional<z.ZodNumber>;
325
501
  height: z.ZodOptional<z.ZodNumber>;
326
502
  fps: z.ZodOptional<z.ZodNumber>;
@@ -691,6 +867,134 @@ export declare const streamBrokerCapability: {
691
867
  released: z.ZodBoolean;
692
868
  refcount: z.ZodNumber;
693
869
  }, z.core.$strip>, "mutation">;
870
+ /**
871
+ * THE ffmpeg primitive. Acquire an encoded stream matching a structured
872
+ * encode plan; the broker builds the argv through the ONE builder
873
+ * (`@camstack/types` `ffmpeg/invocation.ts`), resolves decode hardware from
874
+ * the DECODER ADDON's per-node ranking, and returns a dialable RTSP url.
875
+ *
876
+ * **Refcounted and deduplicated on EXACT match.** Two requesters whose
877
+ * requests produce the same `egressTranscodeSharingKey` receive the SAME
878
+ * `pipelineKey` and the same child process. Nearly-identical requests are
879
+ * NOT merged.
880
+ *
881
+ * **The handle is immutable.** There is deliberately no `reconfigure`
882
+ * method and this one never accepts a `pipelineKey` alongside encode
883
+ * parameters: a consumer whose requirements change releases and
884
+ * re-acquires. A mutable shared handle is exactly what made Alexa's old
885
+ * `derived:alexa-<id>` stream a co-tenant hazard — one consumer's
886
+ * downgrade dragged every other consumer down with it.
887
+ *
888
+ * Placement: unpinned, `classifyCapRoute` serves this hub-in-process
889
+ * (Priority 1). A caller that wants the transcode elsewhere passes
890
+ * `nodePin(nodeId)` and a `hostname` it can dial.
891
+ */
892
+ readonly acquireEgressTranscode: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
893
+ deviceId: z.ZodNumber;
894
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
895
+ kind: z.ZodLiteral<"profile">;
896
+ profile: z.ZodEnum<{
897
+ high: "high";
898
+ mid: "mid";
899
+ low: "low";
900
+ }>;
901
+ }, z.core.$strip>, z.ZodObject<{
902
+ kind: z.ZodLiteral<"cam-stream">;
903
+ camStreamId: z.ZodString;
904
+ }, z.core.$strip>], "kind">;
905
+ encode: z.ZodObject<{
906
+ audio: z.ZodUnion<readonly [z.ZodLiteral<"passthrough">, z.ZodObject<{
907
+ codec: z.ZodEnum<{
908
+ copy: "copy";
909
+ opus: "opus";
910
+ aac: "aac";
911
+ pcmu: "pcmu";
912
+ pcma: "pcma";
913
+ }>;
914
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
915
+ sampleRateHz: z.ZodOptional<z.ZodNumber>;
916
+ channels: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
917
+ }, z.core.$strip>]>;
918
+ video: z.ZodObject<{
919
+ codec: z.ZodEnum<{
920
+ h264: "h264";
921
+ h265: "h265";
922
+ copy: "copy";
923
+ }>;
924
+ profile: z.ZodOptional<z.ZodEnum<{
925
+ high: "high";
926
+ baseline: "baseline";
927
+ main: "main";
928
+ }>>;
929
+ level: z.ZodOptional<z.ZodString>;
930
+ width: z.ZodOptional<z.ZodNumber>;
931
+ height: z.ZodOptional<z.ZodNumber>;
932
+ fps: z.ZodOptional<z.ZodNumber>;
933
+ bitrateKbps: z.ZodOptional<z.ZodNumber>;
934
+ gopFrames: z.ZodOptional<z.ZodNumber>;
935
+ bf: z.ZodOptional<z.ZodNumber>;
936
+ preset: z.ZodOptional<z.ZodEnum<{
937
+ fast: "fast";
938
+ ultrafast: "ultrafast";
939
+ superfast: "superfast";
940
+ veryfast: "veryfast";
941
+ faster: "faster";
942
+ medium: "medium";
943
+ }>>;
944
+ tune: z.ZodOptional<z.ZodEnum<{
945
+ zerolatency: "zerolatency";
946
+ film: "film";
947
+ animation: "animation";
948
+ }>>;
949
+ }, z.core.$strip>;
950
+ }, z.core.$strip>;
951
+ rateControl: z.ZodOptional<z.ZodEnum<{
952
+ tight: "tight";
953
+ relaxed: "relaxed";
954
+ }>>;
955
+ bitstreamFilter: z.ZodOptional<z.ZodEnum<{
956
+ dump_extra: "dump_extra";
957
+ h264_mp4toannexb: "h264_mp4toannexb";
958
+ hevc_mp4toannexb: "hevc_mp4toannexb";
959
+ }>>;
960
+ publishLocally: z.ZodOptional<z.ZodBoolean>;
961
+ pixelFormat: z.ZodOptional<z.ZodEnum<{
962
+ yuv420p: "yuv420p";
963
+ nv12: "nv12";
964
+ }>>;
965
+ decodeHwAccel: z.ZodOptional<z.ZodEnum<{
966
+ none: "none";
967
+ auto: "auto";
968
+ videotoolbox: "videotoolbox";
969
+ vaapi: "vaapi";
970
+ qsv: "qsv";
971
+ cuda: "cuda";
972
+ }>>;
973
+ hostname: z.ZodOptional<z.ZodString>;
974
+ tag: z.ZodOptional<z.ZodString>;
975
+ }, z.core.$strip>, z.ZodObject<{
976
+ url: z.ZodString;
977
+ pipelineKey: z.ZodString;
978
+ videoCodec: z.ZodEnum<{
979
+ H264: "H264";
980
+ H265: "H265";
981
+ }>;
982
+ resolution: z.ZodObject<{
983
+ width: z.ZodNumber;
984
+ height: z.ZodNumber;
985
+ }, z.core.$strip>;
986
+ transcoded: z.ZodBoolean;
987
+ encoder: z.ZodString;
988
+ decodeHwAccel: z.ZodNullable<z.ZodString>;
989
+ camStreamId: z.ZodNullable<z.ZodString>;
990
+ }, z.core.$strip>, "mutation">;
991
+ /** Drop one reference. The child dies when the last holder releases. */
992
+ readonly releaseEgressTranscode: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
993
+ pipelineKey: z.ZodString;
994
+ }, z.core.$strip>, z.ZodObject<{
995
+ released: z.ZodBoolean;
996
+ refcount: z.ZodNumber;
997
+ }, z.core.$strip>, "mutation">;
694
998
  /**
695
999
  * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
696
1000
  *
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { type InferNativeProvider } from './capability-definition.js';
3
2
  import { DeviceType } from '../device/device-type.js';
3
+ import { type InferNativeProvider } from './capability-definition.js';
4
4
  /** One of the camera's stream profiles. */
5
5
  export declare const StreamProfileSchema: z.ZodEnum<{
6
6
  sub: "sub";
@@ -183,7 +183,13 @@ export declare const StreamParamsOptionsSchema: z.ZodObject<{
183
183
  }, z.core.$strip>>;
184
184
  }, z.core.$strip>;
185
185
  /** A partial change to one profile — every field optional; a provider
186
- * ignores fields it doesn't support. */
186
+ * ignores fields it doesn't support.
187
+ *
188
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
189
+ * reachable from no form and honoured by exactly one provider, while the
190
+ * camera's microphone is a whole-device fact. It now has one writer,
191
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
192
+ * `privacy-mask.cap.ts`. */
187
193
  export declare const StreamProfilePatchSchema: z.ZodObject<{
188
194
  width: z.ZodOptional<z.ZodNumber>;
189
195
  height: z.ZodOptional<z.ZodNumber>;
@@ -203,7 +209,6 @@ export declare const StreamProfilePatchSchema: z.ZodObject<{
203
209
  main: "main";
204
210
  }>>;
205
211
  gop: z.ZodOptional<z.ZodNumber>;
206
- audio: z.ZodOptional<z.ZodBoolean>;
207
212
  }, z.core.$strip>;
208
213
  export type StreamParamsStatus = z.infer<typeof StreamParamsStatusSchema>;
209
214
  export type StreamParamsOptions = z.infer<typeof StreamParamsOptionsSchema>;
@@ -316,7 +321,6 @@ export declare const streamParamsCapability: {
316
321
  main: "main";
317
322
  }>>;
318
323
  gop: z.ZodOptional<z.ZodNumber>;
319
- audio: z.ZodOptional<z.ZodBoolean>;
320
324
  }, z.core.$strip>;
321
325
  }, z.core.$strip>, z.ZodVoid, "mutation">;
322
326
  /**
@@ -120,10 +120,18 @@ export interface DeviceProfile {
120
120
  */
121
121
  readonly defaults: DeviceProfileDefaults;
122
122
  /**
123
- * Generic settings overrides (snapshot interval, pre-buffer flag,
124
- * etc.). Same sticky semantics as defaults applied on first read,
125
- * operator override wins. Keys are dot-paths interpreted by the
126
- * per-driver / per-addon config schema.
123
+ * Generic settings overrides, keyed by dot-path, intended to be
124
+ * interpreted by the per-driver / per-addon config schema.
125
+ *
126
+ * **Nothing reads this today.** It is a declared extension point with no
127
+ * consumer anywhere in the workspace, so any entry placed here is inert. It
128
+ * shipped populated — `snapshot.minRefreshIntervalSec: 3600` and
129
+ * `streamBroker.preBufferEnabled: false` on the battery profile — which read
130
+ * as live policy and were not: both behaviours are really enforced off
131
+ * `DeviceFeature.BatteryOperated` directly, by `BATTERY_DEFAULT_MAX_AGE_S`
132
+ * in `snapshot.addon.ts` and by `getEffectivePreBufferSec` in
133
+ * `stream-broker-manager.ts`. Emptied 2026-08-07; keep it empty until a
134
+ * reader exists, or an entry here is just documentation that lies.
127
135
  */
128
136
  readonly settings: Readonly<Record<string, unknown>>;
129
137
  }
@@ -62,6 +62,17 @@ export interface SystemMirrorApi extends SliceHandleApi {
62
62
  addonId?: string;
63
63
  }): Promise<ReadonlyArray<DeviceInfo>>;
64
64
  };
65
+ /**
66
+ * Single-device metadata read. Required — `refreshDeviceMetadata`
67
+ * runs once per `device.registered` / `device.updated` event, and a
68
+ * full-catalog fetch there is what melts the hub during a
69
+ * deploy/restart storm (see `refreshDeviceMetadata`).
70
+ */
71
+ readonly getDevice: {
72
+ query(input: {
73
+ deviceId: number;
74
+ }): Promise<DeviceInfo | null>;
75
+ };
65
76
  };
66
77
  readonly deviceState: SliceHandleApi['deviceState'] & {
67
78
  readonly getAllSnapshots: {
@@ -22,6 +22,7 @@ export declare const VideoEncodeSchema: z.ZodObject<{
22
22
  baseline: "baseline";
23
23
  main: "main";
24
24
  }>>;
25
+ level: z.ZodOptional<z.ZodString>;
25
26
  width: z.ZodOptional<z.ZodNumber>;
26
27
  height: z.ZodOptional<z.ZodNumber>;
27
28
  fps: z.ZodOptional<z.ZodNumber>;
@@ -68,6 +69,7 @@ export declare const EncodeProfileSchema: z.ZodObject<{
68
69
  baseline: "baseline";
69
70
  main: "main";
70
71
  }>>;
72
+ level: z.ZodOptional<z.ZodString>;
71
73
  width: z.ZodOptional<z.ZodNumber>;
72
74
  height: z.ZodOptional<z.ZodNumber>;
73
75
  fps: z.ZodOptional<z.ZodNumber>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The ONE home for live-egress encode constants.
3
+ *
4
+ * Before this file there were five sets, in four packages, that nobody could
5
+ * diff: the WebRTC fallback, Alexa's "fallback", the transcode-down ceiling,
6
+ * the broker's audio presets, and HomeKit's inline block. Three of them
7
+ * disagreed about Opus channel count alone. They live here now because the
8
+ * consumers are separate ADDONS and addons may never import each other —
9
+ * `@camstack/types` is the only legal shared home.
10
+ *
11
+ * ## What changed value, and what kept a deliberate divergence
12
+ *
13
+ * KEPT, with the reason:
14
+ * - **VBV window.** HomeKit uses a ONE-second window; the browser and Alexa
15
+ * use TWO. HomeKit's link budget is enforced per second by the controller,
16
+ * so a keyframe allowed to borrow from the next second overshoots and the
17
+ * iOS client drops the stream. A browser wants the opposite: let a keyframe
18
+ * spike, keep the average. Encoded as {@link FfmpegRateControl}, not as two
19
+ * copies of a number.
20
+ * - **`preset`.** HomeKit and the browser use `ultrafast` (the encoder is on
21
+ * the interactive path); Alexa uses `veryfast` (Echo tolerates ~200 ms more
22
+ * latency and the extra quality is visible on a 10" panel).
23
+ * - **`-bsf:v dump_extra`** is HomeKit-only on BOTH the copy and encode
24
+ * branches: HAP negotiates its own SDP and cannot carry out-of-band
25
+ * extradata. Nothing else needs it, and emitting it elsewhere costs bytes.
26
+ *
27
+ * CHANGED — the divergences that were accidents:
28
+ * - **Opus channels.** Was 2 (broker preset), 1 (Alexa), 1 (HomeKit). Now 1
29
+ * everywhere: every consumer here is a camera microphone, which is mono at
30
+ * the source, and encoding a duplicated second channel spent bitrate on
31
+ * nothing. The broker's 2 was the outlier and it is the one that changed.
32
+ * - **Alexa gained `level: '3.1'`.** Its SDP advertises `profile-level-id=
33
+ * 42e01f` and its encoder was unconstrained. Now it matches.
34
+ * - **Alexa GOP.** Was 25 frames at 25 fps (1 s); the browser is 30 at 25 fps
35
+ * (1.2 s). Both are now expressed as `gopSeconds` × fps so the intent is
36
+ * legible; Alexa's numeric GOP is unchanged, the browser's rounds to 30.
37
+ */
38
+ import type { EncodeProfile } from '../encode-profile.js';
39
+ import type { FfmpegAudioEncodePlan, FfmpegRateControl } from './invocation.js';
40
+ /**
41
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
42
+ * Baseline because it is the one profile every consumer in this repo decodes
43
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
44
+ */
45
+ export declare const BASE_LIVE_EGRESS_PROFILE: EncodeProfile;
46
+ /**
47
+ * The browser WebRTC viewer's transcode profile. Used only when the broker
48
+ * MUST spawn ffmpeg (source codec ∉ the browser-accepted set — an H.265 source
49
+ * on an H.264-only browser). Audio is `passthrough` because werift carries the
50
+ * audio plane out-of-band; putting it in the video pipeline would encode a
51
+ * track nothing reads.
52
+ */
53
+ export declare const WEBRTC_EGRESS_PROFILE: EncodeProfile;
54
+ /**
55
+ * Alexa's egress profile. Alexa's audio ALSO rides the WebRTC session's own
56
+ * audio plane (out-of-band), so this is `passthrough` exactly like the browser
57
+ * — see the ADR for why the previous in-band Opus was encoded and discarded.
58
+ */
59
+ /**
60
+ * Alexa's egress asks for OPUS, and that is a change with a history.
61
+ *
62
+ * The previous profile encoded Opus IN-BAND into an MPEG-TS, where it was
63
+ * discarded: Opus is `stream_type 0x06` and the broker's demuxer maps only
64
+ * `0x0f` (aac), `0x03`/`0x04` (mp2) and `0x81` (ac3). Every frame it produced
65
+ * died at the demuxer, so the encode was replaced with `passthrough` — correct,
66
+ * because paying libopus for nothing is worse than silence.
67
+ *
68
+ * But `passthrough` means the egress emits NO audio at all, and Alexa's audio
69
+ * does not arrive by magic: it rides the published stream, which is what the
70
+ * WebRTC session dials. So the Echo had video and silence either way.
71
+ *
72
+ * Opus here now reaches the egress AUDIO SIDECAR — a separate RTP leg the
73
+ * restreamer grafts onto its SDP — which never touches the MPEG-TS demuxer that
74
+ * killed the in-band attempt. Same codec, different plane, and this one the
75
+ * consumer can actually negotiate.
76
+ */
77
+ export declare const ALEXA_EGRESS_PROFILE: EncodeProfile;
78
+ /** VBV window for a consumer whose budget is enforced per second (HomeKit). */
79
+ export declare const RATE_CONTROL_TIGHT: FfmpegRateControl;
80
+ /** VBV window for a consumer that tolerates a keyframe spike (browser, Echo). */
81
+ export declare const RATE_CONTROL_RELAXED: FfmpegRateControl;
82
+ /**
83
+ * Transcode-DOWN ceiling: the shape a remote/unhealthy WebRTC session is
84
+ * reduced to. 360p / 400 kbps is the `low` rung of the adaptive ladder — a
85
+ * session steered here is already failing, so the ceiling is chosen to survive
86
+ * a bad link, not to look good.
87
+ */
88
+ export declare const TRANSCODE_DOWN_MAX_HEIGHT = 360;
89
+ export declare const TRANSCODE_DOWN_MAX_BITRATE_KBPS = 400;
90
+ /**
91
+ * Camera-microphone audio, per codec. Defined in `./invocation.js` (an import
92
+ * cycle otherwise) and re-exported here, which is where to read it.
93
+ */
94
+ export { AUDIO_PRESETS } from './invocation.js';
95
+ /**
96
+ * HomeKit's two-way audio leg. `lowdelay` + a short frame duration because the
97
+ * leg is interactive; `globalHeader` because HAP owns the SDP and ffmpeg must
98
+ * not put extradata in-band. `sampleRateHz` and `frameDurationMs` are
99
+ * NEGOTIATED per session — the controller picks them — so they are absent here
100
+ * and filled in by the caller.
101
+ */
102
+ export declare const HAP_AUDIO_BITRATE_KBPS = 24;
103
+ export declare const HAP_AUDIO_BASE: FfmpegAudioEncodePlan;
104
+ /** HomeKit's `-bufsize` for the audio plane — four seconds of its 24 kbps. */
105
+ export declare const HAP_AUDIO_VBV_KBITS: number;
106
+ /** Seconds between forced IDRs on a HomeKit transcode. */
107
+ export declare const HAP_KEYFRAME_INTERVAL_SEC = 4;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Decode hardware acceleration for an EGRESS transcode — resolved through the
3
+ * ONE ranking that is known to work on this hardware.
4
+ *
5
+ * ## Why this file exists
6
+ *
7
+ * The repo has two hwaccel opinions and they disagree in the direction that
8
+ * breaks things:
9
+ *
10
+ * - the raw kernel resolver (`ctx.kernel.hwaccel.resolve()`) returns
11
+ * `['qsv','vaapi']` for an Intel host — qsv FIRST;
12
+ * - `addon-decoder-ffmpeg`'s `DECODE_HWACCEL_RANK` puts **vaapi above qsv on
13
+ * purpose**, because on this exact hub a qsv decode child exits `code=171`
14
+ * producing no frames, while vaapi decodes 8 MP h264 at ~50 fps.
15
+ *
16
+ * **The decoder addon is the authority.** It publishes its answer per node as
17
+ * the `probedBestHwaccel@<node>` setting and exposes it on the `decoder` cap's
18
+ * `getInfo` — the same value `AgentLoadService.readNodeDecodeHwaccel` reads to
19
+ * show per-node hwaccel in the UI. This module reads THAT, and deliberately
20
+ * never re-derives a ranking: a second copy of the rank table is exactly how
21
+ * the two opinions appeared. Addons may not import each other, so the read is
22
+ * a `ctx.api.decoder.getInfo` call injected as {@link EgressHwAccelDeps.readDecoderBackend}.
23
+ *
24
+ * ## Why the kernel resolver is still a dependency here
25
+ *
26
+ * It is only ever used to say something in a log line. It is never allowed to
27
+ * SELECT a backend: if the decoder cannot answer, this resolver returns
28
+ * software rather than adopting the kernel's qsv-first order, because that
29
+ * order is the known-bad one. `never silently downgrades` in the spec is the
30
+ * test that holds this.
31
+ *
32
+ * ## The failure mode this must not reproduce
33
+ *
34
+ * A silent fall back to software decode is a FLOW bug on this hub, not a
35
+ * capability limit (vaapi works — 10 concurrent 4K decodes pass). So every
36
+ * path that ends in software calls {@link EgressHwAccelDeps.onFallback}, which
37
+ * the caller logs at `warn` with `tags: { deviceId }`. Silence reads as
38
+ * "hardware was used".
39
+ */
40
+ /** Why a resolve ended in software decode. */
41
+ export type EgressHwAccelFallbackReason =
42
+ /** The `decoder` cap could not be reached (offline node, cap not mounted). */
43
+ 'decoder-unreadable'
44
+ /** The decoder answered, but has not probed a backend on this node yet. */
45
+ | 'decoder-unprobed';
46
+ export interface EgressHwAccelFallback {
47
+ readonly reason: EgressHwAccelFallbackReason;
48
+ /** What the kernel resolver would have said — for the log line ONLY. */
49
+ readonly kernelPreferred: readonly string[];
50
+ readonly error?: string;
51
+ }
52
+ /**
53
+ * A bounded memo so a per-session resolve is not a per-session cross-process
54
+ * cap call. Owned by the caller (one per broker / exporter), never a module
55
+ * global — a module global would outlive an addon respawn and survive an
56
+ * operator changing the decoder backend.
57
+ */
58
+ export interface HwAccelCache {
59
+ read(): string | null | undefined;
60
+ write(value: string | null): void;
61
+ }
62
+ export interface HwAccelCacheOptions {
63
+ readonly ttlMs: number;
64
+ readonly now?: () => number;
65
+ }
66
+ export declare function createHwAccelCache(options: HwAccelCacheOptions): HwAccelCache;
67
+ export interface EgressHwAccelDeps {
68
+ /**
69
+ * `ctx.api.decoder.getInfo.query(undefined, nodePin(nodeId)).probedBestHwaccel`
70
+ * — the DECODER ADDON's own answer for THIS node, already re-ranked through
71
+ * `DECODE_HWACCEL_RANK`. Throw or return `''` when it is not knowable.
72
+ */
73
+ readDecoderBackend(): Promise<string>;
74
+ /**
75
+ * `ctx.kernel.hwaccel.resolve().preferred` — reported in the fallback log so
76
+ * an operator can see the disagreement. NEVER used to select a backend.
77
+ */
78
+ readKernelPreferred(): Promise<readonly string[]>;
79
+ /** Called on every path that ends in software. The caller logs at `warn`. */
80
+ onFallback(fallback: EgressHwAccelFallback): void;
81
+ /**
82
+ * An explicit operator choice (`decoder.hwaccel`, or a per-consumer pin).
83
+ * `'auto'` is passed through as the literal ffmpeg instruction; `'none'` /
84
+ * `'copy'` force software; anything else pins that backend. Absent ⇒ ask the
85
+ * decoder.
86
+ */
87
+ readonly override?: string | null;
88
+ readonly cache?: HwAccelCache;
89
+ }
90
+ /**
91
+ * Resolve the `-hwaccel` value for an egress transcode. Hardware is the
92
+ * DEFAULT — an egress that decodes in software on a hub with working vaapi is
93
+ * paying for nothing — and every software outcome is announced.
94
+ *
95
+ * Returns a concrete backend name, the literal `'auto'`, or `null` for
96
+ * software decode (⇒ {@link buildFfmpegArgs} emits no `-hwaccel` at all).
97
+ */
98
+ export declare function resolveEgressDecodeHwAccel(deps: EgressHwAccelDeps): Promise<string | null>;