@camstack/types 1.2.41 → 1.2.42

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 (34) 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/capabilities/index.d.ts +2 -2
  6. package/dist/capabilities/notification-rules.cap.d.ts +41 -0
  7. package/dist/capabilities/pipeline-analytics.cap.d.ts +92 -4
  8. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +123 -0
  9. package/dist/capabilities/pipeline-runner.cap.d.ts +119 -1
  10. package/dist/capabilities/platform-probe.cap.d.ts +3 -3
  11. package/dist/capabilities/recording.cap.d.ts +3 -0
  12. package/dist/capabilities/stream-broker.cap.d.ts +300 -0
  13. package/dist/encode-profile.d.ts +2 -0
  14. package/dist/ffmpeg/encode-defaults.d.ts +89 -0
  15. package/dist/ffmpeg/hwaccel.d.ts +98 -0
  16. package/dist/ffmpeg/invocation.d.ts +250 -0
  17. package/dist/ffmpeg/process.d.ts +135 -0
  18. package/dist/ffmpeg/sharing-key.d.ts +39 -0
  19. package/dist/generated/addon-api.d.ts +60 -4
  20. package/dist/generated/device-proxy.d.ts +1 -1
  21. package/dist/generated/method-access-map.d.ts +1 -1
  22. package/dist/generated/system-proxy.d.ts +2 -2
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +1354 -20
  25. package/dist/index.mjs +1316 -21
  26. package/dist/interfaces/camera-switches.d.ts +217 -0
  27. package/dist/interfaces/ops-log.d.ts +4 -0
  28. package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
  29. package/dist/node.d.ts +2 -0
  30. package/dist/node.js +270 -36
  31. package/dist/node.mjs +269 -36
  32. package/dist/{sleep-CXimb854.mjs → sleep-BmNKsY7v.mjs} +5 -0
  33. package/dist/{sleep-DTce7-ch.js → sleep-Cvi1JxZp.js} +5 -0
  34. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Decode hardware acceleration for an EGRESS transcode — resolved through the
