@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.
- 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/capabilities/index.d.ts +2 -2
- 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 +123 -0
- package/dist/capabilities/pipeline-runner.cap.d.ts +119 -1
- package/dist/capabilities/platform-probe.cap.d.ts +3 -3
- package/dist/capabilities/recording.cap.d.ts +3 -0
- package/dist/capabilities/stream-broker.cap.d.ts +300 -0
- package/dist/encode-profile.d.ts +2 -0
- package/dist/ffmpeg/encode-defaults.d.ts +89 -0
- package/dist/ffmpeg/hwaccel.d.ts +98 -0
- package/dist/ffmpeg/invocation.d.ts +250 -0
- package/dist/ffmpeg/process.d.ts +135 -0
- package/dist/ffmpeg/sharing-key.d.ts +39 -0
- package/dist/generated/addon-api.d.ts +60 -4
- package/dist/generated/device-proxy.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1354 -20
- package/dist/index.mjs +1316 -21
- package/dist/interfaces/camera-switches.d.ts +217 -0
- 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/{sleep-CXimb854.mjs → sleep-BmNKsY7v.mjs} +5 -0
- package/dist/{sleep-DTce7-ch.js → sleep-Cvi1JxZp.js} +5 -0
- 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("
|
|
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("
|
|
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("
|
|
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("
|
|
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:
|
|
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 {
|