@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.
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/canonical-hash-7nfBbEqR.mjs +35 -0
- package/dist/canonical-hash-BcZHRHIx.js +40 -0
- package/dist/cap-call-context.d.ts +26 -0
- package/dist/capabilities/index.d.ts +5 -5
- package/dist/capabilities/notification-rules.cap.d.ts +41 -0
- package/dist/capabilities/pipeline-analytics.cap.d.ts +92 -4
- package/dist/capabilities/pipeline-orchestrator.cap.d.ts +149 -0
- package/dist/capabilities/pipeline-runner.cap.d.ts +119 -1
- package/dist/capabilities/platform-probe.cap.d.ts +3 -3
- package/dist/capabilities/privacy-mask.cap.d.ts +69 -9
- package/dist/capabilities/recording.cap.d.ts +30 -0
- package/dist/capabilities/snapshot.cap.d.ts +1 -1
- package/dist/capabilities/stream-broker.cap.d.ts +304 -0
- package/dist/capabilities/stream-params.cap.d.ts +8 -4
- package/dist/device/device-profile.d.ts +12 -4
- package/dist/device/system-mirror.d.ts +11 -0
- package/dist/encode-profile.d.ts +2 -0
- package/dist/ffmpeg/encode-defaults.d.ts +107 -0
- package/dist/ffmpeg/hwaccel.d.ts +98 -0
- package/dist/ffmpeg/invocation.d.ts +348 -0
- package/dist/ffmpeg/process.d.ts +135 -0
- package/dist/ffmpeg/sharing-key.d.ts +91 -0
- package/dist/generated/addon-api.d.ts +92 -4
- package/dist/generated/device-proxy.d.ts +2 -2
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +2 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2069 -42
- package/dist/index.mjs +2006 -43
- package/dist/interfaces/camera-switches.d.ts +375 -0
- package/dist/interfaces/inference-engine.d.ts +24 -3
- package/dist/interfaces/ops-log.d.ts +4 -0
- package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +270 -36
- package/dist/node.mjs +269 -36
- package/dist/pipeline/native-lease.d.ts +150 -0
- package/dist/{sleep-DTce7-ch.js → sleep-Bx9IIoT0.js} +43 -1
- package/dist/{sleep-CXimb854.mjs → sleep-DtstvzWm.mjs} +38 -2
- package/dist/utils/addon-id.d.ts +30 -0
- 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 {};
|