3
+ * ONE ranking that is known to work on this hardware.
4
+ *
5
+ * ## Why this file exists
6
+ *
7
+ * The repo has two hwaccel opinions and they disagree in the direction that
8
+ * breaks things:
9
+ *
10
+ * - the raw kernel resolver (`ctx.kernel.hwaccel.resolve()`) returns
11
+ * `['qsv','vaapi']` for an Intel host — qsv FIRST;
12
+ * - `addon-decoder-ffmpeg`'s `DECODE_HWACCEL_RANK` puts **vaapi above qsv on
13
+ * purpose**, because on this exact hub a qsv decode child exits `code=171`
14
+ * producing no frames, while vaapi decodes 8 MP h264 at ~50 fps.
15
+ *
16
+ * **The decoder addon is the authority.** It publishes its answer per node as
17
+ * the `probedBestHwaccel@<node>` setting and exposes it on the `decoder` cap's
18
+ * `getInfo` — the same value `AgentLoadService.readNodeDecodeHwaccel` reads to
19
+ * show per-node hwaccel in the UI. This module reads THAT, and deliberately
20
+ * never re-derives a ranking: a second copy of the rank table is exactly how
21
+ * the two opinions appeared. Addons may not import each other, so the read is
22
+ * a `ctx.api.decoder.getInfo` call injected as {@link EgressHwAccelDeps.readDecoderBackend}.
23
+ *
24
+ * ## Why the kernel resolver is still a dependency here
25
+ *
26
+ * It is only ever used to say something in a log line. It is never allowed to
27
+ * SELECT a backend: if the decoder cannot answer, this resolver returns
28
+ * software rather than adopting the kernel's qsv-first order, because that
29
+ * order is the known-bad one. `never silently downgrades` in the spec is the
30
+ * test that holds this.
31
+ *
32
+ * ## The failure mode this must not reproduce
33
+ *
34
+ * A silent fall back to software decode is a FLOW bug on this hub, not a
35
+ * capability limit (vaapi works — 10 concurrent 4K decodes pass). So every
36
+ * path that ends in software calls {@link EgressHwAccelDeps.onFallback}, which
37
+ * the caller logs at `warn` with `tags: { deviceId }`. Silence reads as
38
+ * "hardware was used".
39
+ */
40
+ /** Why a resolve ended in software decode. */
41
+ export type EgressHwAccelFallbackReason =
42
+ /** The `decoder` cap could not be reached (offline node, cap not mounted). */
43
+ 'decoder-unreadable'
44
+ /** The decoder answered, but has not probed a backend on this node yet. */
45
+ | 'decoder-unprobed';
46
+ export interface EgressHwAccelFallback {
47
+ readonly reason: EgressHwAccelFallbackReason;
48
+ /** What the kernel resolver would have said — for the log line ONLY. */
49
+ readonly kernelPreferred: readonly string[];
50
+ readonly error?: string;
51
+ }
52
+ /**
53
+ * A bounded memo so a per-session resolve is not a per-session cross-process
54
+ * cap call. Owned by the caller (one per broker / exporter), never a module
55
+ * global — a module global would outlive an addon respawn and survive an
56
+ * operator changing the decoder backend.
57
+ */
58
+ export interface HwAccelCache {
59
+ read(): string | null | undefined;
60
+ write(value: string | null): void;
61
+ }
62
+ export interface HwAccelCacheOptions {
63
+ readonly ttlMs: number;
64
+ readonly now?: () => number;
65
+ }
66
+ export declare function createHwAccelCache(options: HwAccelCacheOptions): HwAccelCache;
67
+ export interface EgressHwAccelDeps {
68
+ /**
69
+ * `ctx.api.decoder.getInfo.query(undefined, nodePin(nodeId)).probedBestHwaccel`
70
+ * — the DECODER ADDON's own answer for THIS node, already re-ranked through
71
+ * `DECODE_HWACCEL_RANK`. Throw or return `''` when it is not knowable.
72
+ */
73
+ readDecoderBackend(): Promise<string>;
74
+ /**
75
+ * `ctx.kernel.hwaccel.resolve().preferred` — reported in the fallback log so
76
+ * an operator can see the disagreement. NEVER used to select a backend.
77
+ */
78
+ readKernelPreferred(): Promise<readonly string[]>;
79
+ /** Called on every path that ends in software. The caller logs at `warn`. */
80
+ onFallback(fallback: EgressHwAccelFallback): void;
81
+ /**
82
+ * An explicit operator choice (`decoder.hwaccel`, or a per-consumer pin).
83
+ * `'auto'` is passed through as the literal ffmpeg instruction; `'none'` /
84
+ * `'copy'` force software; anything else pins that backend. Absent ⇒ ask the
85
+ * decoder.
86
+ */
87
+ readonly override?: string | null;
88
+ readonly cache?: HwAccelCache;
89
+ }
90
+ /**
91
+ * Resolve the `-hwaccel` value for an egress transcode. Hardware is the
92
+ * DEFAULT — an egress that decodes in software on a hub with working vaapi is
93
+ * paying for nothing — and every software outcome is announced.
94
+ *
95
+ * Returns a concrete backend name, the literal `'auto'`, or `null` for
96
+ * software decode (⇒ {@link buildFfmpegArgs} emits no `-hwaccel` at all).
97
+ */
98
+ export declare function resolveEgressDecodeHwAccel(deps: EgressHwAccelDeps): Promise<string | null>;
@@ -0,0 +1,250 @@
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 { EncodeProfile, AudioEncode } 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
+ /** Caller-supplied input options (an `EncodeProfile.inputArgs`, probe flags). */
38
+ readonly extraArgs?: readonly string[];
39
+ }
40
+ /** ffmpeg video encoder ids the repo selects between. */
41
+ 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';
42
+ /**
43
+ * How the encoder is bounded.
44
+ *
45
+ * - `'cbr'` — a TARGET bitrate plus a matching peak (`-b:v` + `-maxrate`).
46
+ * What a real-time egress with a fixed budget wants.
47
+ * - `'cap'` — a CEILING only (`-maxrate`, no `-b:v`): the encoder spends less
48
+ * on an easy scene but never exceeds the bound.
49
+ *
50
+ * `vbvSeconds` is the VBV window as a multiple of the bitrate. One second is a
51
+ * tight window that keeps instantaneous rate near the target (HomeKit's link
52
+ * budget is per-second); two seconds lets a keyframe spike borrow from the next
53
+ * second, which is what a browser viewer wants.
54
+ */
55
+ export type FfmpegRateControl = {
56
+ readonly kind: 'cbr';
57
+ readonly vbvSeconds: number;
58
+ } | {
59
+ readonly kind: 'cap';
60
+ readonly vbvSeconds: number;
61
+ };
62
+ /** Fit-inside (never upscale, even dimensions) vs an exact rescale. */
63
+ export interface FfmpegScalePlan {
64
+ readonly mode: 'fit' | 'exact';
65
+ readonly width: number;
66
+ readonly height: number;
67
+ }
68
+ export interface FfmpegVideoEncodePlan {
69
+ readonly kind: 'encode';
70
+ readonly encoder: FfmpegVideoEncoderId;
71
+ readonly scale?: FfmpegScalePlan | null;
72
+ readonly preset?: string;
73
+ readonly tune?: string;
74
+ /** `-profile:v` — baseline / main / high. */
75
+ readonly profile?: string;
76
+ /**
77
+ * `-level`. Omitting this while advertising a level in SDP is the defect
78
+ * that kept HomeKit black for a year and that Alexa shipped too: a stream
79
+ * that does not match its own advertisement.
80
+ */
81
+ readonly level?: string;
82
+ readonly pixelFormat?: string;
83
+ readonly fps?: number;
84
+ readonly gopFrames?: number;
85
+ readonly bf?: number;
86
+ readonly bitrateKbps?: number;
87
+ readonly rateControl?: FfmpegRateControl;
88
+ /** `-bsf:v`, e.g. `dump_extra` when the consumer negotiates its own SDP. */
89
+ readonly bitstreamFilter?: string;
90
+ }
91
+ export interface FfmpegVideoCopyPlan {
92
+ readonly kind: 'copy';
93
+ readonly bitstreamFilter?: string;
94
+ }
95
+ export type FfmpegVideoPlan = FfmpegVideoCopyPlan | FfmpegVideoEncodePlan;
96
+ export type FfmpegAudioCodecId = 'opus' | 'aac' | 'pcmu' | 'pcma';
97
+ export interface FfmpegAudioEncodePlan {
98
+ readonly kind: 'encode';
99
+ readonly codec: FfmpegAudioCodecId;
100
+ readonly bitrateKbps?: number;
101
+ readonly sampleRateHz?: number;
102
+ readonly channels?: number;
103
+ /** libopus `-application` — `lowdelay` for a real-time two-way leg. */
104
+ readonly application?: 'lowdelay' | 'voip' | 'audio';
105
+ /** libopus `-frame_duration` in ms — negotiated per consumer session. */
106
+ readonly frameDurationMs?: number;
107
+ /** `-bufsize` for the audio plane, in kbit. */
108
+ readonly vbvBufferKbits?: number;
109
+ /** `-flags +global_header` — required when the consumer owns the SDP. */
110
+ readonly globalHeader?: boolean;
111
+ /** `-af` filter chain, e.g. `aresample=async=1000:first_pts=0`. */
112
+ readonly filter?: string;
113
+ }
114
+ export type FfmpegAudioPlan = {
115
+ readonly kind: 'none';
116
+ } | {
117
+ readonly kind: 'copy';
118
+ } | FfmpegAudioEncodePlan;
119
+ /**
120
+ * Camera-microphone audio, per codec. Lives HERE rather than in
121
+ * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
122
+ * on these types); it is re-exported from there, which is where to read it.
123
+ *
124
+ * Every source in this repo is a mono camera mic. The former broker preset
125
+ * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
126
+ * that is the value this consolidation changed.
127
+ */
128
+ export declare const AUDIO_PRESETS: Readonly<Record<'aac' | 'opus' | 'pcmu', FfmpegAudioEncodePlan>>;
129
+ /** One RTP output leg. The payload type / SSRC / MTU are the CONSUMER's facts. */
130
+ export interface FfmpegRtpOutput {
131
+ /** `rtp://host:port[?pkt_size=N]`. */
132
+ readonly url: string;
133
+ readonly payloadType?: number;
134
+ /** ffmpeg parses `-ssrc` as a SIGNED int32 — callers coerce with `| 0`. */
135
+ readonly ssrc?: number;
136
+ /** Path ffmpeg writes this output's SDP to (`-sdp_file`). */
137
+ readonly sdpFile?: string;
138
+ }
139
+ export type FfmpegSink = {
140
+ readonly kind: 'rtsp-listen';
141
+ readonly url: string;
142
+ } | {
143
+ readonly kind: 'stdout';
144
+ readonly container: 'h264' | 'hevc' | 'mpegts';
145
+ }
146
+ /**
147
+ * Two independent mapped RTP outputs. Generic ffmpeg RTP vocabulary — the
148
+ * SRTP encryption, the port allocation and the payload-type numbers stay
149
+ * with the consumer that negotiated them.
150
+ */
151
+ | {
152
+ readonly kind: 'rtp-outputs';
153
+ readonly video: FfmpegRtpOutput | null;
154
+ readonly audio: FfmpegRtpOutput | null;
155
+ };
156
+ /**
157
+ * An optional SECOND output carrying source audio as its own RTP stream,
158
+ * alongside an elementary video sink (which is `-an`). ffmpeg writes the
159
+ * authoritative SDP to `sdpFile`.
160
+ */
161
+ export interface FfmpegAudioSidecar {
162
+ readonly codec: FfmpegAudioCodecId;
163
+ readonly rtpUrl: string;
164
+ readonly sdpFile: string;
165
+ }
166
+ export interface FfmpegInvocation {
167
+ readonly logLevel: 'error' | 'warning' | 'info';
168
+ /**
169
+ * A concrete `-hwaccel` backend, the literal `'auto'`, or a software
170
+ * sentinel (`null` / `'none'` / `'copy'`) ⇒ no `-hwaccel` at all.
171
+ * Resolved by `resolveDecodeHwAccel` (see `./hwaccel.js`), which reads the
172
+ * DECODER ADDON's ranking, never the raw kernel resolver's.
173
+ */
174
+ readonly decodeHwAccel: string | null;
175
+ readonly input: FfmpegInputPlan;
176
+ readonly video: FfmpegVideoPlan;
177
+ readonly audio: FfmpegAudioPlan;
178
+ /** `0` = auto (omit `-threads` and let ffmpeg decide). */
179
+ readonly threadCount: number;
180
+ /** Consumer output options, emitted verbatim after the encode block. */
181
+ readonly outputArgs: readonly string[];
182
+ readonly sink: FfmpegSink;
183
+ readonly audioSidecar?: FfmpegAudioSidecar | null;
184
+ }
185
+ /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
186
+ export declare function logBannerArgs(level: 'error' | 'warning' | 'info'): string[];
187
+ /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
188
+ export declare function isSoftwareDecode(decodeHwAccel: string | null): boolean;
189
+ /**
190
+ * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
191
+ * appended to this list by a caller — that is the whole point of the function.
192
+ */
193
+ export declare function buildInputArgs(input: FfmpegInputPlan, decodeHwAccel: string | null): string[];
194
+ /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
195
+ export declare function buildVideoArgs(video: FfmpegVideoPlan, outputArgs: readonly string[]): string[];
196
+ /** The whole audio block, after `-i`. */
197
+ export declare function buildAudioArgs(audio: FfmpegAudioPlan): string[];
198
+ /**
199
+ * Assemble the full ffmpeg argument list. Layout:
200
+ *
201
+ * -hide_banner -loglevel <level>
202
+ * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
203
+ * [<input.extraArgs>] │
204
+ * [-fflags <flag>…] │
205
+ * [-rtsp_transport tcp] │
206
+ * -i <url> ─┘
207
+ * <video block> <threads> <audio block> ─┐ OUTPUT options.
208
+ * <consumer outputArgs verbatim> │
209
+ * <sink> ─┘ terminal
210
+ */
211
+ export declare function buildFfmpegArgs(inv: FfmpegInvocation): string[];
212
+ /**
213
+ * The hardware encoder for a target codec on `backend`, or the software one.
214
+ * `'auto'` is NOT a backend identity (it is an instruction to ffmpeg), so it
215
+ * maps to software encoding.
216
+ */
217
+ export declare function pickVideoEncoder(target: 'h264' | 'h265', backend: string | null, useHardware: boolean): FfmpegVideoEncoderId;
218
+ /** Map an `EncodeProfile.audio` to an audio plan. */
219
+ export declare function audioPlanFromEncodeProfile(audio: AudioEncode): FfmpegAudioPlan;
220
+ export interface EncodeProfileInvocationInput {
221
+ readonly profile: EncodeProfile;
222
+ /** The SOURCE's video codec — drives the smart-copy decision. */
223
+ readonly sourceCodec: 'h264' | 'h265';
224
+ readonly sourceUrl: string;
225
+ readonly sink: FfmpegSink;
226
+ readonly decodeHwAccel: string | null;
227
+ /**
228
+ * Defeat smart-copy. A DERIVED stream always applies its profile — applying
229
+ * it is its entire reason to exist — so it re-encodes even when the source
230
+ * already speaks the target codec. A `copy` profile still copies.
231
+ */
232
+ readonly forceReencode?: boolean;
233
+ /** Select the hardware encoder for {@link decodeHwAccel}'s backend. */
234
+ readonly hardwareEncoders?: boolean;
235
+ readonly logLevel?: 'error' | 'warning' | 'info';
236
+ readonly threadCount?: number;
237
+ readonly audioSidecar?: FfmpegAudioSidecar | null;
238
+ }
239
+ /**
240
+ * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
241
+ * {@link FfmpegInvocation}. This is the ONLY bridge between the two models —
242
+ * a second one is how the repo grew two argv builders that disagreed about
243
+ * hardware.
244
+ *
245
+ * Smart video copy: when the source already speaks the requested codec the
246
+ * encode block is elided entirely and ffmpeg runs as a re-muxer on the video
247
+ * plane. Width / height / fps / bitrate in the profile are a downstream BUDGET,
248
+ * not a forced rescale.
249
+ */
250
+ 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,39 @@
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
+ interface CanonicalEgressPlan {
12
+ readonly deviceId: number;
13
+ readonly source: string;
14
+ readonly video: Readonly<Record<string, string | number>>;
15
+ readonly audio: Readonly<Record<string, string | number>>;
16
+ readonly rateControl: string;
17
+ readonly bitstreamFilter: string;
18
+ readonly pixelFormat: string;
19
+ readonly decodeHwAccel: string;
20
+ }
21
+ /**
22
+ * The normalised plan a key is computed from. Exported so a test — and a
23
+ * future operator-facing "why are these two not sharing?" surface — can diff
24
+ * two requests without reversing a hash.
25
+ */
26
+ export declare function canonicalEgressPlan(request: EgressTranscodeRequest): CanonicalEgressPlan;
27
+ /**
28
+ * The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
29
+ * so a request built with a different field order produces the same digest.
30
+ *
31
+ * The handle this keys is IMMUTABLE: there is no `reconfigure`. A consumer
32
+ * whose requirements change RELEASES and re-acquires; the refcount does the
33
+ * rest. That is what stops co-tenants disturbing each other — the co-tenant
34
+ * hazard that made Alexa's shared `derived:alexa-<id>` stream a hazard was
35
+ * exactly a mutable shared object, where one consumer's downgrade dragged
36
+ * every other consumer to 360p.
37
+ */
38
+ export declare function egressTranscodeSharingKey(request: EgressTranscodeRequest): string;
39
+ export {};
@@ -4370,6 +4370,20 @@ export type AppRouter = TrpcCoreRouter<{
4370
4370
  output: z.infer<typeof notificationRulesCapability.methods.setRuleEnabled.output>;
4371
4371
  meta: object;
4372
4372
  }>;
