@camstack/types 1.2.267 → 1.2.269

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 (69) hide show
  1. package/dist/addon.js +4 -4
  2. package/dist/addon.mjs +3 -3
  3. package/dist/capabilities/battery.cap.d.ts +17 -2
  4. package/dist/capabilities/capability-definition.d.ts +30 -1
  5. package/dist/capabilities/composer.cap.d.ts +105 -0
  6. package/dist/capabilities/consumables.cap.d.ts +79 -0
  7. package/dist/capabilities/core-blocks.cap.d.ts +1213 -104
  8. package/dist/capabilities/device-extension.cap.d.ts +12 -13
  9. package/dist/capabilities/device-manager.cap.d.ts +4 -4
  10. package/dist/capabilities/device-state.cap.d.ts +186 -2
  11. package/dist/capabilities/doorbell.cap.d.ts +4 -0
  12. package/dist/capabilities/index.d.ts +3 -3
  13. package/dist/capabilities/media-player.cap.d.ts +4 -4
  14. package/dist/capabilities/recording-archive.cap.d.ts +3 -3
  15. package/dist/catalogs/sensor-active-state.d.ts +25 -1
  16. package/dist/composition/composition-events.d.ts +42 -0
  17. package/dist/composition/composition-graph.d.ts +42 -9
  18. package/dist/composition/composition-items.d.ts +25 -0
  19. package/dist/composition/composition-report.d.ts +115 -12
  20. package/dist/composition/composition.d.ts +162 -3
  21. package/dist/composition/customization-name.d.ts +6 -0
  22. package/dist/composition/evaluate-field.d.ts +20 -0
  23. package/dist/composition/examples.d.ts +33 -0
  24. package/dist/composition/expression-kinds.d.ts +1 -0
  25. package/dist/composition/field-kinds.d.ts +42 -0
  26. package/dist/composition/index.d.ts +2 -0
  27. package/dist/composition/plan-result.d.ts +18 -0
  28. package/dist/composition/state-fields.d.ts +6 -0
  29. package/dist/composition/validate-composition.d.ts +44 -9
  30. package/dist/composition-BAH3n-zq.js +2582 -0
  31. package/dist/composition-D5BQywfe.mjs +2055 -0
  32. package/dist/device/device-context.d.ts +9 -0
  33. package/dist/device/device-runtime-state.d.ts +15 -1
  34. package/dist/device/index.d.ts +1 -1
  35. package/dist/device-extension/slot-descriptor.d.ts +8 -11
  36. package/dist/device-extension/slot-provider.d.ts +0 -2
  37. package/dist/enums/event-category.d.ts +25 -0
  38. package/dist/enums.js +1 -1
  39. package/dist/enums.mjs +1 -1
  40. package/dist/err-msg-DX6i_MY4.mjs +339 -0
  41. package/dist/err-msg-Dx2Kor0g.js +368 -0
  42. package/dist/{event-category-BVDXG4tB.mjs → event-category-C5xZWqz6.mjs} +31 -1
  43. package/dist/{event-category-BVfsrBYA.js → event-category-MbnB-6AN.js} +48 -0
  44. package/dist/expression/ast.d.ts +2 -0
  45. package/dist/expression/builtins.d.ts +9 -0
  46. package/dist/expression/evaluator.d.ts +8 -2
  47. package/dist/expression/expression-source.d.ts +26 -4
  48. package/dist/expression/index.d.ts +6 -6
  49. package/dist/generated/addon-api.d.ts +42 -0
  50. package/dist/generated/device-proxy.d.ts +1 -1
  51. package/dist/generated/method-access-map.d.ts +1 -1
  52. package/dist/generated/method-device-selectors.d.ts +2 -2
  53. package/dist/generated/system-proxy.d.ts +2 -2
  54. package/dist/index.d.ts +7 -6
  55. package/dist/index.js +1687 -1833
  56. package/dist/index.mjs +1359 -1550
  57. package/dist/interfaces/kernel-abstractions.d.ts +16 -0
  58. package/dist/interfaces/recording-config.d.ts +3 -3
  59. package/dist/interfaces/status-overlay.d.ts +42 -0
  60. package/dist/node.d.ts +1 -0
  61. package/dist/node.js +29 -8
  62. package/dist/node.mjs +23 -3
  63. package/dist/{sleep-8K9gue-H.js → sleep-Bg0IB7gg.js} +10 -367
  64. package/dist/{sleep-DC-wdyeS.mjs → sleep-E6eD4yQS.mjs} +11 -344
  65. package/package.json +1 -1
  66. package/dist/canonical-hash-CPK2Dy60.mjs +0 -715
  67. package/dist/canonical-hash-CSE4ioRi.js +0 -792
  68. package/dist/err-msg-COpsHMw2.js +0 -18
  69. package/dist/err-msg-IQTHeDzc.mjs +0 -13
