@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
@@ -0,0 +1,348 @@
1
+ /**
2
+ * THE ffmpeg argv builder. Every ffmpeg this repo spawns for a LIVE MEDIA
3
+ * transcode — the broker transcode pool, the WebRTC transcode leg, a derived
4
+ * stream, HomeKit's SRTP egress, Alexa — assembles its argument list here.
5
+ *
6
+ * It lives in `@camstack/types` rather than in an addon for one reason: three
7
+ * addons need it (`addon-pipeline`, `addon-export-hap`, `addon-export-alexa`)
8
+ * and addons may never import each other. `@camstack/types` is the framework
9
+ * package all three already depend on, and it already owns `EncodeProfile`.
10
+ *
11
+ * The builder is PURE — argument list only. The binary path comes from
12
+ * `system-config`'s `ffmpeg.binaryPath` and goes to `spawn` separately (see
13
+ * `../ffmpeg-node/process.js` for the lifecycle wrapper).
14
+ *
15
+ * ## The one rule this file exists to enforce
16
+ *
17
+ * **`-hwaccel` is an INPUT option.** ffmpeg accepts it after `-i` without any
18
+ * error and then silently decodes in software: the CPU cost stays, the GPU is
19
+ * idle, and the log line still says "using hardware decode". Everything that
20
+ * configures the INPUT is emitted by {@link buildInputArgs} strictly before
21
+ * `-i`; everything that configures an OUTPUT comes after. `__tests__/
22
+ * ffmpeg-invocation.spec.ts` asserts INDEXES, never membership.
23
+ */
24
+ import type { AudioEncode, EncodeProfile } from '../encode-profile.js';
25
+ /** Everything that configures the ffmpeg INPUT. Emitted strictly before `-i`. */
26
+ export interface FfmpegInputPlan {
27
+ /** An RTSP url, a `pipe:0` stdin feed, or a file path. */
28
+ readonly url: string;
29
+ /**
30
+ * `-rtsp_transport`. `null` for a non-RTSP input (stdin, file). TCP is the
31
+ * only value in production use: the broker's loopback restreams are TCP and
32
+ * UDP loses packets under a burst.
33
+ */
34
+ readonly rtspTransport?: 'tcp' | 'udp' | null;
35
+ /** `-fflags` values, e.g. `['+discardcorrupt']`, `['+genpts']`. */
36
+ readonly fflags?: readonly string[];
37
+ /**
38
+ * `-analyzeduration` (microseconds) and `-probesize` (bytes): how long ffmpeg
39
+ * inspects the input before it emits ANYTHING.
40
+ *
41
+ * Unset means ffmpeg's defaults — 5 s and 5 MB — and on a live egress pull
42
+ * that is a five-second stall before the first packet, paid by whoever is
43
+ * staring at the player. Measured on the live hub: a HomeKit session on a
44
+ * 9 fps slot took 9 s from `stream started` to the first packet out of
45
+ * ffmpeg and 12.2 s to the first key frame. The SLOWER the slot, the longer
46
+ * the wait, because the default budget is wall-clock over an input that
47
+ * delivers few frames per second — which is why the 25 fps slot looked fine
48
+ * and the 9 fps one looked broken.
49
+ *
50
+ * Safe to shrink on RTSP specifically: the SDP already declares the codec,
51
+ * so there is nothing for the probe to discover. Leave unset for inputs that
52
+ * genuinely need discovery (a file of unknown provenance, a raw pipe).
53
+ */
54
+ readonly analyzeDurationUs?: number;
55
+ readonly probeSizeBytes?: number;
56
+ /** Caller-supplied input options (an `EncodeProfile.inputArgs`, probe flags). */
57
+ readonly extraArgs?: readonly string[];
58
+ }
59
+ /** ffmpeg video encoder ids the repo selects between. */
60
+ export type FfmpegVideoEncoderId = 'libx264' | 'libx265' | 'h264_videotoolbox' | 'hevc_videotoolbox' | 'h264_vaapi' | 'hevc_vaapi' | 'h264_qsv' | 'hevc_qsv' | 'h264_nvenc' | 'hevc_nvenc' | 'h264_amf' | 'hevc_amf';
61
+ /**
62
+ * How the encoder is bounded.
63
+ *
64
+ * - `'cbr'` — a TARGET bitrate plus a matching peak (`-b:v` + `-maxrate`).
65
+ * What a real-time egress with a fixed budget wants.
66
+ * - `'cap'` — a CEILING only (`-maxrate`, no `-b:v`): the encoder spends less
67
+ * on an easy scene but never exceeds the bound.
68
+ *
69
+ * `vbvSeconds` is the VBV window as a multiple of the bitrate. One second is a
70
+ * tight window that keeps instantaneous rate near the target (HomeKit's link
71
+ * budget is per-second); two seconds lets a keyframe spike borrow from the next
72
+ * second, which is what a browser viewer wants.
73
+ */
74
+ export type FfmpegRateControl = {
75
+ readonly kind: 'cbr';
76
+ readonly vbvSeconds: number;
77
+ } | {
78
+ readonly kind: 'cap';
79
+ readonly vbvSeconds: number;
80
+ };
81
+ /** Fit-inside (never upscale, even dimensions) vs an exact rescale. */
82
+ export interface FfmpegScalePlan {
83
+ readonly mode: 'fit' | 'exact';
84
+ readonly width: number;
85
+ readonly height: number;
86
+ }
87
+ export interface FfmpegVideoEncodePlan {
88
+ readonly kind: 'encode';
89
+ readonly encoder: FfmpegVideoEncoderId;
90
+ readonly scale?: FfmpegScalePlan | null;
91
+ readonly preset?: string;
92
+ readonly tune?: string;
93
+ /** `-profile:v` — baseline / main / high. */
94
+ readonly profile?: string;
95
+ /**
96
+ * `-level`. Omitting this while advertising a level in SDP is the defect
97
+ * that kept HomeKit black for a year and that Alexa shipped too: a stream
98
+ * that does not match its own advertisement.
99
+ */
100
+ readonly level?: string;
101
+ readonly pixelFormat?: string;
102
+ readonly fps?: number;
103
+ readonly gopFrames?: number;
104
+ /**
105
+ * `-force_key_frames expr:gte(t,n_forced*<sec>)` — an IDR every `<sec>`
106
+ * seconds of OUTPUT time, regardless of what the source does.
107
+ *
108
+ * Only a fragmented-MP4 consumer needs this today, and it needs it for a
109
+ * structural reason rather than a quality one: an fMP4 fragment written with
110
+ * `frag_keyframe` is cut at a key frame, so the key-frame cadence IS the
111
+ * fragment cadence. `-g` bounds the GOP the encoder would choose anyway;
112
+ * this pins the boundary to a wall-clock grid the muxer can meet.
113
+ *
114
+ * Structurally absent from {@link FfmpegVideoCopyPlan}: a copy has no
115
+ * encoder to instruct, and ffmpeg accepts the flag there, warns, and ignores
116
+ * it — the silent-no-op shape this file exists to prevent.
117
+ */
118
+ readonly forceKeyFramesSeconds?: number;
119
+ readonly bf?: number;
120
+ readonly bitrateKbps?: number;
121
+ readonly rateControl?: FfmpegRateControl;
122
+ /** `-bsf:v`, e.g. `dump_extra` when the consumer negotiates its own SDP. */
123
+ readonly bitstreamFilter?: string;
124
+ }
125
+ export interface FfmpegVideoCopyPlan {
126
+ readonly kind: 'copy';
127
+ readonly bitstreamFilter?: string;
128
+ }
129
+ export type FfmpegVideoPlan = FfmpegVideoCopyPlan | FfmpegVideoEncodePlan;
130
+ export type FfmpegAudioCodecId = 'opus' | 'aac' | 'pcmu' | 'pcma';
131
+ export interface FfmpegAudioEncodePlan {
132
+ readonly kind: 'encode';
133
+ readonly codec: FfmpegAudioCodecId;
134
+ readonly bitrateKbps?: number;
135
+ readonly sampleRateHz?: number;
136
+ readonly channels?: number;
137
+ /** libopus `-application` — `lowdelay` for a real-time two-way leg. */
138
+ readonly application?: 'lowdelay' | 'voip' | 'audio';
139
+ /** libopus `-frame_duration` in ms — negotiated per consumer session. */
140
+ readonly frameDurationMs?: number;
141
+ /** `-bufsize` for the audio plane, in kbit. */
142
+ readonly vbvBufferKbits?: number;
143
+ /** `-flags +global_header` — required when the consumer owns the SDP. */
144
+ readonly globalHeader?: boolean;
145
+ /** `-af` filter chain, e.g. `aresample=async=1000:first_pts=0`. */
146
+ readonly filter?: string;
147
+ }
148
+ export type FfmpegAudioPlan = {
149
+ readonly kind: 'none';
150
+ } | {
151
+ readonly kind: 'copy';
152
+ } | FfmpegAudioEncodePlan;
153
+ /**
154
+ * Camera-microphone audio, per codec. Lives HERE rather than in
155
+ * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
156
+ * on these types); it is re-exported from there, which is where to read it.
157
+ *
158
+ * Every source in this repo is a mono camera mic. The former broker preset
159
+ * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
160
+ * that is the value this consolidation changed.
161
+ */
162
+ export declare const AUDIO_PRESETS: Readonly<Record<'aac' | 'opus' | 'pcmu', FfmpegAudioEncodePlan>>;
163
+ /** One RTP output leg. The payload type / SSRC / MTU are the CONSUMER's facts. */
164
+ export interface FfmpegRtpOutput {
165
+ /** `rtp://host:port[?pkt_size=N]`. */
166
+ readonly url: string;
167
+ readonly payloadType?: number;
168
+ /** ffmpeg parses `-ssrc` as a SIGNED int32 — callers coerce with `| 0`. */
169
+ readonly ssrc?: number;
170
+ /** Path ffmpeg writes this output's SDP to (`-sdp_file`). */
171
+ readonly sdpFile?: string;
172
+ }
173
+ /**
174
+ * A fragmented-MP4 bytestream on stdout: `ftyp`+`moov` once, then a
175
+ * `moof`+`mdat` per fragment. The shape HomeKit Secure Video's recording
176
+ * delegate consumes, and the shape a CamStack clip source can consume from the
177
+ * same child (D67's primitive, one fragmenter, two consumers).
178
+ *
179
+ * ## Why `frag_keyframe` + `min_frag_duration`, and NOT `frag_duration`
180
+ *
181
+ * HKSV requires every media fragment to START on a sync sample. `frag_keyframe`
182
+ * cuts a fragment at each key frame, which satisfies that by construction.
183
+ * `-frag_duration` cuts at a wall-clock boundary *whether or not* a key frame
184
+ * has arrived, so a source whose IDR is late produces a fragment that opens on
185
+ * a delta frame — accepted by the muxer, rejected by the consumer, and
186
+ * invisible in an argv test. `-min_frag_duration` is the safe half of the pair:
187
+ * it only ever DELAYS a cut, so the boundary stays a key frame and a source
188
+ * with a 1 s GOP does not emit four fragments where one was negotiated.
189
+ *
190
+ * The consequence, stated because it decides what the caller must do: the
191
+ * fragment length is the key-frame cadence, so a caller that wants ~N-second
192
+ * fragments must also pin {@link FfmpegVideoEncodePlan.forceKeyFramesSeconds}.
193
+ * On a `copy` plan it cannot, and the fragment length is then whatever the
194
+ * SOURCE's GOP makes it — `fragmentMs` becomes a floor, not a target.
195
+ */
196
+ export interface FfmpegFmp4Sink {
197
+ readonly kind: 'stdout';
198
+ readonly container: 'mp4';
199
+ /**
200
+ * The negotiated fragment length in milliseconds. Emitted as
201
+ * `-min_frag_duration` (microseconds): no fragment is cut before this much
202
+ * media has accumulated, and the cut itself still lands on a key frame.
203
+ */
204
+ readonly fragmentMs: number;
205
+ }
206
+ export type FfmpegSink = {
207
+ readonly kind: 'rtsp-listen';
208
+ readonly url: string;
209
+ } | {
210
+ readonly kind: 'stdout';
211
+ readonly container: 'h264' | 'hevc' | 'mpegts';
212
+ } | FfmpegFmp4Sink
213
+ /**
214
+ * Two independent mapped RTP outputs. Generic ffmpeg RTP vocabulary — the
215
+ * SRTP encryption, the port allocation and the payload-type numbers stay
216
+ * with the consumer that negotiated them.
217
+ */
218
+ | {
219
+ readonly kind: 'rtp-outputs';
220
+ readonly video: FfmpegRtpOutput | null;
221
+ readonly audio: FfmpegRtpOutput | null;
222
+ };
223
+ /**
224
+ * An optional SECOND output carrying source audio as its own RTP stream,
225
+ * alongside an elementary video sink (which is `-an`). ffmpeg writes the
226
+ * authoritative SDP to `sdpFile`.
227
+ */
228
+ export interface FfmpegAudioSidecar {
229
+ readonly codec: FfmpegAudioCodecId;
230
+ readonly rtpUrl: string;
231
+ readonly sdpFile: string;
232
+ }
233
+ export interface FfmpegInvocation {
234
+ readonly logLevel: 'error' | 'warning' | 'info';
235
+ /**
236
+ * A concrete `-hwaccel` backend, the literal `'auto'`, or a software
237
+ * sentinel (`null` / `'none'` / `'copy'`) ⇒ no `-hwaccel` at all.
238
+ * Resolved by `resolveDecodeHwAccel` (see `./hwaccel.js`), which reads the
239
+ * DECODER ADDON's ranking, never the raw kernel resolver's.
240
+ */
241
+ readonly decodeHwAccel: string | null;
242
+ readonly input: FfmpegInputPlan;
243
+ readonly video: FfmpegVideoPlan;
244
+ readonly audio: FfmpegAudioPlan;
245
+ /** `0` = auto (omit `-threads` and let ffmpeg decide). */
246
+ readonly threadCount: number;
247
+ /** Consumer output options, emitted verbatim after the encode block. */
248
+ readonly outputArgs: readonly string[];
249
+ readonly sink: FfmpegSink;
250
+ readonly audioSidecar?: FfmpegAudioSidecar | null;
251
+ }
252
+ /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
253
+ export declare function logBannerArgs(level: 'error' | 'warning' | 'info'): string[];
254
+ /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
255
+ export declare function isSoftwareDecode(decodeHwAccel: string | null): boolean;
256
+ /**
257
+ * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
258
+ * appended to this list by a caller — that is the whole point of the function.
259
+ */
260
+ export declare function buildInputArgs(input: FfmpegInputPlan, decodeHwAccel: string | null): string[];
261
+ /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
262
+ export declare function buildVideoArgs(video: FfmpegVideoPlan, outputArgs: readonly string[]): string[];
263
+ /** The whole audio block, after `-i`. */
264
+ export declare function buildAudioArgs(audio: FfmpegAudioPlan): string[];
265
+ /**
266
+ * Assemble the full ffmpeg argument list. Layout:
267
+ *
268
+ * -hide_banner -loglevel <level>
269
+ * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
270
+ * [<input.extraArgs>] │
271
+ * [-fflags <flag>…] │
272
+ * [-rtsp_transport tcp] │
273
+ * -i <url> ─┘
274
+ * <video block> <threads> <audio block> ─┐ OUTPUT options.
275
+ * <consumer outputArgs verbatim> │
276
+ * <sink> ─┘ terminal
277
+ */
278
+ export declare function buildFfmpegArgs(inv: FfmpegInvocation): string[];
279
+ /**
280
+ * The hardware encoder for a target codec on `backend`, or the software one.
281
+ * `'auto'` is NOT a backend identity (it is an instruction to ffmpeg), so it
282
+ * maps to software encoding.
283
+ */
284
+ export declare function pickVideoEncoder(target: 'h264' | 'h265', backend: string | null, useHardware: boolean): FfmpegVideoEncoderId;
285
+ /** Map an `EncodeProfile.audio` to an audio plan. */
286
+ export declare function audioPlanFromEncodeProfile(audio: AudioEncode): FfmpegAudioPlan;
287
+ export interface EncodeProfileInvocationInput {
288
+ readonly profile: EncodeProfile;
289
+ /** The SOURCE's video codec — drives the smart-copy decision. */
290
+ readonly sourceCodec: 'h264' | 'h265';
291
+ readonly sourceUrl: string;
292
+ readonly sink: FfmpegSink;
293
+ readonly decodeHwAccel: string | null;
294
+ /**
295
+ * Defeat smart-copy. A DERIVED stream always applies its profile — applying
296
+ * it is its entire reason to exist — so it re-encodes even when the source
297
+ * already speaks the target codec. A `copy` profile still copies.
298
+ */
299
+ readonly forceReencode?: boolean;
300
+ /** Select the hardware encoder for {@link decodeHwAccel}'s backend. */
301
+ readonly hardwareEncoders?: boolean;
302
+ readonly logLevel?: 'error' | 'warning' | 'info';
303
+ readonly threadCount?: number;
304
+ readonly audioSidecar?: FfmpegAudioSidecar | null;
305
+ /**
306
+ * How the encoder is bounded. `EncodeProfile` carries a bitrate but not the
307
+ * WINDOW it is enforced over, and the window is the difference between a
308
+ * HomeKit stream that fits its per-second budget and one that overshoots on
309
+ * every key frame. Absent ⇒ the relaxed two-second default a browser wants.
310
+ *
311
+ * The three overlays below are on the INVOCATION rather than on
312
+ * `EncodeProfile` on purpose: they are consumer-protocol facts
313
+ * (`acquireEgressTranscode` exposes each as its own named request field),
314
+ * while `EncodeProfile` is the operator-facing shape a derived-stream editor
315
+ * writes. Folding them into the profile would put ffmpeg detail in front of
316
+ * an operator who has no way to judge it.
317
+ */
318
+ readonly rateControl?: FfmpegRateControl;
319
+ /** `-bsf:v` — `dump_extra` for a consumer that negotiates its own SDP. */
320
+ readonly bitstreamFilter?: string;
321
+ /** `-pix_fmt`. */
322
+ readonly pixelFormat?: string;
323
+ /**
324
+ * `-force_key_frames` on a fixed grid — see
325
+ * {@link FfmpegVideoEncodePlan.forceKeyFramesSeconds}. An overlay like the
326
+ * three above rather than an `EncodeProfile` field: it is a CONSUMER-protocol
327
+ * fact (an fMP4 fragment length the consumer negotiated), not something an
328
+ * operator authoring a derived stream can judge.
329
+ *
330
+ * Applies only to the encode branch. On a smart COPY there is no encoder to
331
+ * instruct, and the caller — not this adapter — has to decide whether a
332
+ * source-dictated GOP is acceptable, because only the caller knows what it
333
+ * promised its consumer.
334
+ */
335
+ readonly forceKeyFramesSeconds?: number;
336
+ }
337
+ /**
338
+ * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
339
+ * {@link FfmpegInvocation}. This is the ONLY bridge between the two models —
340
+ * a second one is how the repo grew two argv builders that disagreed about
341
+ * hardware.
342
+ *
343
+ * Smart video copy: when the source already speaks the requested codec the
344
+ * encode block is elided entirely and ffmpeg runs as a re-muxer on the video
345
+ * plane. Width / height / fps / bitrate in the profile are a downstream BUDGET,
346
+ * not a forced rescale.
347
+ */
348
+ export declare function invocationFromEncodeProfile(input: EncodeProfileInvocationInput): FfmpegInvocation;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `FfmpegProcess` — the ONE spawn/lifecycle wrapper for a live-media ffmpeg.
3
+ *
4
+ * Extracted from `TranscodeEgress.spawnAttempt`, which was already the most
5
+ * complete of the repo's hand-rolled lifecycles: first-data deadline, hardware
6
+ * →software retry, SIGTERM-then-SIGKILL. This generalises it and adds the two
7
+ * things every copy was missing — a `tags: { deviceId }` on every line, and a
8
+ * bounded restart — so a consumer gets them by construction instead of by
9
+ * remembering.
10
+ *
11
+ * ## What it owns
12
+ *
13
+ * - spawn, with the argv from the ONE builder (`./invocation.js`);
14
+ * - a FIRST-DATA deadline: an ffmpeg that starts but never emits is dead, and
15
+ * nothing downstream can tell that apart from a slow camera;
16
+ * - HARDWARE→SOFTWARE retry, announced at `warn`. A silent downgrade on this
17
+ * hub is a flow bug, not a capability limit — see `docs/design/decode-path.md`;
18
+ * - exit classification (`ok` / `signalled-by-us` / `crashed` / `no-output`);
19
+ * - bounded restart with backoff, and a terminal give-up (never an infinite
20
+ * loop — the same rule `CrashSupervisor` enforces for runners, D6);
21
+ * - SIGTERM then SIGKILL after a grace, gated on the child not having already
22
+ * exited.
23
+ *
24
+ * ## What it does NOT own
25
+ *
26
+ * The output PLUMBING. A consumer attaches to `stdout` / `stderr` itself,
27
+ * because what the bytes mean is the consumer's business: the broker deframes
28
+ * Annex-B into a restreamer, the WebRTC leg regroups access units, HomeKit
29
+ * writes nothing to stdout at all (its output is two RTP sockets). A wrapper
30
+ * that also owned the bytes would need a mode per consumer, which is the same
31
+ * mistake as one builder per consumer.
32
+ */
33
+ import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';
34
+ import type { IScopedLogger } from '../interfaces/logging.js';
35
+ /** How a child ended. */
36
+ export type FfmpegExitClass =
37
+ /** Exited 0. */
38
+ 'ok'
39
+ /** We asked it to stop — an exit code here is expected, not a failure. */
40
+ | 'stopped'
41
+ /** Exited non-zero / on a signal we did not send. */
42
+ | 'crashed'
43
+ /** Ran, but never produced a byte before the first-data deadline. */
44
+ | 'no-output';
45
+ export interface FfmpegExit {
46
+ readonly classification: FfmpegExitClass;
47
+ readonly code: number | null;
48
+ readonly signal: NodeJS.Signals | null;
49
+ /** The last stderr lines — the only diagnosis available after the fact. */
50
+ readonly stderrTail: readonly string[];
51
+ /** Whether this child was decoding in hardware. */
52
+ readonly decodeHwAccel: string | null;
53
+ }
54
+ export interface FfmpegProcessOptions {
55
+ /** `system-config` `ffmpeg.binaryPath`. */
56
+ readonly binaryPath: string;
57
+ /**
58
+ * Build the argv for one attempt. Called per spawn so a retry can lower
59
+ * `decodeHwAccel` to `null` (software) without the caller re-deriving
60
+ * anything else.
61
+ */
62
+ readonly buildArgs: (decodeHwAccel: string | null) => readonly string[];
63
+ /** The resolved hardware backend for the FIRST attempt (`null` ⇒ software). */
64
+ readonly decodeHwAccel: string | null;
65
+ readonly logger: IScopedLogger;
66
+ /**
67
+ * The camera this ffmpeg serves. Stamped on EVERY line as `tags.deviceId`:
68
+ * a miss rate or a media gap is always asked per camera, and a line without
69
+ * it cannot answer "why is 617 worse than 615".
70
+ */
71
+ readonly deviceId: number;
72
+ /** Free-form label for the log (`'webrtc-transcode'`, `'hap-egress'`, …). */
73
+ readonly role: string;
74
+ /** Extra queryable tags (sessionId, brokerId, camStreamId…). */
75
+ readonly tags?: Readonly<Record<string, string | number>>;
76
+ readonly stdio?: SpawnOptions['stdio'];
77
+ /** Wire up stdout/stderr. Called once per spawned child. */
78
+ readonly onChild: (child: ChildProcess) => void;
79
+ /**
80
+ * Called when a child ends. `restart` tells the wrapper whether to respawn;
81
+ * it is bounded by {@link maxRestarts} regardless of what this returns.
82
+ */
83
+ readonly onExit?: (exit: FfmpegExit) => void;
84
+ /** Wall-clock budget for the first output byte. `0` disables the deadline. */
85
+ readonly firstDataTimeoutMs?: number;
86
+ /** `0` ⇒ never restart (a run-to-completion job). */
87
+ readonly maxRestarts?: number;
88
+ readonly restartDelayMs?: number;
89
+ /** Consecutive-failure counter resets after a child has run this long. */
90
+ readonly stableRunMs?: number;
91
+ readonly killGraceMs?: number;
92
+ /** Injected for tests. */
93
+ readonly spawnFn?: typeof spawn;
94
+ readonly now?: () => number;
95
+ readonly setTimeoutFn?: typeof setTimeout;
96
+ }
97
+ export declare class FfmpegProcess {
98
+ private readonly opts;
99
+ private child;
100
+ private stopped;
101
+ private producedOutput;
102
+ private consecutiveFailures;
103
+ private startedAtMs;
104
+ private stderrTail;
105
+ private activeHwAccel;
106
+ private triedSoftwareFallback;
107
+ private firstDataTimer;
108
+ constructor(opts: FfmpegProcessOptions);
109
+ /** Queryable tags on every line — `deviceId` is never optional. */
110
+ private get logTags();
111
+ private get now();
112
+ /** `true` while a child is running. */
113
+ isRunning(): boolean;
114
+ /** The backend the CURRENT child decodes with (`null` ⇒ software). */
115
+ activeDecodeHwAccel(): string | null;
116
+ /**
117
+ * Spawn the first child. Resolves as soon as it produces output; rejects if
118
+ * it dies or stays silent past the deadline AFTER the software retry has
119
+ * also been exhausted. A caller that wants fire-and-forget can ignore the
120
+ * promise — the restart loop runs regardless.
121
+ */
122
+ start(): Promise<void>;
123
+ private spawnAttempt;
124
+ /**
125
+ * A child failed before going live. Try SOFTWARE once if it was decoding in
126
+ * hardware — loudly — then fall through to the bounded restart.
127
+ */
128
+ private handleFailure;
129
+ private scheduleRestart;
130
+ private report;
131
+ private clearFirstDataTimer;
132
+ /** Terminate for good. Idempotent; no restart follows. */
133
+ stop(): void;
134
+ private killChild;
135
+ }
@@ -0,0 +1,91 @@
1
+ import type { EgressTranscodeRequest } from '../capabilities/stream-broker.cap.js';
2
+ /**
3
+ * The fields that describe WHAT IS ENCODED. Everything outside this shape is
4
+ * excluded on purpose:
5
+ *
6
+ * - `hostname` only rewrites the DIAL address of a restreamer that is the same
7
+ * object either way (`substituteRtspHost`), so folding it in would fork the
8
+ * child once per consuming node;
9
+ * - `tag` is attribution for the broker panel.
10
+ *
11
+ * `publishLocally` is INSIDE the shape, and it is not a delivery detail like
12
+ * `hostname`. A push child emits MPEG-TS and its demuxed packets go straight
13
+ * into a published cam stream; a dial child emits an elementary Annex-B
14
+ * bytestream into a restreamer a consumer dials. Same encode parameters,
15
+ * completely different transports — so with it excluded, whoever acquired
16
+ * FIRST decided the transport for every later sharer, and the loser received a
17
+ * handle whose transport its consumer cannot use.
18
+ */
19
+ /**
20
+ * How the encoded bytes REACH the consumer. Three transports, three containers,
21
+ * three delivery mechanisms — and a child can serve exactly one of them, which
22
+ * is why this is in the key and `hostname` is not.
23
+ *
24
+ * - `dial` — elementary Annex-B into a restreamer the consumer dials.
25
+ * - `push` — MPEG-TS demuxed straight into a locally published cam stream.
26
+ * - `fragments` — a fragmented-MP4 byte stream split into an initialisation
27
+ * segment plus `moof`+`mdat` units, delivered IN-PROCESS on a subscribable
28
+ * plane. No url and no cam stream: the consumer holds an object, not an
29
+ * address. Not reachable from `EgressTranscodeRequest` today — see
30
+ * {@link egressTransportFromRequest}.
31
+ */
32
+ export type EgressTransport = 'dial' | 'push' | 'fragments';
33
+ /**
34
+ * Everything about DELIVERY that decides which child a request lands on.
35
+ *
36
+ * It is a second argument rather than fields on `EgressTranscodeRequest`
37
+ * because the `fragments` transport has no cap surface yet: HKSV's delegate and
38
+ * the clip source that will share its fragmenter are both in-process consumers,
39
+ * and putting `fragmentMs` on the request schema now would mean a codegen run
40
+ * and a `@camstack/system` train for a field nothing off-node can call. When
41
+ * that changes, {@link egressTransportFromRequest} is the ONE place that learns
42
+ * to read it, and every sharing decision follows automatically.
43
+ */
44
+ export interface EgressTransportPlan {
45
+ readonly transport: EgressTransport;
46
+ /**
47
+ * Negotiated fragment length, `fragments` only. In the key: two consumers
48
+ * that negotiated different fragment lengths are asking for differently-cut
49
+ * media out of differently-configured muxers, so they cannot share a child.
50
+ */
51
+ readonly fragmentMs?: number;
52
+ }
53
+ /**
54
+ * The transport a CAP request describes. `fragments` is deliberately
55
+ * unreachable from here — the request schema has no way to ask for it, so the
56
+ * cap path can never be handed a fragment child by accident.
57
+ */
58
+ export declare function egressTransportFromRequest(request: EgressTranscodeRequest): EgressTransportPlan;
59
+ interface CanonicalEgressPlan {
60
+ readonly deviceId: number;
61
+ readonly source: string;
62
+ /** See {@link EgressTransport}. */
63
+ readonly transport: EgressTransport;
64
+ /** The negotiated fragment length, or `-1` for a transport without one. */
65
+ readonly fragmentMs: number;
66
+ readonly video: Readonly<Record<string, string | number>>;
67
+ readonly audio: Readonly<Record<string, string | number>>;
68
+ readonly rateControl: string;
69
+ readonly bitstreamFilter: string;
70
+ readonly pixelFormat: string;
71
+ readonly decodeHwAccel: string;
72
+ }
73
+ /**
74
+ * The normalised plan a key is computed from. Exported so a test — and a
75
+ * future operator-facing "why are these two not sharing?" surface — can diff
76
+ * two requests without reversing a hash.
77
+ */
78
+ export declare function canonicalEgressPlan(request: EgressTranscodeRequest, delivery?: EgressTransportPlan): CanonicalEgressPlan;
79
+ /**
80
+ * The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
81
+ * so a request built with a different field order produces the same digest.
82
+ *
83
+ * The handle this keys is IMMUTABLE: there is no `reconfigure`. A consumer
84
+ * whose requirements change RELEASES and re-acquires; the refcount does the
85
+ * rest. That is what stops co-tenants disturbing each other — the co-tenant
86
+ * hazard that made Alexa's shared `derived:alexa-<id>` stream a hazard was
87
+ * exactly a mutable shared object, where one consumer's downgrade dragged
88
+ * every other consumer to 360p.
89
+ */
90
+ export declare function egressTranscodeSharingKey(request: EgressTranscodeRequest, delivery?: EgressTransportPlan): string;
91
+ export {};