4373
+ listDeviceMutes: TRPCQueryProcedure<{
4374
+ input: {
4375
+ [x: string]: unknown;
4376
+ } & z.input<typeof notificationRulesCapability.methods.listDeviceMutes.input>;
4377
+ output: z.infer<typeof notificationRulesCapability.methods.listDeviceMutes.output>;
4378
+ meta: object;
4379
+ }>;
4380
+ setDeviceMuted: TRPCMutationProcedure<{
4381
+ input: {
4382
+ [x: string]: unknown;
4383
+ } & z.input<typeof notificationRulesCapability.methods.setDeviceMuted.input>;
4384
+ output: z.infer<typeof notificationRulesCapability.methods.setDeviceMuted.output>;
4385
+ meta: object;
4386
+ }>;
4373
4387
  testRule: TRPCMutationProcedure<{
4374
4388
  input: {
4375
4389
  [x: string]: unknown;
@@ -4735,6 +4749,13 @@ export type AppRouter = TrpcCoreRouter<{
4735
4749
  output: z.infer<typeof pipelineAnalyticsCapability.methods.deleteTracks.output>;
4736
4750
  meta: object;
4737
4751
  }>;
4752
+ setTrackFlags: TRPCMutationProcedure<{
4753
+ input: {
4754
+ [x: string]: unknown;
4755
+ } & z.input<typeof pipelineAnalyticsCapability.methods.setTrackFlags.input>;
4756
+ output: z.infer<typeof pipelineAnalyticsCapability.methods.setTrackFlags.output>;
4757
+ meta: object;
4758
+ }>;
4738
4759
  getEventStoreFootprint: TRPCQueryProcedure<{
4739
4760
  input: {
4740
4761
  [x: string]: unknown;
@@ -5358,6 +5379,20 @@ export type AppRouter = TrpcCoreRouter<{
5358
5379
  output: z.infer<typeof pipelineOrchestratorCapability.methods.resolvePipeline.output>;
5359
5380
  meta: object;
5360
5381
  }>;
5382
+ getCameraSwitches: TRPCQueryProcedure<{
5383
+ input: {
5384
+ [x: string]: unknown;
5385
+ } & z.input<typeof pipelineOrchestratorCapability.methods.getCameraSwitches.input>;
5386
+ output: z.infer<typeof pipelineOrchestratorCapability.methods.getCameraSwitches.output>;
5387
+ meta: object;
5388
+ }>;
5389
+ setCameraSwitch: TRPCMutationProcedure<{
5390
+ input: {
5391
+ [x: string]: unknown;
5392
+ } & z.input<typeof pipelineOrchestratorCapability.methods.setCameraSwitch.input>;
5393
+ output: z.infer<typeof pipelineOrchestratorCapability.methods.setCameraSwitch.output>;
5394
+ meta: object;
5395
+ }>;
5361
5396
  getCameraStatus: TRPCQueryProcedure<{
5362
5397
  input: {
5363
5398
  [x: string]: unknown;
@@ -5477,6 +5512,13 @@ export type AppRouter = TrpcCoreRouter<{
5477
5512
  output: z.infer<typeof pipelineRunnerCapability.methods.runDetailSubtree.output>;
5478
5513
  meta: object;
5479
5514
  }>;
5515
+ runStatelessStep: TRPCMutationProcedure<{
5516
+ input: {
5517
+ [x: string]: unknown;
5518
+ } & z.input<typeof pipelineRunnerCapability.methods.runStatelessStep.input>;
5519
+ output: z.infer<typeof pipelineRunnerCapability.methods.runStatelessStep.output>;
5520
+ meta: object;
5521
+ }>;
5480
5522
  }>>;
5481
5523
  plateGallery: TRPCBuiltRouter<{
5482
5524
  ctx: TrpcContext;
@@ -6827,6 +6869,20 @@ export type AppRouter = TrpcCoreRouter<{
6827
6869
  output: z.infer<typeof streamBrokerCapability.methods.releaseStreamWithCodec.output>;
6828
6870
  meta: object;
6829
6871
  }>;
6872
+ acquireEgressTranscode: TRPCMutationProcedure<{
6873
+ input: {
6874
+ [x: string]: unknown;
6875
+ } & z.input<typeof streamBrokerCapability.methods.acquireEgressTranscode.input>;
6876
+ output: z.infer<typeof streamBrokerCapability.methods.acquireEgressTranscode.output>;
6877
+ meta: object;
6878
+ }>;
6879
+ releaseEgressTranscode: TRPCMutationProcedure<{
6880
+ input: {
6881
+ [x: string]: unknown;
6882
+ } & z.input<typeof streamBrokerCapability.methods.releaseEgressTranscode.input>;
6883
+ output: z.infer<typeof streamBrokerCapability.methods.releaseEgressTranscode.output>;
6884
+ meta: object;
6885
+ }>;
6830
6886
  subscribeAudioChunks: TRPCMutationProcedure<{
6831
6887
  input: {
6832
6888
  [x: string]: unknown;
@@ -8274,14 +8330,14 @@ export type AppRouter = TrpcCoreRouter<{
8274
8330
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
8275
8331
  listCapabilities: import("@trpc/server").TRPCQueryProcedure<{
8276
8332
  input: void;
8277
- output: import("@camstack/types").CapabilityInfo[];
8333
+ output: import("packages/types/dist").CapabilityInfo[];
8278
8334
  meta: object;
8279
8335
  }>;
8280
8336
  getCapability: import("@trpc/server").TRPCQueryProcedure<{
8281
8337
  input: {
8282
8338
  name: string;
8283
8339
  };
8284
- output: import("@camstack/types").CapabilityInfo | null;
8340
+ output: import("packages/types/dist").CapabilityInfo | null;
8285
8341
  meta: object;
8286
8342
  }>;
8287
8343
  setActiveSingleton: import("@trpc/server").TRPCMutationProcedure<{
@@ -8353,14 +8409,14 @@ export type AppRouter = TrpcCoreRouter<{
8353
8409
  }>> & import("@trpc/server").TRPCDecorateCreateRouterOptions<{
8354
8410
  listCapabilities: import("@trpc/server").TRPCQueryProcedure<{
8355
8411
  input: void;
8356
- output: import("@camstack/types").CapabilityInfo[];
8412
+ output: import("packages/types/dist").CapabilityInfo[];
8357
8413
  meta: object;
8358
8414
  }>;
8359
8415
  getCapability: import("@trpc/server").TRPCQueryProcedure<{
8360
8416
  input: {
8361
8417
  name: string;
8362
8418
  };
8363
- output: import("@camstack/types").CapabilityInfo | null;
8419
+ output: import("packages/types/dist").CapabilityInfo | null;
8364
8420
  meta: object;
8365
8421
  }>;
8366
8422
  setActiveSingleton: import("@trpc/server").TRPCMutationProcedure<{
@@ -273,7 +273,7 @@ export interface DeviceProxy {
273
273
  readonly faceGallery: Pick<InferDeviceProxyCap<typeof faceGalleryCapability>, 'getFaceByTrack'>;
274
274
  readonly networkQuality: Pick<InferDeviceProxyCap<typeof networkQualityCapability>, 'getDeviceStats' | 'reportClientStats'>;
275
275
  readonly pipelineExecutor: Pick<InferDeviceProxyCap<typeof pipelineExecutorCapability>, 'runPipeline' | 'runPipelineBatch'>;
276
- readonly pipelineOrchestrator: Pick<InferDeviceProxyCap<typeof pipelineOrchestratorCapability>, 'assignPipeline' | 'unassignPipeline' | 'setPipelineDevicePin' | 'getPipelineDevicePin' | 'getPipelineAssignment' | 'getCameraMetrics' | 'assignAudio' | 'unassignAudio' | 'getAudioAssignment' | 'getAudioAssignments' | 'getCameraSettings' | 'setCameraStepToggle' | 'getCameraStepOverrides' | 'setCameraStepOverride' | 'setCameraPipelineForAgent' | 'resolvePipeline' | 'getCameraStatus' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
276
+ readonly pipelineOrchestrator: Pick<InferDeviceProxyCap<typeof pipelineOrchestratorCapability>, 'assignPipeline' | 'unassignPipeline' | 'setPipelineDevicePin' | 'getPipelineDevicePin' | 'getPipelineAssignment' | 'getCameraMetrics' | 'assignAudio' | 'unassignAudio' | 'getAudioAssignment' | 'getAudioAssignments' | 'getCameraSettings' | 'setCameraStepToggle' | 'getCameraStepOverrides' | 'setCameraStepOverride' | 'setCameraPipelineForAgent' | 'resolvePipeline' | 'getCameraSwitches' | 'setCameraSwitch' | 'getCameraStatus' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
277
277
  readonly pipelineRunner: Pick<InferDeviceProxyCap<typeof pipelineRunnerCapability>, 'detachCamera' | 'getCameraMetrics' | 'runDetailSubtree'>;
278
278
  readonly plateGallery: Pick<InferDeviceProxyCap<typeof plateGalleryCapability>, 'listPlates' | 'getPlateByTrack'>;
279
279
  readonly recording: Pick<InferDeviceProxyCap<typeof recordingCapability>, 'getAvailability' | 'getDaysWithRecordings' | 'getPlaybackManifest' | 'getDeviceConfig' | 'locateSegment' | 'readSegmentBytes' | 'setDeviceConfig' | 'rescanStorage' | 'pruneFootage' | 'deleteFootprint' | 'renderGif' | 'renderClip' | 'getStatus' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
@@ -6,7 +6,7 @@
6
6
  * scope+access check inside `protectedProcedure` (see
7
7
  * `server/backend/src/api/trpc/trpc.middleware.ts`).
8
8
  *
9
- * Coverage: 866 method paths across 120 capabilities.
9
+ * Coverage: 874 method paths across 120 capabilities.
10
10
  */
11
11
  import type { CapabilityMethodAccess } from '../capabilities/capability-definition.js';
12
12
  export interface MethodAccessRecord {