@camstack/types 1.2.42 → 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.
@@ -21,7 +21,7 @@
21
21
  * `-i`; everything that configures an OUTPUT comes after. `__tests__/
22
22
  * ffmpeg-invocation.spec.ts` asserts INDEXES, never membership.
23
23
  */
24
- import type { EncodeProfile, AudioEncode } from '../encode-profile.js';
24
+ import type { AudioEncode, EncodeProfile } from '../encode-profile.js';
25
25
  /** Everything that configures the ffmpeg INPUT. Emitted strictly before `-i`. */
26
26
  export interface FfmpegInputPlan {
27
27
  /** An RTSP url, a `pipe:0` stdin feed, or a file path. */
@@ -34,6 +34,25 @@ export interface FfmpegInputPlan {
34
34
  readonly rtspTransport?: 'tcp' | 'udp' | null;
35
35
  /** `-fflags` values, e.g. `['+discardcorrupt']`, `['+genpts']`. */
36
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;
37
56
  /** Caller-supplied input options (an `EncodeProfile.inputArgs`, probe flags). */
38
57
  readonly extraArgs?: readonly string[];
39
58
  }
@@ -82,6 +101,21 @@ export interface FfmpegVideoEncodePlan {
82
101
  readonly pixelFormat?: string;
83
102
  readonly fps?: number;
84
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;
85
119
  readonly bf?: number;
86
120
  readonly bitrateKbps?: number;
87
121
  readonly rateControl?: FfmpegRateControl;
@@ -136,13 +170,46 @@ export interface FfmpegRtpOutput {
136
170
  /** Path ffmpeg writes this output's SDP to (`-sdp_file`). */
137
171
  readonly sdpFile?: string;
138
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
+ }
139
206
  export type FfmpegSink = {
140
207
  readonly kind: 'rtsp-listen';
141
208
  readonly url: string;
142
209
  } | {
143
210
  readonly kind: 'stdout';
144
211
  readonly container: 'h264' | 'hevc' | 'mpegts';
145
- }
212
+ } | FfmpegFmp4Sink
146
213
  /**
147
214
  * Two independent mapped RTP outputs. Generic ffmpeg RTP vocabulary — the
148
215
  * SRTP encryption, the port allocation and the payload-type numbers stay
@@ -235,6 +302,37 @@ export interface EncodeProfileInvocationInput {
235
302
  readonly logLevel?: 'error' | 'warning' | 'info';
236
303
  readonly threadCount?: number;
237
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;
238
336
  }
239
337
  /**
240
338
  * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
@@ -7,10 +7,62 @@ import type { EgressTranscodeRequest } from '../capabilities/stream-broker.cap.j
7
7
  * object either way (`substituteRtspHost`), so folding it in would fork the
8
8
  * child once per consuming node;
9
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.
10
57
  */
58
+ export declare function egressTransportFromRequest(request: EgressTranscodeRequest): EgressTransportPlan;
11
59
  interface CanonicalEgressPlan {
12
60
  readonly deviceId: number;
13
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;
14
66
  readonly video: Readonly<Record<string, string | number>>;
15
67
  readonly audio: Readonly<Record<string, string | number>>;
16
68
  readonly rateControl: string;
@@ -23,7 +75,7 @@ interface CanonicalEgressPlan {
23
75
  * future operator-facing "why are these two not sharing?" surface — can diff
24
76
  * two requests without reversing a hash.
25
77
  */
26
- export declare function canonicalEgressPlan(request: EgressTranscodeRequest): CanonicalEgressPlan;
78
+ export declare function canonicalEgressPlan(request: EgressTranscodeRequest, delivery?: EgressTransportPlan): CanonicalEgressPlan;
27
79
  /**
28
80
  * The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
29
81
  * so a request built with a different field order produces the same digest.
@@ -35,5 +87,5 @@ export declare function canonicalEgressPlan(request: EgressTranscodeRequest): Ca
35
87
  * exactly a mutable shared object, where one consumer's downgrade dragged
36
88
  * every other consumer to 360p.
37
89
  */
38
- export declare function egressTranscodeSharingKey(request: EgressTranscodeRequest): string;
90
+ export declare function egressTranscodeSharingKey(request: EgressTranscodeRequest, delivery?: EgressTransportPlan): string;
39
91
  export {};
@@ -5746,6 +5746,13 @@ export type AppRouter = TrpcCoreRouter<{
5746
5746
  output: z.infer<typeof privacyMaskCapability.methods.setMask.output>;
5747
5747
  meta: object;
5748
5748
  }>;
5749
+ setAudioEnabled: TRPCMutationProcedure<{
5750
+ input: {
5751
+ [x: string]: unknown;
5752
+ } & z.input<typeof privacyMaskCapability.methods.setAudioEnabled.input>;
5753
+ output: z.infer<typeof privacyMaskCapability.methods.setAudioEnabled.output>;
5754
+ meta: object;
5755
+ }>;
5749
5756
  }>>;
5750
5757
  ptz: TRPCBuiltRouter<{
5751
5758
  ctx: TrpcContext;
@@ -5970,6 +5977,13 @@ export type AppRouter = TrpcCoreRouter<{
5970
5977
  output: z.infer<typeof recordingCapability.methods.readSegmentBytes.output>;
5971
5978
  meta: object;
5972
5979
  }>;
5980
+ readGopBytes: TRPCQueryProcedure<{
5981
+ input: {
5982
+ [x: string]: unknown;
5983
+ } & z.input<typeof recordingCapability.methods.readGopBytes.input>;
5984
+ output: z.infer<typeof recordingCapability.methods.readGopBytes.output>;
5985
+ meta: object;
5986
+ }>;
5973
5987
  setDeviceConfig: TRPCMutationProcedure<{
5974
5988
  input: {
5975
5989
  [x: string]: unknown;
@@ -8834,6 +8848,15 @@ export type AppRouter = TrpcCoreRouter<{
8834
8848
  errorShape: AugmentedErrorShape;
8835
8849
  transformer: true;
8836
8850
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
8851
+ repairNodeAddons: import("@trpc/server").TRPCMutationProcedure<{
8852
+ input: {
8853
+ nodeId: string;
8854
+ };
8855
+ output: {
8856
+ success: boolean;
8857
+ };
8858
+ meta: object;
8859
+ }>;
8837
8860
  forgetNode: import("@trpc/server").TRPCMutationProcedure<{
8838
8861
  input: {
8839
8862
  nodeId: string;
@@ -8844,6 +8867,15 @@ export type AppRouter = TrpcCoreRouter<{
8844
8867
  meta: object;
8845
8868
  }>;
8846
8869
  }>> & import("@trpc/server").TRPCDecorateCreateRouterOptions<{
8870
+ repairNodeAddons: import("@trpc/server").TRPCMutationProcedure<{
8871
+ input: {
8872
+ nodeId: string;
8873
+ };
8874
+ output: {
8875
+ success: boolean;
8876
+ };
8877
+ meta: object;
8878
+ }>;
8847
8879
  forgetNode: import("@trpc/server").TRPCMutationProcedure<{
8848
8880
  input: {
8849
8881
  nodeId: string;
@@ -276,7 +276,7 @@ export interface DeviceProxy {
276
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
- readonly recording: Pick<InferDeviceProxyCap<typeof recordingCapability>, 'getAvailability' | 'getDaysWithRecordings' | 'getPlaybackManifest' | 'getDeviceConfig' | 'locateSegment' | 'readSegmentBytes' | 'setDeviceConfig' | 'rescanStorage' | 'pruneFootage' | 'deleteFootprint' | 'renderGif' | 'renderClip' | 'getStatus' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
279
+ readonly recording: Pick<InferDeviceProxyCap<typeof recordingCapability>, 'getAvailability' | 'getDaysWithRecordings' | 'getPlaybackManifest' | 'getDeviceConfig' | 'locateSegment' | 'readSegmentBytes' | 'readGopBytes' | 'setDeviceConfig' | 'rescanStorage' | 'pruneFootage' | 'deleteFootprint' | 'renderGif' | 'renderClip' | 'getStatus' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
280
280
  readonly recordingExport: Pick<InferDeviceProxyCap<typeof recordingExportCapability>, 'createExport' | 'listExports'>;
281
281
  readonly streamBroker: Pick<InferDeviceProxyCap<typeof streamBrokerCapability>, 'publishCameraStream' | 'retractCameraStream' | 'assignProfile' | 'unassignProfile' | 'renderPreBufferClip' | 'restartProfile' | 'getDeviceSettingsContribution' | 'getDeviceLiveContribution' | 'applyDeviceSettingsPatch'>;
282
282
  }
@@ -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: 874 method paths across 120 capabilities.
9
+ * Coverage: 876 method paths across 120 capabilities.
10
10
  */
11
11
  import type { CapabilityMethodAccess } from '../capabilities/capability-definition.js';
12
12
  export interface MethodAccessRecord {
package/dist/index.d.ts CHANGED
@@ -188,6 +188,7 @@ export type { CameraDetectionCapabilities, CameraMotionConfig, CameraNativeDetec
188
188
  export { DEVICE_TYPE_INFO, type DeviceTypeInfo } from './types/device-type.js';
189
189
  export * from './units/index.js';
190
190
  export { bestLocationMatch, locationSimilarity } from './util/location-match.js';
191
+ export { bareAddonId, isSameAddonId } from './utils/addon-id.js';
191
192
  export { cosineSimilarity } from './utils/cosine-similarity.js';
192
193
  export { ElementConfigStore } from './utils/element-config-store.js';
193
194
  export { errMsg } from './utils/err-msg.js';
@@ -204,6 +205,7 @@ export { sleep, sleepCancellable } from './utils/sleep.js';
204
205
  export { decodeVectorBase64, encodeVectorBase64, vectorDimFromBase64, } from './utils/vector-codec.js';
205
206
  export { evaluateZoneRules, type ZoneRuleEvalResult } from './utils/zone-rule-eval.js';
206
207
  export { DEFAULT_DETAIL_CROP_CONVENTION, DETAIL_CROP_PADDING_FIELD, DETAIL_CROP_PADDING_KEY, DETAIL_CROP_SECTION_ID, DETAIL_CROP_SQUARE_KEY, type DetailCropConvention, DetailCropConventionSchema, type DetailCropRect, deriveDetailCropRect, type HydratedSettingsSection, type HydratedSettingsView, pickDetailCropConvention, readDetailCropConvention, } from './pipeline/detail-crop.js';
208
+ export { DEFAULT_NATIVE_LEASE_SETTINGS, NATIVE_LEASE_ACTIVITY_FIELD, NATIVE_LEASE_ACTIVITY_KEY, NATIVE_LEASE_ADMISSION_FIELD, NATIVE_LEASE_ADMISSION_KEY, NATIVE_LEASE_BUDGET_FIELD, NATIVE_LEASE_BUDGET_KEY, NATIVE_LEASE_SECTION_ID, NATIVE_LEASE_TTL_FIELD, NATIVE_LEASE_TTL_KEY, type NativeLeaseAdmission, NativeLeaseAdmissionSchema, type NativeLeaseKnob, type NativeLeaseNumberKnob, type NativeLeaseSettings, NativeLeaseSettingsSchema, type NativeLeaseSettingsOverride, pickNativeLeaseOverride, readNativeLeaseOverride, } from './pipeline/native-lease.js';
207
209
  export { bindAddonActions } from './helpers/bind-addon-actions.js';
208
210
  export type { DeviceOption, InferenceDeviceDescriptor, RuntimeId, } from './inference/runtime-capabilities.js';
209
211
  export { defaultDeviceFor, enumerateInferenceDevices, modelFormatForRuntime, runtimeDevices, scoreRuntimes, supportedRuntimes, } from './inference/runtime-capabilities.js';