@@ -1,792 +0,0 @@
1
- let node_crypto = require("node:crypto");
2
- //#region src/ffmpeg/invocation.ts
3
- var AUDIO_ENCODER_BY_CODEC = {
4
- opus: "libopus",
5
- aac: "aac",
6
- pcmu: "pcm_mulaw",
7
- pcma: "pcm_alaw"
8
- };
9
- /**
10
- * Camera-microphone audio, per codec. Lives HERE rather than in
11
- * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
12
- * on these types); it is re-exported from there, which is where to read it.
13
- *
14
- * Every source in this repo is a mono camera mic. The former broker preset
15
- * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
16
- * that is the value this consolidation changed.
17
- */
18
- var AUDIO_PRESETS = {
19
- aac: {
20
- kind: "encode",
21
- codec: "aac",
22
- bitrateKbps: 128,
23
- sampleRateHz: 48e3,
24
- channels: 1
25
- },
26
- opus: {
27
- kind: "encode",
28
- codec: "opus",
29
- bitrateKbps: 64,
30
- sampleRateHz: 48e3,
31
- channels: 1
32
- },
33
- pcmu: {
34
- kind: "encode",
35
- codec: "pcmu",
36
- sampleRateHz: 8e3,
37
- channels: 1
38
- }
39
- };
40
- /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
41
- function logBannerArgs(level) {
42
- return [
43
- "-hide_banner",
44
- "-loglevel",
45
- level
46
- ];
47
- }
48
- /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
49
- function isSoftwareDecode(decodeHwAccel) {
50
- return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
51
- }
52
- /**
53
- * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
54
- * appended to this list by a caller — that is the whole point of the function.
55
- */
56
- function buildInputArgs(input, decodeHwAccel) {
57
- const args = [];
58
- if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
59
- if (input.extraArgs?.length) args.push(...input.extraArgs);
60
- if (input.decodeThreadCount !== void 0 && input.decodeThreadCount > 0) args.push("-threads", String(input.decodeThreadCount));
61
- if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
62
- if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
63
- if (input.lowDelay === true) args.push("-flags", "low_delay");
64
- if (input.useWallclockTimestamps === true) args.push("-use_wallclock_as_timestamps", "1");
65
- if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
66
- if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
67
- if (input.rawVideo !== void 0) {
68
- const raw = input.rawVideo;
69
- args.push("-f", "rawvideo", "-pix_fmt", raw.pixelFormat, "-video_size", `${String(raw.width)}x${String(raw.height)}`, "-framerate", String(raw.framerate));
70
- }
71
- args.push("-i", input.url);
72
- return args;
73
- }
74
- /**
75
- * Derive the output `-map` selectors. The three cases, in the order they are
76
- * decided:
77
- *
78
- * 1. **A filter graph** — the picture is the graph's declared video pad, and
79
- * the audio is its audio pad when it has one, otherwise input 0's mic. A
80
- * graph's output is not addressable any other way.
81
- * 2. **N inputs, no graph** — map input 0 EXPLICITLY. ffmpeg's own selection
82
- * picks the "best" stream across ALL inputs, so the egress would silently
83
- * become whichever camera happens to have the largest frame.
84
- * 3. **One input, no graph** — no `-map`. Anything else would change the argv
85
- * of every call site that exists today for no gain.
86
- */
87
- function resolveStreamMaps(selection) {
88
- const graph = selection.filterGraph;
89
- if (graph) return {
90
- video: `[${graph.videoOutLabel}]`,
91
- audio: graph.audioOutLabel ? `[${graph.audioOutLabel}]` : "0:a:0?"
92
- };
93
- if (selection.extraInputs?.length) return {
94
- video: "0:v:0",
95
- audio: "0:a:0?"
96
- };
97
- return {
98
- video: null,
99
- audio: null
100
- };
101
- }
102
- /** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
103
- function buildVideoFilterArgs(scale, outputArgs, filterGraph, hwUpload) {
104
- if (filterGraph) {
105
- if (scale) throw new Error("FfmpegInvocation: video.scale cannot be combined with a filter graph — scale inside the graph instead (the -vf would feed an output the graph already feeds)");
106
- if (outputArgs.some((a) => a === "-vf")) throw new Error("FfmpegInvocation: outputArgs carries a -vf while a filter graph feeds the same output — fold the filter into the graph");
107
- return [];
108
- }
109
- if (outputArgs.some((a) => a === "-vf")) return [];
110
- if (!scale) return hwUpload.length > 0 ? ["-vf", hwUpload] : [];
111
- const suffix = hwUpload.length > 0 ? `,${hwUpload}` : "";
112
- if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}${suffix}`];
113
- return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2${suffix}`];
114
- }
115
- /**
116
- * The upload filter a hardware encode needs, or `''`.
117
- *
118
- * Nothing is uploaded when the DECODE already left the frames on the GPU
119
- * (`-hwaccel vaapi`): they are surfaces already, and uploading a surface is an
120
- * error rather than a no-op.
121
- */
122
- function resolveHwUpload(device, decodeHwAccel) {
123
- if (device === void 0) return "";
124
- if (!isSoftwareDecode(decodeHwAccel)) return "";
125
- return "format=nv12,hwupload";
126
- }
127
- /** Rate-control args for an encode plan. */
128
- function buildRateControlArgs(video) {
129
- const kbps = video.bitrateKbps;
130
- if (kbps === void 0) return [];
131
- const rc = video.rateControl ?? {
132
- kind: "cap",
133
- vbvSeconds: 2
134
- };
135
- const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
136
- return [
137
- ...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
138
- "-maxrate",
139
- `${kbps}k`,
140
- "-bufsize",
141
- `${bufsize}k`
142
- ];
143
- }
144
- /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
145
- function buildVideoArgs(video, outputArgs, filterGraph, hwEncode) {
146
- if (video.kind === "raw") {
147
- if (filterGraph && video.filter !== void 0) throw new Error("FfmpegInvocation: video.filter cannot be combined with a filter graph — fold the filter into the graph");
148
- return [
149
- ...video.filter !== void 0 ? ["-vf", video.filter] : [],
150
- "-c:v",
151
- "rawvideo",
152
- "-pix_fmt",
153
- video.pixelFormat,
154
- ...video.fps !== void 0 ? ["-r", String(video.fps)] : []
155
- ];
156
- }
157
- if (video.kind === "copy") {
158
- if (filterGraph) throw new Error("FfmpegInvocation: a filter graph produces a NEW picture, so video.kind must be `encode`");
159
- return [
160
- "-c:v",
161
- "copy",
162
- ...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
163
- ];
164
- }
165
- const hwUpload = hwEncode === void 0 ? "" : resolveHwUpload(hwEncode.device, hwEncode.decodeHwAccel);
166
- if (hwEncode !== void 0 && filterGraph) throw new Error("FfmpegInvocation: a hardware encode fed by a filter graph must upload INSIDE the graph — hwEncodeDevice cannot be combined with filterGraph");
167
- const args = [
168
- ...buildVideoFilterArgs(video.scale, outputArgs, filterGraph, hwUpload),
169
- "-c:v",
170
- video.encoder
171
- ];
172
- if (video.preset !== void 0) args.push("-preset", video.preset);
173
- if (video.tune !== void 0) args.push("-tune", video.tune);
174
- if (video.singleSlicePerFrame === true && video.encoder === "libx264") args.push("-x264-params", "sliced-threads=0");
175
- if (video.profile !== void 0) args.push("-profile:v", video.profile);
176
- if (video.level !== void 0) args.push("-level", video.level);
177
- if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
178
- if (video.fps !== void 0) args.push("-r", String(video.fps));
179
- if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
180
- if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
181
- if (video.bf !== void 0) args.push("-bf", String(video.bf));
182
- args.push(...buildRateControlArgs(video));
183
- if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
184
- return args;
185
- }
186
- /** The whole audio block, after `-i`. */
187
- function buildAudioArgs(audio) {
188
- if (audio.kind === "none") return ["-an"];
189
- if (audio.kind === "copy") return ["-c:a", "copy"];
190
- const args = [];
191
- if (audio.filter !== void 0) args.push("-af", audio.filter);
192
- args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
193
- if (audio.application !== void 0) args.push("-application", audio.application);
194
- if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
195
- if (audio.globalHeader === true) args.push("-flags", "+global_header");
196
- if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
197
- if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
198
- if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
199
- if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
200
- return args;
201
- }
202
- /** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
203
- function buildRtpOutputArgs(out) {
204
- const args = [];
205
- if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
206
- if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
207
- if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
208
- args.push("-f", "rtp", out.url);
209
- return args;
210
- }
211
- /** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
212
- function isElementaryVideoSink(sink) {
213
- return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
214
- }
215
- /**
216
- * The fragmented-MP4 muxer flags, in the order the recorder has proven them
217
- * (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
218
- * `-segment_format_options`, across every vendor in the fleet):
219
- *
220
- * - `frag_keyframe` — cut a fragment at each key frame, so every fragment
221
- * opens on a sync sample. HKSV's whole requirement.
222
- * - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
223
- * is what makes the head a standalone INITIALISATION segment.
224
- * - `default_base_moof` — fragment offsets are self-relative, so a fragment is
225
- * demuxable without the bytes that preceded it. D31's byte-range read path
226
- * depends on exactly this property of the recorder's segments.
227
- */
228
- var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
229
- /**
230
- * The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
231
- * union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
232
- * which is what a plain `container` read would have done for `mp4` — a valid
233
- * argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
234
- * unusable byte stream.
235
- */
236
- function buildStdoutOrRtspSinkArgs(sink) {
237
- if (sink.kind === "rtsp-listen") return [
238
- "-f",
239
- "rtsp",
240
- "-rtsp_transport",
241
- "tcp",
242
- "-rtsp_flags",
243
- "listen",
244
- sink.url
245
- ];
246
- if (sink.kind === "rtp-outputs") return [];
247
- return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
248
- "-f",
249
- sink.container,
250
- "pipe:1"
251
- ];
252
- }
253
- /**
254
- * How far BELOW the negotiated fragment length `-min_frag_duration` is set.
255
- *
256
- * `-min_frag_duration` refuses to cut before that much media has accumulated,
257
- * and then waits for the next key frame. Set to exactly `fragmentMs`, the
258
- * commonest camera configuration in existence — a key-frame grid EQUAL to the
259
- * requested fragment length — lands the deadline on the same instant as the key
260
- * frame, loses the race, and skips to the following one: **every fragment comes
261
- * out at twice the requested length.**
262
- *
263
- * Measured on the live fleet 2026-08-07, camera 615, `-c:v copy` (D84):
264
- *
265
- * | slot | GOP | `-min_frag_duration` | median gap |
266
- * | --- | --- | --- | --- |
267
- * | 1280×720 | 40 f @ 10 fps = 4.0 s | 4000 ms | **7944 ms** |
268
- * | 1280×720 | 40 f @ 10 fps = 4.0 s | 3600 ms | 3973 ms |
269
- * | 3840×2160 | 100 f @ 25 fps = 4.0 s | 4000 ms | 8042 ms |
270
- * | 3840×2160 | 100 f @ 25 fps = 4.0 s | 3600 ms | 3998 ms |
271
- *
272
- * A doubled fragment is not a cosmetic overshoot: HKSV requires every fragment
273
- * to be no longer than the length the controller SELECTED, so the shipped-but-
274
- * inert phase-1 sink would have violated the contract on its first real clip.
275
- *
276
- * 10 % is chosen against the two failures either side of it. Too small and
277
- * ordinary jitter (measured spread 3953-4096 ms) re-loses the race; too large
278
- * and a source with a key frame slightly EARLY than the grid gets cut there,
279
- * yielding a short fragment for no reason.
280
- */
281
- var FMP4_MIN_FRAG_MARGIN = .9;
282
- /** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
283
- function buildFmp4SinkArgs(sink) {
284
- return [
285
- "-movflags",
286
- FMP4_MOVFLAGS,
287
- "-min_frag_duration",
288
- String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
289
- "-f",
290
- "mp4",
291
- "pipe:1"
292
- ];
293
- }
294
- /**
295
- * A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
296
- * audio optional so a source with no audio skips it instead of failing the
297
- * whole invocation.
298
- */
299
- function buildAudioSidecarArgs(sidecar) {
300
- return [
301
- "-map",
302
- "0:a:0?",
303
- ...buildAudioArgs(sidecar.codec === "pcma" ? {
304
- kind: "encode",
305
- codec: "pcma",
306
- sampleRateHz: 8e3,
307
- channels: 1
308
- } : AUDIO_PRESETS[sidecar.codec]),
309
- ...buildRtpOutputArgs({
310
- url: sidecar.rtpUrl,
311
- sdpFile: sidecar.sdpFile
312
- })
313
- ];
314
- }
315
- /**
316
- * Assemble the full ffmpeg argument list. Layout:
317
- *
318
- * -hide_banner -loglevel <level>
319
- * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
320
- * [<input.extraArgs>] │
321
- * [-fflags <flag>…] │
322
- * [-rtsp_transport tcp] │
323
- * -i <url> ─┘ …repeated per extraInput
324
- * [-filter_complex <graph>] ─ global, after the LAST -i
325
- * [-map <video>] <video block> <threads> ─┐ OUTPUT options.
326
- * [-map <audio>] <audio block> │
327
- * <consumer outputArgs verbatim> │
328
- * <sink> ─┘ terminal
329
- *
330
- * The `-map`s are DERIVED (see {@link resolveStreamMaps}), never constants:
331
- * one input with no graph emits none at all, which is why the argv of every
332
- * call site that predates the N-input extension is unchanged to the byte.
333
- *
334
- * {@link FfmpegInvocation.audioSidecar} is the one map that stays literal
335
- * (`0:a:0?`): the sidecar is defined as the SOURCE's microphone on its own RTP
336
- * output, not as whatever the main output happens to carry.
337
- */
338
- /** The hardware-encode context for this invocation, or `undefined`. */
339
- function hwEncodeFor(inv) {
340
- return inv.hwEncodeDevice === void 0 ? void 0 : {
341
- device: inv.hwEncodeDevice,
342
- decodeHwAccel: inv.decodeHwAccel
343
- };
344
- }
345
- function buildFfmpegArgs(inv) {
346
- const graph = inv.filterGraph ?? null;
347
- const head = [
348
- ...logBannerArgs(inv.logLevel),
349
- ...inv.hwEncodeDevice !== void 0 ? ["-vaapi_device", inv.hwEncodeDevice.renderNode] : [],
350
- ...buildInputArgs(inv.input, inv.decodeHwAccel),
351
- ...(inv.extraInputs ?? []).flatMap((extra) => buildInputArgs(extra, inv.decodeHwAccel)),
352
- ...graph ? ["-filter_complex", graph.graph] : []
353
- ];
354
- const threadArgs = [...inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [], ...inv.filterThreadCount !== void 0 && inv.filterThreadCount > 0 ? ["-filter_threads", String(inv.filterThreadCount)] : []];
355
- const maps = resolveStreamMaps(inv);
356
- const videoMapArgs = maps.video ? ["-map", maps.video] : [];
357
- const audioMapArgs = maps.audio ? ["-map", maps.audio] : [];
358
- if (inv.sink.kind === "rtp-outputs") {
359
- const videoLeg = inv.sink.video ? [
360
- "-an",
361
- "-map",
362
- maps.video ?? "0:v:0",
363
- ...buildVideoArgs(inv.video, inv.outputArgs, graph, hwEncodeFor(inv)),
364
- ...threadArgs,
365
- ...inv.outputArgs,
366
- ...buildRtpOutputArgs(inv.sink.video)
367
- ] : [];
368
- const audioLeg = inv.sink.audio ? [
369
- "-vn",
370
- "-map",
371
- maps.audio ?? "0:a:0?",
372
- ...buildAudioArgs(inv.audio),
373
- ...buildRtpOutputArgs(inv.sink.audio)
374
- ] : [];
375
- return [
376
- ...head,
377
- ...videoLeg,
378
- ...audioLeg
379
- ];
380
- }
381
- const mutedAudio = isElementaryVideoSink(inv.sink) || inv.audio.kind === "none";
382
- const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
383
- return [
384
- ...head,
385
- ...videoMapArgs,
386
- ...buildVideoArgs(inv.video, inv.outputArgs, graph, hwEncodeFor(inv)),
387
- ...threadArgs,
388
- ...mutedAudio ? [] : audioMapArgs,
389
- ...audioArgs,
390
- ...inv.outputArgs,
391
- ...buildStdoutOrRtspSinkArgs(inv.sink),
392
- ...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
393
- ];
394
- }
395
- /**
396
- * Hardware ENCODER ids per decode-hwaccel backend — a static, deterministic
397
- * map (the same shape the reference NVR uses: platform → encoder, no probe).
398
- */
399
- var ENCODER_IDS_BY_BACKEND = {
400
- videotoolbox: {
401
- h264: "h264_videotoolbox",
402
- h265: "hevc_videotoolbox"
403
- },
404
- vaapi: {
405
- h264: "h264_vaapi",
406
- h265: "hevc_vaapi"
407
- },
408
- qsv: {
409
- h264: "h264_qsv",
410
- h265: "hevc_qsv"
411
- },
412
- cuda: {
413
- h264: "h264_nvenc",
414
- h265: "hevc_nvenc"
415
- },
416
- nvdec: {
417
- h264: "h264_nvenc",
418
- h265: "hevc_nvenc"
419
- },
420
- amf: {
421
- h264: "h264_amf",
422
- h265: "hevc_amf"
423
- }
424
- };
425
- /**
426
- * The hardware encoder for a target codec on `backend`, or the software one.
427
- * `'auto'` is NOT a backend identity (it is an instruction to ffmpeg), so it
428
- * maps to software encoding.
429
- */
430
- function pickVideoEncoder(target, backend, useHardware) {
431
- const software = target === "h264" ? "libx264" : "libx265";
432
- if (!useHardware || backend === null || isSoftwareDecode(backend) || backend === "auto") return software;
433
- const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
434
- if (!ids) return software;
435
- return target === "h264" ? ids.h264 : ids.h265;
436
- }
437
- /** Map an `EncodeProfile.audio` to an audio plan. */
438
- function audioPlanFromEncodeProfile(audio) {
439
- if (audio === "passthrough") return { kind: "none" };
440
- if (audio.codec === "copy") return { kind: "copy" };
441
- return {
442
- kind: "encode",
443
- codec: audio.codec,
444
- ...audio.bitrateKbps !== void 0 ? { bitrateKbps: audio.bitrateKbps } : {},
445
- ...audio.sampleRateHz !== void 0 ? { sampleRateHz: audio.sampleRateHz } : {},
446
- ...audio.channels !== void 0 ? { channels: audio.channels } : {}
447
- };
448
- }
449
- /**
450
- * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
451
- * {@link FfmpegInvocation}. This is the ONLY bridge between the two models —
452
- * a second one is how the repo grew two argv builders that disagreed about
453
- * hardware.
454
- *
455
- * Smart video copy: when the source already speaks the requested codec the
456
- * encode block is elided entirely and ffmpeg runs as a re-muxer on the video
457
- * plane. Width / height / fps / bitrate in the profile are a downstream BUDGET,
458
- * not a forced rescale.
459
- */
460
- function invocationFromEncodeProfile(input) {
461
- const v = input.profile.video;
462
- const shouldCopy = v.codec === "copy" || input.forceReencode !== true && v.codec === input.sourceCodec;
463
- const scale = v.width !== void 0 && v.height !== void 0 ? {
464
- mode: "fit",
465
- width: v.width,
466
- height: v.height
467
- } : null;
468
- const target = v.codec === "h265" ? "h265" : "h264";
469
- const video = shouldCopy ? {
470
- kind: "copy",
471
- ...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
472
- } : {
473
- kind: "encode",
474
- encoder: pickVideoEncoder(target, input.decodeHwAccel, input.hardwareEncoders === true),
475
- scale,
476
- ...v.preset !== void 0 ? { preset: v.preset } : {},
477
- ...v.tune !== void 0 ? { tune: v.tune } : {},
478
- ...v.singleSlicePerFrame !== void 0 ? { singleSlicePerFrame: v.singleSlicePerFrame } : {},
479
- ...v.profile !== void 0 ? { profile: v.profile } : {},
480
- ...v.level !== void 0 ? { level: v.level } : {},
481
- ...input.pixelFormat !== void 0 ? { pixelFormat: input.pixelFormat } : {},
482
- ...v.fps !== void 0 ? { fps: v.fps } : {},
483
- ...v.gopFrames !== void 0 ? { gopFrames: v.gopFrames } : {},
484
- ...input.forceKeyFramesSeconds !== void 0 ? { forceKeyFramesSeconds: input.forceKeyFramesSeconds } : {},
485
- ...v.bf !== void 0 ? { bf: v.bf } : {},
486
- ...v.bitrateKbps !== void 0 ? { bitrateKbps: v.bitrateKbps } : {},
487
- ...input.rateControl !== void 0 ? { rateControl: input.rateControl } : {},
488
- ...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
489
- };
490
- return {
491
- logLevel: input.logLevel ?? "error",
492
- decodeHwAccel: input.decodeHwAccel,
493
- input: {
494
- url: input.sourceUrl,
495
- rtspTransport: "tcp",
496
- fflags: ["+discardcorrupt"],
497
- ...input.profile.inputArgs?.length ? { extraArgs: input.profile.inputArgs } : {}
498
- },
499
- video,
500
- audio: audioPlanFromEncodeProfile(input.profile.audio),
501
- threadCount: input.threadCount ?? 0,
502
- outputArgs: input.profile.outputArgs ?? [],
503
- sink: input.sink,
504
- ...input.audioSidecar !== void 0 ? { audioSidecar: input.audioSidecar } : {}
505
- };
506
- }
507
- //#endregion
508
- //#region src/ffmpeg/fmp4-box-splitter.ts
509
- var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
510
- /** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
511
- var BOX_HEADER_BYTES = 8;
512
- var LARGE_BOX_HEADER_BYTES = 16;
513
- var Fmp4BoxSplitter = class {
514
- maxUnitBytes;
515
- /** Bytes of the CURRENT unit plus any partial box after it. */
516
- buffer = new Uint8Array(0);
517
- /** Where the current unit starts inside {@link buffer}. */
518
- unitStart = 0;
519
- /** Where the box scanner has reached inside {@link buffer}. */
520
- cursor = 0;
521
- state = "init";
522
- nextSequence = 0;
523
- faultReason = null;
524
- interstitial = /* @__PURE__ */ new Set();
525
- constructor(options = {}) {
526
- this.maxUnitBytes = options.maxUnitBytes ?? DEFAULT_MAX_UNIT_BYTES;
527
- }
528
- /**
529
- * Non-null once the stream cannot be split. The splitter emits nothing
530
- * further, so a caller polls this to kill the child rather than watching a
531
- * silent stall — a fragmenter that quietly stops producing looks exactly like
532
- * a camera with no motion.
533
- */
534
- get fault() {
535
- return this.faultReason;
536
- }
537
- /** Bytes currently held. The memory bound, observable rather than asserted. */
538
- get pendingBytes() {
539
- return this.buffer.length - this.unitStart;
540
- }
541
- /**
542
- * Top-level box types seen BETWEEN fragments and discarded — `mfra`, `free`,
543
- * a stray `sidx`. Reported rather than dropped in silence: they are legal and
544
- * useless to a fragment consumer, but a type nobody expected showing up here
545
- * is the first symptom of a muxer that is not writing what we think it is.
546
- */
547
- get discardedInterstitialTypes() {
548
- return [...this.interstitial];
549
- }
550
- /**
551
- * Feed bytes; get back whatever units completed. Returns `[]` once faulted.
552
- */
553
- push(chunk) {
554
- if (this.faultReason !== null || chunk.length === 0) return [];
555
- this.append(chunk);
556
- if (this.pendingBytes > this.maxUnitBytes) return this.fail(`a single fMP4 unit exceeded ${this.maxUnitBytes} bytes — this stream is not fragmented`);
557
- return this.drainBoxes();
558
- }
559
- append(chunk) {
560
- if (this.buffer.length === 0) {
561
- this.buffer = chunk.slice();
562
- return;
563
- }
564
- const next = new Uint8Array(this.buffer.length + chunk.length);
565
- next.set(this.buffer, 0);
566
- next.set(chunk, this.buffer.length);
567
- this.buffer = next;
568
- }
569
- /** Consume every COMPLETE top-level box now in the buffer. */
570
- drainBoxes() {
571
- const units = [];
572
- for (;;) {
573
- const header = this.readHeader();
574
- if (this.faultReason !== null) return units;
575
- if (header === null) break;
576
- if (this.cursor + header.totalBytes > this.buffer.length) break;
577
- const boxStart = this.cursor;
578
- const boxEnd = boxStart + header.totalBytes;
579
- this.cursor = boxEnd;
580
- const unit = this.consumeBox(header.type, boxStart, boxEnd);
581
- if (this.faultReason !== null) return units;
582
- if (unit !== null) units.push(unit);
583
- }
584
- this.compact();
585
- return units;
586
- }
587
- /**
588
- * Apply one box to the state machine. Returns a unit when this box CLOSED
589
- * one, `null` otherwise.
590
- */
591
- consumeBox(type, boxStart, boxEnd) {
592
- if (this.state === "init") {
593
- if (type !== "moof") return null;
594
- if (boxStart === this.unitStart) {
595
- this.fail("a moof arrived before any initialisation box — there is no ftyp/moov to send");
596
- return null;
597
- }
598
- const init = this.emit("init", this.unitStart, boxStart);
599
- this.unitStart = boxStart;
600
- this.state = "fragment";
601
- return init;
602
- }
603
- if (this.state === "idle") {
604
- if (type !== "moof") {
605
- this.interstitial.add(type);
606
- this.unitStart = boxEnd;
607
- return null;
608
- }
609
- this.unitStart = boxStart;
610
- this.state = "fragment";
611
- return null;
612
- }
613
- if (type !== "mdat") return null;
614
- const fragment = this.emit("fragment", this.unitStart, boxEnd);
615
- this.unitStart = boxEnd;
616
- this.state = "idle";
617
- return fragment;
618
- }
619
- /**
620
- * Parse the header at {@link cursor}, or `null` when too few bytes have
621
- * arrived to know. Faults on a size the splitter cannot honour.
622
- */
623
- readHeader() {
624
- const available = this.buffer.length - this.cursor;
625
- if (available < BOX_HEADER_BYTES) return null;
626
- const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
627
- const size = view.getUint32(this.cursor);
628
- const type = String.fromCharCode(this.buffer[this.cursor + 4] ?? 0, this.buffer[this.cursor + 5] ?? 0, this.buffer[this.cursor + 6] ?? 0, this.buffer[this.cursor + 7] ?? 0);
629
- if (size === 0) {
630
- this.fail(`box "${type}" declares size 0 (to EOF) — an unbounded box cannot be fragmented`);
631
- return null;
632
- }
633
- if (size === 1) {
634
- if (available < LARGE_BOX_HEADER_BYTES) return null;
635
- const large = view.getBigUint64(this.cursor + BOX_HEADER_BYTES);
636
- if (large > BigInt(this.maxUnitBytes)) {
637
- this.fail(`box "${type}" declares ${large} bytes, over the ${this.maxUnitBytes} byte bound`);
638
- return null;
639
- }
640
- return {
641
- type,
642
- totalBytes: Number(large)
643
- };
644
- }
645
- if (size < BOX_HEADER_BYTES) {
646
- this.fail(`box "${type}" declares an impossible size of ${size} bytes`);
647
- return null;
648
- }
649
- return {
650
- type,
651
- totalBytes: size
652
- };
653
- }
654
- emit(kind, start, end) {
655
- const sequence = this.nextSequence;
656
- this.nextSequence += 1;
657
- return {
658
- kind,
659
- data: this.buffer.slice(start, end),
660
- sequence
661
- };
662
- }
663
- /**
664
- * Drop everything already emitted or discarded. Without this the buffer is
665
- * the whole stream and the process dies in hours, not minutes.
666
- */
667
- compact() {
668
- if (this.unitStart === 0) return;
669
- this.buffer = this.buffer.slice(this.unitStart);
670
- this.cursor -= this.unitStart;
671
- this.unitStart = 0;
672
- }
673
- fail(reason) {
674
- this.faultReason = reason;
675
- this.buffer = new Uint8Array(0);
676
- this.unitStart = 0;
677
- this.cursor = 0;
678
- return [];
679
- }
680
- };
681
- //#endregion
682
- //#region src/utils/canonical-hash.ts
683
- /**
684
- * Deterministic SHA-256 hash of an arbitrary serialisable value. The
685
- * canonical form sorts object keys alphabetically at every depth so two
686
- * structurally-equal inputs with different key insertion orders produce
687
- * the same hash. Returns a 64-char lowercase hex digest.
688
- *
689
- * Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
690
- * accessory-rebuild work when the upstream shape is byte-identical to
691
- * the last applied state — preventing user-visible "re-discovery"
692
- * notifications on every addon-runner respawn. Each respawn re-fires
693
- * `DeviceBindingsChanged` for every cap registration, which without
694
- * this guard would propagate redundant pushes.
695
- *
696
- * Note: this is a SYMPTOMATIC fix layered on top of the binding-change
697
- * subscription. The proper fix is a single "device ready" lifecycle
698
- * barrier so exports react only when the full cap set has landed —
699
- * tracked separately for post-HA-integration work.
700
- */
701
- function canonicalHash(value) {
702
- const canonical = JSON.stringify(value, replaceWithSortedKeys);
703
- return (0, node_crypto.createHash)("sha256").update(canonical ?? "").digest("hex");
704
- }
705
- function replaceWithSortedKeys(_key, value) {
706
- if (value && typeof value === "object" && !Array.isArray(value)) {
707
- const obj = value;
708
- const out = {};
709
- for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
710
- return out;
711
- }
712
- return value;
713
- }
714
- //#endregion
715
- Object.defineProperty(exports, "AUDIO_PRESETS", {
716
- enumerable: true,
717
- get: function() {
718
- return AUDIO_PRESETS;
719
- }
720
- });
721
- Object.defineProperty(exports, "Fmp4BoxSplitter", {
722
- enumerable: true,
723
- get: function() {
724
- return Fmp4BoxSplitter;
725
- }
726
- });
727
- Object.defineProperty(exports, "audioPlanFromEncodeProfile", {
728
- enumerable: true,
729
- get: function() {
730
- return audioPlanFromEncodeProfile;
731
- }
732
- });
733
- Object.defineProperty(exports, "buildAudioArgs", {
734
- enumerable: true,
735
- get: function() {
736
- return buildAudioArgs;
737
- }
738
- });
739
- Object.defineProperty(exports, "buildFfmpegArgs", {
740
- enumerable: true,
741
- get: function() {
742
- return buildFfmpegArgs;
743
- }
744
- });
745
- Object.defineProperty(exports, "buildInputArgs", {
746
- enumerable: true,
747
- get: function() {
748
- return buildInputArgs;
749
- }
750
- });
751
- Object.defineProperty(exports, "buildVideoArgs", {
752
- enumerable: true,
753
- get: function() {
754
- return buildVideoArgs;
755
- }
756
- });
757
- Object.defineProperty(exports, "canonicalHash", {
758
- enumerable: true,
759
- get: function() {
760
- return canonicalHash;
761
- }
762
- });
763
- Object.defineProperty(exports, "invocationFromEncodeProfile", {
764
- enumerable: true,
765
- get: function() {
766
- return invocationFromEncodeProfile;
767
- }
768
- });
769
- Object.defineProperty(exports, "isSoftwareDecode", {
770
- enumerable: true,
771
- get: function() {
772
- return isSoftwareDecode;
773
- }
774
- });
775
- Object.defineProperty(exports, "logBannerArgs", {
776
- enumerable: true,
777
- get: function() {
778
- return logBannerArgs;
779
- }
780
- });
781
- Object.defineProperty(exports, "pickVideoEncoder", {
782
- enumerable: true,
783
- get: function() {
784
- return pickVideoEncoder;
785
- }
786
- });
787
- Object.defineProperty(exports, "resolveStreamMaps", {
788
- enumerable: true,
789
- get: function() {
790
- return resolveStreamMaps;
791
- }
792
- });