@camstack/types 1.2.43 → 1.2.45
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 +8 -2
- package/dist/addon.mjs +8 -2
- package/dist/capabilities/index.d.ts +21 -3
- package/dist/capabilities/osd-manager.cap.d.ts +900 -0
- package/dist/capabilities/pipeline-analytics.cap.d.ts +1192 -74
- package/dist/capabilities/pipeline-orchestrator.cap.d.ts +10 -0
- package/dist/capabilities/pipeline-runner.cap.d.ts +6 -0
- package/dist/capabilities/schemas/streaming-shared.d.ts +2 -0
- package/dist/capabilities/server-management.cap.d.ts +3 -3
- package/dist/capabilities/stream-broker.cap.d.ts +32 -0
- package/dist/ffmpeg/fmp4-box-splitter.d.ts +113 -0
- package/dist/ffmpeg/fmp4-fragment-child.d.ts +85 -0
- package/dist/ffmpeg/fmp4-fragment-plane.d.ts +142 -0
- package/dist/ffmpeg/invocation.d.ts +4 -2
- package/dist/fmp4-box-splitter-B53u9-Nu.mjs +615 -0
- package/dist/fmp4-box-splitter-BkWH7O3L.js +686 -0
- package/dist/generated/addon-api.d.ts +162 -0
- package/dist/generated/cap-input-defaults.d.ts +1 -1
- package/dist/generated/capability-router-map.d.ts +5 -2
- package/dist/generated/device-proxy.d.ts +3 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1114 -407
- package/dist/index.mjs +1075 -397
- package/dist/interfaces/camera-switches.d.ts +48 -0
- package/dist/interfaces/stream-broker.d.ts +19 -49
- package/dist/node.d.ts +4 -0
- package/dist/node.js +509 -3
- package/dist/node.mjs +507 -3
- package/dist/notification/schedule.d.ts +20 -0
- package/dist/{sleep-Bx9IIoT0.js → sleep-BOI-sVEA.js} +37 -1
- package/dist/{sleep-DtstvzWm.mjs → sleep-Bf5fBs7u.mjs} +37 -1
- package/dist/types/detection.d.ts +31 -0
- package/dist/types/pipeline-step.d.ts +22 -1
- package/package.json +1 -1
- package/dist/canonical-hash-7nfBbEqR.mjs +0 -35
- package/dist/canonical-hash-BcZHRHIx.js +0 -40
|
@@ -449,6 +449,7 @@ declare const CameraStatusSchema: z.ZodObject<{
|
|
|
449
449
|
notifications: "notifications";
|
|
450
450
|
"object-detection": "object-detection";
|
|
451
451
|
"device-audio": "device-audio";
|
|
452
|
+
"broker-audio": "broker-audio";
|
|
452
453
|
}>>>;
|
|
453
454
|
fetchedAt: z.ZodNumber;
|
|
454
455
|
}, z.core.$strip>;
|
|
@@ -1099,6 +1100,7 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1099
1100
|
notifications: "notifications";
|
|
1100
1101
|
"object-detection": "object-detection";
|
|
1101
1102
|
"device-audio": "device-audio";
|
|
1103
|
+
"broker-audio": "broker-audio";
|
|
1102
1104
|
}>;
|
|
1103
1105
|
label: z.ZodString;
|
|
1104
1106
|
costWhenOff: z.ZodString;
|
|
@@ -1124,6 +1126,8 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1124
1126
|
}, z.core.$strip>, z.ZodObject<{
|
|
1125
1127
|
kind: z.ZodLiteral<"camera-mask">;
|
|
1126
1128
|
capName: z.ZodString;
|
|
1129
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1130
|
+
kind: z.ZodLiteral<"broker-audio-mute">;
|
|
1127
1131
|
}, z.core.$strip>], "kind">;
|
|
1128
1132
|
}, z.core.$strip>>>;
|
|
1129
1133
|
fetchedAt: z.ZodNumber;
|
|
@@ -1151,6 +1155,7 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1151
1155
|
notifications: "notifications";
|
|
1152
1156
|
"object-detection": "object-detection";
|
|
1153
1157
|
"device-audio": "device-audio";
|
|
1158
|
+
"broker-audio": "broker-audio";
|
|
1154
1159
|
}>;
|
|
1155
1160
|
enabled: z.ZodBoolean;
|
|
1156
1161
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -1164,6 +1169,7 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1164
1169
|
notifications: "notifications";
|
|
1165
1170
|
"object-detection": "object-detection";
|
|
1166
1171
|
"device-audio": "device-audio";
|
|
1172
|
+
"broker-audio": "broker-audio";
|
|
1167
1173
|
}>;
|
|
1168
1174
|
label: z.ZodString;
|
|
1169
1175
|
costWhenOff: z.ZodString;
|
|
@@ -1189,6 +1195,8 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1189
1195
|
}, z.core.$strip>, z.ZodObject<{
|
|
1190
1196
|
kind: z.ZodLiteral<"camera-mask">;
|
|
1191
1197
|
capName: z.ZodString;
|
|
1198
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1199
|
+
kind: z.ZodLiteral<"broker-audio-mute">;
|
|
1192
1200
|
}, z.core.$strip>], "kind">;
|
|
1193
1201
|
}, z.core.$strip>>>;
|
|
1194
1202
|
fetchedAt: z.ZodNumber;
|
|
@@ -1310,6 +1318,7 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1310
1318
|
notifications: "notifications";
|
|
1311
1319
|
"object-detection": "object-detection";
|
|
1312
1320
|
"device-audio": "device-audio";
|
|
1321
|
+
"broker-audio": "broker-audio";
|
|
1313
1322
|
}>>>;
|
|
1314
1323
|
fetchedAt: z.ZodNumber;
|
|
1315
1324
|
}, z.core.$strip>, import("./capability-definition.js").CapabilityMethodKind>;
|
|
@@ -1429,6 +1438,7 @@ export declare const pipelineOrchestratorCapability: {
|
|
|
1429
1438
|
notifications: "notifications";
|
|
1430
1439
|
"object-detection": "object-detection";
|
|
1431
1440
|
"device-audio": "device-audio";
|
|
1441
|
+
"broker-audio": "broker-audio";
|
|
1432
1442
|
}>>>;
|
|
1433
1443
|
fetchedAt: z.ZodNumber;
|
|
1434
1444
|
}, z.core.$strip>>>, import("./capability-definition.js").CapabilityMethodKind>;
|
|
@@ -55,6 +55,8 @@ declare const DetailResultSchema: z.ZodObject<{
|
|
|
55
55
|
}, z.core.$strip>>;
|
|
56
56
|
embedding: z.ZodOptional<z.ZodString>;
|
|
57
57
|
label: z.ZodOptional<z.ZodString>;
|
|
58
|
+
labelTier: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
|
|
59
|
+
labelModelId: z.ZodOptional<z.ZodString>;
|
|
58
60
|
alignedCropJpeg: z.ZodOptional<z.ZodString>;
|
|
59
61
|
nativeFaceShortSidePx: z.ZodOptional<z.ZodNumber>;
|
|
60
62
|
}, z.core.$strip>;
|
|
@@ -623,6 +625,8 @@ export declare const pipelineRunnerCapability: {
|
|
|
623
625
|
}, z.core.$strip>>;
|
|
624
626
|
embedding: z.ZodOptional<z.ZodString>;
|
|
625
627
|
label: z.ZodOptional<z.ZodString>;
|
|
628
|
+
labelTier: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
|
|
629
|
+
labelModelId: z.ZodOptional<z.ZodString>;
|
|
626
630
|
alignedCropJpeg: z.ZodOptional<z.ZodString>;
|
|
627
631
|
nativeFaceShortSidePx: z.ZodOptional<z.ZodNumber>;
|
|
628
632
|
}, z.core.$strip>>;
|
|
@@ -699,6 +703,8 @@ export declare const pipelineRunnerCapability: {
|
|
|
699
703
|
}, z.core.$strip>>;
|
|
700
704
|
embedding: z.ZodOptional<z.ZodString>;
|
|
701
705
|
label: z.ZodOptional<z.ZodString>;
|
|
706
|
+
labelTier: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
|
|
707
|
+
labelModelId: z.ZodOptional<z.ZodString>;
|
|
702
708
|
alignedCropJpeg: z.ZodOptional<z.ZodString>;
|
|
703
709
|
nativeFaceShortSidePx: z.ZodOptional<z.ZodNumber>;
|
|
704
710
|
}, z.core.$strip>>;
|
|
@@ -317,6 +317,8 @@ export declare const BrokerStatsSchema: z.ZodObject<{
|
|
|
317
317
|
channels: z.ZodNumber;
|
|
318
318
|
supported: z.ZodBoolean;
|
|
319
319
|
}, z.core.$strip>>>;
|
|
320
|
+
audioMuted: z.ZodOptional<z.ZodBoolean>;
|
|
321
|
+
audioMutedDropped: z.ZodOptional<z.ZodNumber>;
|
|
320
322
|
}, z.core.$strip>;
|
|
321
323
|
/**
|
|
322
324
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -50,8 +50,8 @@ declare const ServerBootModeSchema: z.ZodEnum<{
|
|
|
50
50
|
*/
|
|
51
51
|
declare const ServerUpdateStateSchema: z.ZodEnum<{
|
|
52
52
|
idle: "idle";
|
|
53
|
-
checking: "checking";
|
|
54
53
|
staging: "staging";
|
|
54
|
+
checking: "checking";
|
|
55
55
|
"pending-restart": "pending-restart";
|
|
56
56
|
"awaiting-confirmation": "awaiting-confirmation";
|
|
57
57
|
}>;
|
|
@@ -123,8 +123,8 @@ declare const ServerPackageStatusSchema: z.ZodObject<{
|
|
|
123
123
|
}>;
|
|
124
124
|
updateState: z.ZodEnum<{
|
|
125
125
|
idle: "idle";
|
|
126
|
-
checking: "checking";
|
|
127
126
|
staging: "staging";
|
|
127
|
+
checking: "checking";
|
|
128
128
|
"pending-restart": "pending-restart";
|
|
129
129
|
"awaiting-confirmation": "awaiting-confirmation";
|
|
130
130
|
}>;
|
|
@@ -189,8 +189,8 @@ export declare const serverManagementCapability: {
|
|
|
189
189
|
}>;
|
|
190
190
|
updateState: z.ZodEnum<{
|
|
191
191
|
idle: "idle";
|
|
192
|
-
checking: "checking";
|
|
193
192
|
staging: "staging";
|
|
193
|
+
checking: "checking";
|
|
194
194
|
"pending-restart": "pending-restart";
|
|
195
195
|
"awaiting-confirmation": "awaiting-confirmation";
|
|
196
196
|
}>;
|
|
@@ -689,6 +689,8 @@ export declare const streamBrokerCapability: {
|
|
|
689
689
|
channels: z.ZodNumber;
|
|
690
690
|
supported: z.ZodBoolean;
|
|
691
691
|
}, z.core.$strip>>>;
|
|
692
|
+
audioMuted: z.ZodOptional<z.ZodBoolean>;
|
|
693
|
+
audioMutedDropped: z.ZodOptional<z.ZodNumber>;
|
|
692
694
|
}, z.core.$strip>, import("./capability-definition.js").CapabilityMethodKind>;
|
|
693
695
|
/**
|
|
694
696
|
* Force a one-shot probe of a single source stream: transiently dial the
|
|
@@ -1151,6 +1153,36 @@ export declare const streamBrokerCapability: {
|
|
|
1151
1153
|
readonly isRtspEnabled: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
|
|
1152
1154
|
brokerId: z.ZodString;
|
|
1153
1155
|
}, z.core.$strip>, z.ZodBoolean, import("./capability-definition.js").CapabilityMethodKind>;
|
|
1156
|
+
/**
|
|
1157
|
+
* ── Per-device audio-plane policy (D83) ───────────────────────────
|
|
1158
|
+
*
|
|
1159
|
+
* The BROKER-side mute: while `muted`, this node distributes none of
|
|
1160
|
+
* the device's audio, on any plane it serves — live (WebRTC / encoded
|
|
1161
|
+
* subscribers) AND recording (the RTSP restreamer the recorder pulls,
|
|
1162
|
+
* which additionally serves the video-only SDP so the recorder's ffmpeg
|
|
1163
|
+
* never declares an audio stream it will not receive).
|
|
1164
|
+
*
|
|
1165
|
+
* Keyed by `deviceId`, not `brokerId`: every one of a camera's streams
|
|
1166
|
+
* carries the same microphone, and a per-stream answer would let main
|
|
1167
|
+
* and sub disagree about whether the camera is silent.
|
|
1168
|
+
*
|
|
1169
|
+
* The state lives in the broker's existing `DeviceOverride` blob — no
|
|
1170
|
+
* new store — and the mute is applied to a fresh broker at creation, so
|
|
1171
|
+
* a restart, a re-dial or a catalog republish never un-mutes a camera.
|
|
1172
|
+
*/
|
|
1173
|
+
readonly getDeviceAudioMute: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
|
|
1174
|
+
deviceId: z.ZodNumber;
|
|
1175
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1176
|
+
muted: z.ZodBoolean;
|
|
1177
|
+
appliedBrokers: z.ZodNumber;
|
|
1178
|
+
}, z.core.$strip>, import("./capability-definition.js").CapabilityMethodKind>;
|
|
1179
|
+
readonly setDeviceAudioMute: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
|
|
1180
|
+
deviceId: z.ZodNumber;
|
|
1181
|
+
muted: z.ZodBoolean;
|
|
1182
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1183
|
+
muted: z.ZodBoolean;
|
|
1184
|
+
appliedBrokers: z.ZodNumber;
|
|
1185
|
+
}, z.core.$strip>, "mutation">;
|
|
1154
1186
|
};
|
|
1155
1187
|
readonly events: {
|
|
1156
1188
|
readonly onCamStreamDemand: import("./capability-definition.js").CapabilityEventSchema<z.ZodObject<{
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fmp4BoxSplitter — cut an ffmpeg fragmented-MP4 byte stream into the units a
|
|
3
|
+
* consumer of fragments actually needs: ONE initialisation segment
|
|
4
|
+
* (`ftyp`+`moov`) followed by one unit per `moof`+`mdat` pair.
|
|
5
|
+
*
|
|
6
|
+
* This is the shape HomeKit Secure Video's `CameraRecordingDelegate` yields
|
|
7
|
+
* (`RecordingPacket` #0 is the initialisation, the rest are media fragments),
|
|
8
|
+
* and it is the same shape a CamStack clip source wants — which is the point.
|
|
9
|
+
* One fragmenter, two consumers ([roadmap 4b](../../../../../docs/roadmap.md)).
|
|
10
|
+
* The splitter therefore knows about NEITHER of them: it emits units, and
|
|
11
|
+
* {@link import('./fmp4-fragment-plane.js').Fmp4FragmentPlane} decides who gets
|
|
12
|
+
* them.
|
|
13
|
+
*
|
|
14
|
+
* ## Pure, streaming, bounded
|
|
15
|
+
*
|
|
16
|
+
* No I/O, no timers, no ffmpeg. `push` takes whatever bytes arrived and returns
|
|
17
|
+
* whatever units completed — a caller may hand it one byte at a time or a
|
|
18
|
+
* megabyte, and the units are identical either way (asserted). Memory is
|
|
19
|
+
* bounded by {@link Fmp4SplitterOptions.maxUnitBytes}: the splitter holds at
|
|
20
|
+
* most one in-progress unit plus a partial box, and a stream that exceeds the
|
|
21
|
+
* bound FAULTS rather than growing, because the alternative on a live 24/7
|
|
22
|
+
* source is the broker's heap.
|
|
23
|
+
*
|
|
24
|
+
* ## The box rules, from ISO/IEC 14496-12
|
|
25
|
+
*
|
|
26
|
+
* A box header is `size:uint32` + `type:4 chars`. `size == 1` means the real
|
|
27
|
+
* size is a `uint64` in the next 8 bytes; `size == 0` means "to end of file".
|
|
28
|
+
* A size-0 box is a FAULT here and not a special case: it cannot be cut into
|
|
29
|
+
* fragments at all, and pretending otherwise would produce one enormous unit
|
|
30
|
+
* whose bytes never arrive.
|
|
31
|
+
*
|
|
32
|
+
* ## What it does NOT do
|
|
33
|
+
*
|
|
34
|
+
* It never inspects `tfdt`/`trun` and never computes a fragment's duration. The
|
|
35
|
+
* fragment length is decided upstream by the muxer (`-min_frag_duration` plus a
|
|
36
|
+
* key-frame grid, see `@camstack/types` `ffmpeg/invocation.ts`), and a duration
|
|
37
|
+
* parsed here would be a SECOND opinion about the same fact — the failure mode
|
|
38
|
+
* D67 exists to prevent. A consumer that must verify conformance measures the
|
|
39
|
+
* arrival cadence, which is the thing it actually cares about.
|
|
40
|
+
*/
|
|
41
|
+
/** `init` is the `ftyp`+`moov` head; `fragment` is one `moof`+`mdat` pair. */
|
|
42
|
+
export type Fmp4UnitKind = 'init' | 'fragment';
|
|
43
|
+
export interface Fmp4Unit {
|
|
44
|
+
readonly kind: Fmp4UnitKind;
|
|
45
|
+
/** A standalone copy — the splitter never hands out a view into its buffer. */
|
|
46
|
+
readonly data: Uint8Array;
|
|
47
|
+
/** `0` for the initialisation segment, then `1, 2, 3…` per fragment. */
|
|
48
|
+
readonly sequence: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Fmp4SplitterOptions {
|
|
51
|
+
/**
|
|
52
|
+
* The most bytes one unit may reach before the stream is declared unusable.
|
|
53
|
+
* A 4-second 1080p fragment is a few hundred KB; the default leaves three
|
|
54
|
+
* orders of magnitude of headroom and still bounds a source that is not
|
|
55
|
+
* really fMP4 (a plain MP4 with a trailing `moov`, say, whose single `mdat`
|
|
56
|
+
* is the entire recording).
|
|
57
|
+
*/
|
|
58
|
+
readonly maxUnitBytes?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class Fmp4BoxSplitter {
|
|
61
|
+
private readonly maxUnitBytes;
|
|
62
|
+
/** Bytes of the CURRENT unit plus any partial box after it. */
|
|
63
|
+
private buffer;
|
|
64
|
+
/** Where the current unit starts inside {@link buffer}. */
|
|
65
|
+
private unitStart;
|
|
66
|
+
/** Where the box scanner has reached inside {@link buffer}. */
|
|
67
|
+
private cursor;
|
|
68
|
+
private state;
|
|
69
|
+
private nextSequence;
|
|
70
|
+
private faultReason;
|
|
71
|
+
private readonly interstitial;
|
|
72
|
+
constructor(options?: Fmp4SplitterOptions);
|
|
73
|
+
/**
|
|
74
|
+
* Non-null once the stream cannot be split. The splitter emits nothing
|
|
75
|
+
* further, so a caller polls this to kill the child rather than watching a
|
|
76
|
+
* silent stall — a fragmenter that quietly stops producing looks exactly like
|
|
77
|
+
* a camera with no motion.
|
|
78
|
+
*/
|
|
79
|
+
get fault(): string | null;
|
|
80
|
+
/** Bytes currently held. The memory bound, observable rather than asserted. */
|
|
81
|
+
get pendingBytes(): number;
|
|
82
|
+
/**
|
|
83
|
+
* Top-level box types seen BETWEEN fragments and discarded — `mfra`, `free`,
|
|
84
|
+
* a stray `sidx`. Reported rather than dropped in silence: they are legal and
|
|
85
|
+
* useless to a fragment consumer, but a type nobody expected showing up here
|
|
86
|
+
* is the first symptom of a muxer that is not writing what we think it is.
|
|
87
|
+
*/
|
|
88
|
+
get discardedInterstitialTypes(): readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Feed bytes; get back whatever units completed. Returns `[]` once faulted.
|
|
91
|
+
*/
|
|
92
|
+
push(chunk: Uint8Array): readonly Fmp4Unit[];
|
|
93
|
+
private append;
|
|
94
|
+
/** Consume every COMPLETE top-level box now in the buffer. */
|
|
95
|
+
private drainBoxes;
|
|
96
|
+
/**
|
|
97
|
+
* Apply one box to the state machine. Returns a unit when this box CLOSED
|
|
98
|
+
* one, `null` otherwise.
|
|
99
|
+
*/
|
|
100
|
+
private consumeBox;
|
|
101
|
+
/**
|
|
102
|
+
* Parse the header at {@link cursor}, or `null` when too few bytes have
|
|
103
|
+
* arrived to know. Faults on a size the splitter cannot honour.
|
|
104
|
+
*/
|
|
105
|
+
private readHeader;
|
|
106
|
+
private emit;
|
|
107
|
+
/**
|
|
108
|
+
* Drop everything already emitted or discarded. Without this the buffer is
|
|
109
|
+
* the whole stream and the process dies in hours, not minutes.
|
|
110
|
+
*/
|
|
111
|
+
private compact;
|
|
112
|
+
private fail;
|
|
113
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fmp4FragmentChild — one ffmpeg writing a fragmented MP4 to stdout, split into
|
|
3
|
+
* units and fanned out on a {@link Fmp4FragmentPlane}.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this lives in the PRIMITIVE and not in the broker
|
|
6
|
+
*
|
|
7
|
+
* It was written as the stream-broker's third egress transport, on the premise
|
|
8
|
+
* ([D80](../../../../docs/decisions/adr-0080.md)) that its consumers would be
|
|
9
|
+
* in-process with the broker and could hold a live plane object. **They are
|
|
10
|
+
* not.** `addon-export-hap` — the HKSV recording delegate, the first real
|
|
11
|
+
* consumer — is a `hub-only` addon in its OWN runner ([D2](../../../../docs/decisions/adr-0002.md)),
|
|
12
|
+
* and a plane cannot cross a process boundary. The choice was a second copy of
|
|
13
|
+
* the splitter in the exporter, or one copy here beside the sink that produces
|
|
14
|
+
* the bytes. The sink, the splitter, the plane and this child are one primitive
|
|
15
|
+
* ([D84](../../../../docs/decisions/adr-0084.md)); `EgressTranscodeManager`
|
|
16
|
+
* keeps only what is broker business — sharing key, refcount, release grace.
|
|
17
|
+
*
|
|
18
|
+
* It is deliberately the same shape as `TranscodeEgress` (the dial transport)
|
|
19
|
+
* and `createPushChild` (the push one), because the three failures that cost
|
|
20
|
+
* sessions on those paths are the same three here:
|
|
21
|
+
*
|
|
22
|
+
* - **It resolves on the first UNIT, never on the first byte.** A child whose
|
|
23
|
+
* bytes do not parse as fMP4 is a child that will never produce a fragment,
|
|
24
|
+
* and resolving on bytes hands the caller a handle nothing feeds.
|
|
25
|
+
* - **It retries once in SOFTWARE.** `-hwaccel vaapi` fails to create a device
|
|
26
|
+
* inside the stream-broker runner on the live hub (`code=244`), and the push
|
|
27
|
+
* transport was dark for exactly as long as it lacked this retry.
|
|
28
|
+
* - **A child that dies AFTER going live says so.** Silence reads as "it is
|
|
29
|
+
* running"; the plane must be ended so a consumer's generator completes
|
|
30
|
+
* rather than hanging on a producer that no longer exists.
|
|
31
|
+
*
|
|
32
|
+
* Two consumers: `EgressTranscodeManager.acquireFragments` (the broker, for a
|
|
33
|
+
* future CamStack clip source — roadmap 4b half 2) and `HksvFragmentSource` in
|
|
34
|
+
* `addon-export-hap`. It stays unreachable from `acquireEgressTranscode`'s
|
|
35
|
+
* request schema on purpose (see `egressTransportFromRequest`): a plane is a
|
|
36
|
+
* live object, so an off-node caller could not use one anyway.
|
|
37
|
+
*/
|
|
38
|
+
import type { spawn as nodeSpawn } from 'node:child_process';
|
|
39
|
+
import type { IScopedLogger } from '../interfaces/logging.js';
|
|
40
|
+
import type { FfmpegInvocation } from './invocation.js';
|
|
41
|
+
import type { Fmp4FragmentPlane } from './fmp4-fragment-plane.js';
|
|
42
|
+
export interface Fmp4FragmentChildDeps {
|
|
43
|
+
readonly logger: IScopedLogger;
|
|
44
|
+
readonly ffmpegBinaryPath: string;
|
|
45
|
+
readonly spawnFn: typeof nodeSpawn;
|
|
46
|
+
/** How long the child may produce no UNIT before it is declared dead. */
|
|
47
|
+
readonly firstUnitTimeoutMs?: number;
|
|
48
|
+
/** The child exited, or its output stopped parsing, AFTER it went live. */
|
|
49
|
+
readonly onChildExit?: (error: Error) => void;
|
|
50
|
+
}
|
|
51
|
+
export interface Fmp4FragmentChildArgs {
|
|
52
|
+
readonly sourceId: string;
|
|
53
|
+
/** Every line about this child is grouped per camera, without exception. */
|
|
54
|
+
readonly deviceId: number;
|
|
55
|
+
/** The negotiated fragment length — argv only; the splitter never reads it. */
|
|
56
|
+
readonly fragmentMs: number;
|
|
57
|
+
/** The invocation minus the sink, which this child owns. */
|
|
58
|
+
readonly invocation: Omit<FfmpegInvocation, 'sink' | 'audioSidecar'>;
|
|
59
|
+
/** Where the units go. Owned by the caller, ended by this child. */
|
|
60
|
+
readonly plane: Fmp4FragmentPlane;
|
|
61
|
+
}
|
|
62
|
+
export declare class Fmp4FragmentChild {
|
|
63
|
+
private readonly deps;
|
|
64
|
+
private readonly args;
|
|
65
|
+
private child;
|
|
66
|
+
private splitter;
|
|
67
|
+
private stopped;
|
|
68
|
+
private unitsOut;
|
|
69
|
+
private activeHwAccel;
|
|
70
|
+
constructor(deps: Fmp4FragmentChildDeps, args: Fmp4FragmentChildArgs);
|
|
71
|
+
/** Spawn, and resolve once the INIT segment has been cut out of stdout. */
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
/** The backend the child ACTUALLY ran with — `null` for software. */
|
|
74
|
+
activeDecodeHwAccel(): string | null;
|
|
75
|
+
/** Kill ffmpeg and end the plane. Idempotent. */
|
|
76
|
+
stop(): Promise<void>;
|
|
77
|
+
private spawnAttempt;
|
|
78
|
+
/**
|
|
79
|
+
* The byte stream stopped being splittable. Not recoverable — the splitter
|
|
80
|
+
* cannot resynchronise mid-box — so the child is a corpse and every consumer
|
|
81
|
+
* has to be told, loudly, with the reason.
|
|
82
|
+
*/
|
|
83
|
+
private onFault;
|
|
84
|
+
private killChild;
|
|
85
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { IScopedLogger } from '../interfaces/logging.js';
|
|
2
|
+
import type { Fmp4Unit, Fmp4UnitKind } from './fmp4-box-splitter.js';
|
|
3
|
+
/** One unit as a subscriber receives it. */
|
|
4
|
+
export interface Fmp4Packet {
|
|
5
|
+
readonly kind: Fmp4UnitKind;
|
|
6
|
+
readonly data: Uint8Array;
|
|
7
|
+
/** The producer's sequence number. NOT contiguous per subscriber — a
|
|
8
|
+
* subscriber that attached late starts at the retained init's own number. */
|
|
9
|
+
readonly sequence: number;
|
|
10
|
+
/** The final packet this subscriber will receive. Exactly one, or none. */
|
|
11
|
+
readonly isLast: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Why a subscription stopped. */
|
|
14
|
+
export type Fmp4CloseReason =
|
|
15
|
+
/** The producer ended: the child exited, or the consumer released it. */
|
|
16
|
+
'ended'
|
|
17
|
+
/** The subscriber did not drain fast enough; its stream would have a hole. */
|
|
18
|
+
| 'slow-consumer'
|
|
19
|
+
/** The subscriber closed it. */
|
|
20
|
+
| 'released';
|
|
21
|
+
export interface Fmp4Subscription {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
/** Packets delivered so far — the premise the `isLast` contract rests on. */
|
|
25
|
+
readonly delivered: number;
|
|
26
|
+
/** Non-null once no further packets will arrive. */
|
|
27
|
+
readonly closedReason: Fmp4CloseReason | null;
|
|
28
|
+
/**
|
|
29
|
+
* The packet stream, with real backpressure: the generator suspends until a
|
|
30
|
+
* packet is queued, so a consumer that awaits each packet paces itself.
|
|
31
|
+
* The bounded queue only exists for the case where it does NOT.
|
|
32
|
+
*/
|
|
33
|
+
packets(): AsyncGenerator<Fmp4Packet>;
|
|
34
|
+
/** Stop receiving. Idempotent. */
|
|
35
|
+
release(): void;
|
|
36
|
+
}
|
|
37
|
+
export interface Fmp4SubscriberInfo {
|
|
38
|
+
readonly tag: string;
|
|
39
|
+
readonly subscribedAt: number;
|
|
40
|
+
readonly delivered: number;
|
|
41
|
+
readonly closedReason: Fmp4CloseReason | null;
|
|
42
|
+
}
|
|
43
|
+
export interface Fmp4SubscribeInput {
|
|
44
|
+
/** Short caller identity for diagnostics — `hksv/615`, `clips/615`. */
|
|
45
|
+
readonly tag: string;
|
|
46
|
+
/**
|
|
47
|
+
* Fragments a subscriber may fall behind by before it is closed. Four is two
|
|
48
|
+
* negotiated fragment lengths of slack on a 4 s cadence — enough to ride out
|
|
49
|
+
* a GC pause, far short of the memory a real stall would take.
|
|
50
|
+
*
|
|
51
|
+
* A prebuffer replay is added ON TOP of this, never counted against it.
|
|
52
|
+
*/
|
|
53
|
+
readonly queueCapacity?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Replay the retained prebuffer before live fragments. Off by default: a
|
|
56
|
+
* clip consumer that joins an ongoing recording wants what happens NEXT, and
|
|
57
|
+
* handing it seconds of the past would silently shift its clip's start.
|
|
58
|
+
*/
|
|
59
|
+
readonly withPrebuffer?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** How much past the plane keeps for a {@link Fmp4SubscribeInput.withPrebuffer}. */
|
|
62
|
+
export interface Fmp4PrebufferOptions {
|
|
63
|
+
/**
|
|
64
|
+
* How far back the ring reaches, by fragment ARRIVAL time. HKSV's minimum
|
|
65
|
+
* prebuffer is 4000 ms and the fragment cadence is the source GOP on a copy
|
|
66
|
+
* branch (measured: 8 s on this fleet), so a window shorter than two
|
|
67
|
+
* cadences can hold nothing at all at the moment it is asked for.
|
|
68
|
+
*/
|
|
69
|
+
readonly windowMs: number;
|
|
70
|
+
/**
|
|
71
|
+
* The hard ceiling. Non-negotiable, because the window alone does not bound
|
|
72
|
+
* RAM: same 8 s, 255 KB at 720p and 6.35 MB at 4K on the same cameras.
|
|
73
|
+
*/
|
|
74
|
+
readonly maxBytes: number;
|
|
75
|
+
}
|
|
76
|
+
/** What the ring is holding right now — diagnostics, and a delegate's gate. */
|
|
77
|
+
export interface Fmp4PrebufferStats {
|
|
78
|
+
readonly fragments: number;
|
|
79
|
+
readonly bytes: number;
|
|
80
|
+
/** Age of the OLDEST retained fragment. `0` when the ring is empty. */
|
|
81
|
+
readonly spanMs: number;
|
|
82
|
+
}
|
|
83
|
+
export declare class Fmp4FragmentPlane {
|
|
84
|
+
private readonly logger?;
|
|
85
|
+
private readonly prebuffer?;
|
|
86
|
+
/** Injectable clock — the ring's window is the one thing here that is
|
|
87
|
+
* wall-clock dependent, and a test that sleeps 8 real seconds is a test
|
|
88
|
+
* nobody runs. */
|
|
89
|
+
private readonly now;
|
|
90
|
+
private readonly subscriptions;
|
|
91
|
+
/** The last init unit seen, handed to every later subscriber. */
|
|
92
|
+
private retainedInit;
|
|
93
|
+
private ended;
|
|
94
|
+
/** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
|
|
95
|
+
private readonly ring;
|
|
96
|
+
private ringBytes;
|
|
97
|
+
constructor(logger?: IScopedLogger | undefined, prebuffer?: Fmp4PrebufferOptions | undefined,
|
|
98
|
+
/** Injectable clock — the ring's window is the one thing here that is
|
|
99
|
+
* wall-clock dependent, and a test that sleeps 8 real seconds is a test
|
|
100
|
+
* nobody runs. */
|
|
101
|
+
now?: () => number);
|
|
102
|
+
get subscriberCount(): number;
|
|
103
|
+
/** True once {@link end} has been called — no further units are accepted. */
|
|
104
|
+
get isEnded(): boolean;
|
|
105
|
+
/** What the prebuffer ring holds right now. All zeroes when disabled. */
|
|
106
|
+
prebufferStats(): Fmp4PrebufferStats;
|
|
107
|
+
subscribe(input: Fmp4SubscribeInput): Fmp4Subscription;
|
|
108
|
+
/**
|
|
109
|
+
* Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
|
|
110
|
+
* emits exactly one per child, and a second means the child was respawned, in
|
|
111
|
+
* which case the old one describes a stream that no longer exists.
|
|
112
|
+
*/
|
|
113
|
+
publish(unit: Fmp4Unit): void;
|
|
114
|
+
/**
|
|
115
|
+
* The producer stopped. Every subscriber drains what it holds; its final
|
|
116
|
+
* packet carries `isLast`, and its generator then completes.
|
|
117
|
+
*/
|
|
118
|
+
end(reason?: string): void;
|
|
119
|
+
listSubscribers(): readonly Fmp4SubscriberInfo[];
|
|
120
|
+
/** End and forget everything. Idempotent. */
|
|
121
|
+
dispose(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Add one fragment to the ring and evict from the front until BOTH bounds
|
|
124
|
+
* hold. Eviction is oldest-first, which is the one place in this file where
|
|
125
|
+
* dropping is correct: the ring is context, not stream — nobody is mid-decode
|
|
126
|
+
* on it, and a subscriber only ever receives a contiguous tail of it.
|
|
127
|
+
*/
|
|
128
|
+
private retain;
|
|
129
|
+
/**
|
|
130
|
+
* The ring as a subscriber should receive it — window applied AT SUBSCRIBE
|
|
131
|
+
* time, not only at publish time. A camera that went quiet keeps its last
|
|
132
|
+
* fragment in the ring indefinitely (see the never-evict-the-newest rule),
|
|
133
|
+
* and replaying a 40-second-old fragment as "prebuffer" would put stale video
|
|
134
|
+
* at the head of a clip iOS presents as the moment of the event.
|
|
135
|
+
*/
|
|
136
|
+
private trimmedRing;
|
|
137
|
+
private enqueue;
|
|
138
|
+
private closeSubscription;
|
|
139
|
+
private wake;
|
|
140
|
+
private facade;
|
|
141
|
+
private iterate;
|
|
142
|
+
}
|
|
@@ -198,8 +198,10 @@ export interface FfmpegFmp4Sink {
|
|
|
198
198
|
readonly container: 'mp4';
|
|
199
199
|
/**
|
|
200
200
|
* The negotiated fragment length in milliseconds. Emitted as
|
|
201
|
-
* `-min_frag_duration` (microseconds)
|
|
202
|
-
*
|
|
201
|
+
* `-min_frag_duration` (microseconds) at {@link FMP4_MIN_FRAG_MARGIN} of this
|
|
202
|
+
* value — read that constant before changing anything here, the margin is
|
|
203
|
+
* what stops a camera whose GOP equals `fragmentMs` emitting fragments at
|
|
204
|
+
* twice the requested length.
|
|
203
205
|
*/
|
|
204
206
|
readonly fragmentMs: number;
|
|
205
207
|
}
|