@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.
Files changed (38) hide show
  1. package/dist/addon.js +8 -2
  2. package/dist/addon.mjs +8 -2
  3. package/dist/capabilities/index.d.ts +21 -3
  4. package/dist/capabilities/osd-manager.cap.d.ts +900 -0
  5. package/dist/capabilities/pipeline-analytics.cap.d.ts +1192 -74
  6. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +10 -0
  7. package/dist/capabilities/pipeline-runner.cap.d.ts +6 -0
  8. package/dist/capabilities/schemas/streaming-shared.d.ts +2 -0
  9. package/dist/capabilities/server-management.cap.d.ts +3 -3
  10. package/dist/capabilities/stream-broker.cap.d.ts +32 -0
  11. package/dist/ffmpeg/fmp4-box-splitter.d.ts +113 -0
  12. package/dist/ffmpeg/fmp4-fragment-child.d.ts +85 -0
  13. package/dist/ffmpeg/fmp4-fragment-plane.d.ts +142 -0
  14. package/dist/ffmpeg/invocation.d.ts +4 -2
  15. package/dist/fmp4-box-splitter-B53u9-Nu.mjs +615 -0
  16. package/dist/fmp4-box-splitter-BkWH7O3L.js +686 -0
  17. package/dist/generated/addon-api.d.ts +162 -0
  18. package/dist/generated/cap-input-defaults.d.ts +1 -1
  19. package/dist/generated/capability-router-map.d.ts +5 -2
  20. package/dist/generated/device-proxy.d.ts +3 -1
  21. package/dist/generated/method-access-map.d.ts +1 -1
  22. package/dist/generated/system-proxy.d.ts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +1114 -407
  25. package/dist/index.mjs +1075 -397
  26. package/dist/interfaces/camera-switches.d.ts +48 -0
  27. package/dist/interfaces/stream-broker.d.ts +19 -49
  28. package/dist/node.d.ts +4 -0
  29. package/dist/node.js +509 -3
  30. package/dist/node.mjs +507 -3
  31. package/dist/notification/schedule.d.ts +20 -0
  32. package/dist/{sleep-Bx9IIoT0.js → sleep-BOI-sVEA.js} +37 -1
  33. package/dist/{sleep-DtstvzWm.mjs → sleep-Bf5fBs7u.mjs} +37 -1
  34. package/dist/types/detection.d.ts +31 -0
  35. package/dist/types/pipeline-step.d.ts +22 -1
  36. package/package.json +1 -1
  37. package/dist/canonical-hash-7nfBbEqR.mjs +0 -35
  38. package/dist/canonical-hash-BcZHRHIx.js +0 -40
package/dist/node.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { t as canonicalHash } from "./canonical-hash-7nfBbEqR.mjs";
1
+ import { n as canonicalHash, o as buildFfmpegArgs, t as Fmp4BoxSplitter, u as isSoftwareDecode } from "./fmp4-box-splitter-B53u9-Nu.mjs";
2
2
  import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
@@ -899,4 +899,508 @@ var FfmpegProcess = class {
899
899
  }
900
900
  };
901
901
  //#endregion
902
- export { FfmpegProcess, FilesystemStorageProvider, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint };
902
+ //#region src/ffmpeg/fmp4-fragment-plane.ts
903
+ /**
904
+ * Fmp4FragmentPlane — a SUBSCRIBABLE fragmented-MP4 plane, fed by one
905
+ * {@link import('./fmp4-box-splitter.js').Fmp4BoxSplitter}.
906
+ *
907
+ * ## Why a plane and not a callback
908
+ *
909
+ * The operator's requirement for HKSV was explicit: the live fMP4 source built
910
+ * for it must be **dual-use**, so a HomeKit-triggered recording also lands in
911
+ * CamStack as an additional videoclip source alongside the recorder and the NC
912
+ * clip ring — *one fragmenter, two consumers; do not build an HKSV-only pipe*
913
+ * (`docs/roadmap.md` item 4b). A single-callback pipe makes the second consumer
914
+ * a second ffmpeg child of the same camera. So this is the same shape the
915
+ * broker's other multi-consumer surfaces already have
916
+ * (`AudioChunkPlane`, the push packet plane): N independent subscriptions over
917
+ * one producer.
918
+ *
919
+ * **Nothing consumes it yet.** Phase 4 brings the HKSV delegate and phase 4b the
920
+ * clip source; both are named here so the seam is not re-invented, and neither
921
+ * is built.
922
+ *
923
+ * ## The init segment is RETAINED
924
+ *
925
+ * A subscriber that attaches mid-stream — the clip consumer joining an already
926
+ * running HKSV session, which is the whole dual-use case — receives the
927
+ * retained `ftyp`+`moov` as its first packet and then live fragments. Without
928
+ * retention its fragments are undecodable and the failure looks like a codec
929
+ * problem.
930
+ *
931
+ * ## A slow subscriber is CLOSED, never silently gapped
932
+ *
933
+ * `AudioChunkPlane` drops its oldest chunk on overflow, which for audio costs a
934
+ * click. An fMP4 stream with a hole is not a shorter clip, it is a corrupt one:
935
+ * `moof` sequence numbers jump, the consumer's demuxer desynchronises, and HKSV
936
+ * shows a clip that fails to play with nothing anywhere saying why. So a
937
+ * subscription whose queue overflows is ENDED with a reason, loudly, and the
938
+ * other subscriptions are untouched.
939
+ *
940
+ * ## The PREBUFFER (phase 3)
941
+ *
942
+ * HKSV asks for context BEFORE the trigger — `CameraRecordingOptions.prebufferLength`
943
+ * is a HAP-mandated minimum of 4000 ms — and a subscriber that attaches at the
944
+ * motion edge has none. So the plane optionally retains the last few fragments
945
+ * and replays them to a subscriber that asks for them.
946
+ *
947
+ * Three things this ring gets right, each of which is a measured fact rather
948
+ * than a preference (see [D84](../../../../docs/decisions/adr-0084.md)):
949
+ *
950
+ * - **It is bounded by TIME *and* BYTES.** On the live fleet a 720p copy
951
+ * fragment is ~255 KB and a 4K one is ~6.35 MB — a 25× spread over the same
952
+ * window. A time-only bound is a per-camera RAM figure nobody can predict.
953
+ * - **The window is measured on ARRIVAL, not parsed from `tfdt`.** The
954
+ * splitter deliberately never computes a fragment's duration (a second
955
+ * opinion about a fact the muxer owns), and a prebuffer cares about how long
956
+ * ago the bytes turned up, which is exactly what arrival time answers.
957
+ * - **A replay is not backlog.** A subscriber taking N retained fragments gets
958
+ * its queue capacity raised by N for them, because closing a subscriber as a
959
+ * slow consumer for the prebuffer it explicitly asked for would be the
960
+ * stupidest possible failure — and, with `DEFAULT_QUEUE_CAPACITY` of 4 and a
961
+ * ring of 4, the guaranteed one.
962
+ *
963
+ * ## `isLast`
964
+ *
965
+ * hap-nodejs requires the delegate to mark exactly one `RecordingPacket` with
966
+ * `isLast` — a generator that finishes without it produces the twelve-second
967
+ * timeout loop [D50](../../../../../docs/decisions/adr-0050.md) deleted. The
968
+ * plane therefore computes it at DELIVERY time: a packet is last when the plane
969
+ * has ended and nothing remains queued behind it. A subscription that ends
970
+ * having delivered NOTHING says so through {@link Fmp4Subscription.delivered};
971
+ * the future delegate must not open an HDS stream it cannot feed.
972
+ */
973
+ var DEFAULT_QUEUE_CAPACITY = 4;
974
+ var Fmp4FragmentPlane = class {
975
+ logger;
976
+ prebuffer;
977
+ now;
978
+ subscriptions = /* @__PURE__ */ new Map();
979
+ /** The last init unit seen, handed to every later subscriber. */
980
+ retainedInit = null;
981
+ ended = false;
982
+ /** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
983
+ ring = [];
984
+ ringBytes = 0;
985
+ constructor(logger, prebuffer, now = Date.now) {
986
+ this.logger = logger;
987
+ this.prebuffer = prebuffer;
988
+ this.now = now;
989
+ }
990
+ get subscriberCount() {
991
+ return this.subscriptions.size;
992
+ }
993
+ /** True once {@link end} has been called — no further units are accepted. */
994
+ get isEnded() {
995
+ return this.ended;
996
+ }
997
+ /** What the prebuffer ring holds right now. All zeroes when disabled. */
998
+ prebufferStats() {
999
+ const oldest = this.ring[0];
1000
+ return {
1001
+ fragments: this.ring.length,
1002
+ bytes: this.ringBytes,
1003
+ spanMs: oldest === void 0 ? 0 : this.now() - oldest.arrivedAt
1004
+ };
1005
+ }
1006
+ subscribe(input) {
1007
+ const replay = input.withPrebuffer === true ? this.trimmedRing() : [];
1008
+ const requested = Math.max(1, input.queueCapacity ?? DEFAULT_QUEUE_CAPACITY);
1009
+ const sub = {
1010
+ id: `fmp4-${randomUUID()}`,
1011
+ tag: input.tag,
1012
+ subscribedAt: this.now(),
1013
+ capacity: requested + replay.length,
1014
+ queue: [],
1015
+ delivered: 0,
1016
+ closedReason: null,
1017
+ wake: null,
1018
+ iterating: false
1019
+ };
1020
+ this.subscriptions.set(sub.id, sub);
1021
+ if (this.retainedInit !== null) this.enqueue(sub, this.retainedInit);
1022
+ for (const retained of replay) this.enqueue(sub, retained.unit);
1023
+ if (this.ended) this.closeSubscription(sub, "ended");
1024
+ this.logger?.info("fmp4 plane: subscribed", { meta: {
1025
+ subscriptionId: sub.id,
1026
+ tag: sub.tag,
1027
+ hasRetainedInit: this.retainedInit !== null,
1028
+ prebufferFragments: replay.length,
1029
+ prebufferBytes: replay.reduce((n, r) => n + r.unit.data.length, 0)
1030
+ } });
1031
+ return this.facade(sub);
1032
+ }
1033
+ /**
1034
+ * Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
1035
+ * emits exactly one per child, and a second means the child was respawned, in
1036
+ * which case the old one describes a stream that no longer exists.
1037
+ */
1038
+ publish(unit) {
1039
+ if (this.ended) return;
1040
+ if (unit.kind === "init") {
1041
+ this.retainedInit = unit;
1042
+ this.ring.length = 0;
1043
+ this.ringBytes = 0;
1044
+ } else this.retain(unit);
1045
+ for (const sub of this.subscriptions.values()) {
1046
+ if (sub.closedReason !== null) continue;
1047
+ this.enqueue(sub, unit);
1048
+ }
1049
+ }
1050
+ /**
1051
+ * The producer stopped. Every subscriber drains what it holds; its final
1052
+ * packet carries `isLast`, and its generator then completes.
1053
+ */
1054
+ end(reason = "producer ended") {
1055
+ if (this.ended) return;
1056
+ this.ended = true;
1057
+ this.logger?.info("fmp4 plane: ended", { meta: {
1058
+ reason,
1059
+ subscribers: this.subscriptions.size
1060
+ } });
1061
+ for (const sub of this.subscriptions.values()) if (sub.closedReason === null) this.closeSubscription(sub, "ended");
1062
+ }
1063
+ listSubscribers() {
1064
+ return [...this.subscriptions.values()].map((s) => ({
1065
+ tag: s.tag,
1066
+ subscribedAt: s.subscribedAt,
1067
+ delivered: s.delivered,
1068
+ closedReason: s.closedReason
1069
+ }));
1070
+ }
1071
+ /** End and forget everything. Idempotent. */
1072
+ dispose() {
1073
+ this.end("disposed");
1074
+ this.subscriptions.clear();
1075
+ this.retainedInit = null;
1076
+ this.ring.length = 0;
1077
+ this.ringBytes = 0;
1078
+ }
1079
+ /**
1080
+ * Add one fragment to the ring and evict from the front until BOTH bounds
1081
+ * hold. Eviction is oldest-first, which is the one place in this file where
1082
+ * dropping is correct: the ring is context, not stream — nobody is mid-decode
1083
+ * on it, and a subscriber only ever receives a contiguous tail of it.
1084
+ */
1085
+ retain(unit) {
1086
+ const prebuffer = this.prebuffer;
1087
+ if (prebuffer === void 0) return;
1088
+ const arrivedAt = this.now();
1089
+ this.ring.push({
1090
+ unit,
1091
+ arrivedAt
1092
+ });
1093
+ this.ringBytes += unit.data.length;
1094
+ const cutoff = arrivedAt - prebuffer.windowMs;
1095
+ while (this.ring.length > 0) {
1096
+ const oldest = this.ring[0];
1097
+ if (oldest === void 0) break;
1098
+ const tooOld = oldest.arrivedAt < cutoff;
1099
+ const tooBig = this.ringBytes > prebuffer.maxBytes;
1100
+ if (!tooOld && !tooBig || this.ring.length === 1) break;
1101
+ this.ring.shift();
1102
+ this.ringBytes -= oldest.unit.data.length;
1103
+ }
1104
+ }
1105
+ /**
1106
+ * The ring as a subscriber should receive it — window applied AT SUBSCRIBE
1107
+ * time, not only at publish time. A camera that went quiet keeps its last
1108
+ * fragment in the ring indefinitely (see the never-evict-the-newest rule),
1109
+ * and replaying a 40-second-old fragment as "prebuffer" would put stale video
1110
+ * at the head of a clip iOS presents as the moment of the event.
1111
+ */
1112
+ trimmedRing() {
1113
+ const prebuffer = this.prebuffer;
1114
+ if (prebuffer === void 0) return [];
1115
+ const cutoff = this.now() - prebuffer.windowMs;
1116
+ return this.ring.filter((r) => r.arrivedAt >= cutoff);
1117
+ }
1118
+ enqueue(sub, unit) {
1119
+ if (sub.queue.length >= sub.capacity) {
1120
+ this.logger?.warn("fmp4 plane: subscriber fell behind — CLOSING it rather than gapping it", { meta: {
1121
+ subscriptionId: sub.id,
1122
+ tag: sub.tag,
1123
+ capacity: sub.capacity,
1124
+ delivered: sub.delivered
1125
+ } });
1126
+ this.closeSubscription(sub, "slow-consumer");
1127
+ return;
1128
+ }
1129
+ sub.queue.push({
1130
+ kind: unit.kind,
1131
+ data: unit.data,
1132
+ sequence: unit.sequence,
1133
+ isLast: false
1134
+ });
1135
+ this.wake(sub);
1136
+ }
1137
+ closeSubscription(sub, reason) {
1138
+ if (sub.closedReason !== null) return;
1139
+ sub.closedReason = reason;
1140
+ if (reason === "slow-consumer") sub.queue.length = 0;
1141
+ this.wake(sub);
1142
+ }
1143
+ wake(sub) {
1144
+ const resume = sub.wake;
1145
+ sub.wake = null;
1146
+ resume?.();
1147
+ }
1148
+ facade(sub) {
1149
+ const plane = this;
1150
+ return {
1151
+ id: sub.id,
1152
+ tag: sub.tag,
1153
+ get delivered() {
1154
+ return sub.delivered;
1155
+ },
1156
+ get closedReason() {
1157
+ return sub.closedReason;
1158
+ },
1159
+ packets: () => plane.iterate(sub),
1160
+ release: () => {
1161
+ plane.closeSubscription(sub, "released");
1162
+ plane.subscriptions.delete(sub.id);
1163
+ }
1164
+ };
1165
+ }
1166
+ async *iterate(sub) {
1167
+ if (sub.iterating) throw new Error(`fmp4 plane: subscription ${sub.tag} is already being consumed — take a second subscription`);
1168
+ sub.iterating = true;
1169
+ for (;;) {
1170
+ const next = sub.queue.shift();
1171
+ if (next === void 0) {
1172
+ if (sub.closedReason !== null) return;
1173
+ await new Promise((resolve) => {
1174
+ sub.wake = resolve;
1175
+ });
1176
+ continue;
1177
+ }
1178
+ const isLast = sub.closedReason === "ended" && sub.queue.length === 0;
1179
+ sub.delivered += 1;
1180
+ yield {
1181
+ ...next,
1182
+ isLast
1183
+ };
1184
+ if (isLast) return;
1185
+ }
1186
+ }
1187
+ };
1188
+ //#endregion
1189
+ //#region src/ffmpeg/fmp4-fragment-child.ts
1190
+ var DEFAULT_FIRST_UNIT_TIMEOUT_MS = 12e3;
1191
+ /** Heartbeat cadence — ~2 minutes of 4 s fragments. */
1192
+ var FRAGMENT_LOG_EVERY = 30;
1193
+ var Fmp4FragmentChild = class {
1194
+ deps;
1195
+ args;
1196
+ child = null;
1197
+ splitter = new Fmp4BoxSplitter();
1198
+ stopped = false;
1199
+ unitsOut = 0;
1200
+ activeHwAccel = null;
1201
+ constructor(deps, args) {
1202
+ this.deps = deps;
1203
+ this.args = args;
1204
+ }
1205
+ /** Spawn, and resolve once the INIT segment has been cut out of stdout. */
1206
+ async start() {
1207
+ const requested = this.args.invocation.decodeHwAccel;
1208
+ this.activeHwAccel = requested;
1209
+ try {
1210
+ await this.spawnAttempt(requested);
1211
+ return;
1212
+ } catch (err) {
1213
+ if (this.stopped) throw err;
1214
+ if (requested === null || isSoftwareDecode(requested)) throw err;
1215
+ this.deps.logger.warn("fmp4 fragment child: hardware decode produced NO fragment — retrying in SOFTWARE", {
1216
+ tags: { deviceId: this.args.deviceId },
1217
+ meta: {
1218
+ sourceId: this.args.sourceId,
1219
+ decodeHwAccel: requested,
1220
+ error: errMsg(err)
1221
+ }
1222
+ });
1223
+ this.killChild();
1224
+ this.splitter = new Fmp4BoxSplitter();
1225
+ this.activeHwAccel = null;
1226
+ await this.spawnAttempt(null);
1227
+ }
1228
+ }
1229
+ /** The backend the child ACTUALLY ran with — `null` for software. */
1230
+ activeDecodeHwAccel() {
1231
+ const value = this.activeHwAccel;
1232
+ return value === null || value === "none" || value === "copy" ? null : value;
1233
+ }
1234
+ /** Kill ffmpeg and end the plane. Idempotent. */
1235
+ async stop() {
1236
+ if (this.stopped) return;
1237
+ this.stopped = true;
1238
+ this.killChild();
1239
+ this.args.plane.end("the fragment child stopped");
1240
+ }
1241
+ spawnAttempt(decodeHwAccel) {
1242
+ const args = buildFfmpegArgs({
1243
+ ...this.args.invocation,
1244
+ decodeHwAccel,
1245
+ sink: {
1246
+ kind: "stdout",
1247
+ container: "mp4",
1248
+ fragmentMs: this.args.fragmentMs
1249
+ }
1250
+ });
1251
+ this.deps.logger.info("fmp4 fragment child: spawning ffmpeg", {
1252
+ tags: { deviceId: this.args.deviceId },
1253
+ meta: {
1254
+ sourceId: this.args.sourceId,
1255
+ fragmentMs: this.args.fragmentMs,
1256
+ decodeHwAccel: decodeHwAccel ?? "software",
1257
+ argv: args.join(" ")
1258
+ }
1259
+ });
1260
+ return new Promise((resolve, reject) => {
1261
+ const child = this.deps.spawnFn(this.deps.ffmpegBinaryPath, args, { stdio: [
1262
+ "ignore",
1263
+ "pipe",
1264
+ "pipe"
1265
+ ] });
1266
+ this.child = child;
1267
+ let settled = false;
1268
+ /**
1269
+ * This attempt FAILED. Set before the kill, because SIGTERM makes the
1270
+ * child exit and that exit must not be reported as a death: the retry —
1271
+ * or the caller's rejection — already owns what happens next. Without it
1272
+ * the timeout path ends the plane the software retry is about to fill,
1273
+ * and the consumer sees a stream that stopped for no reason. A "which
1274
+ * spawn is current" counter does NOT cover this: the retry has not been
1275
+ * spawned when the kill's exit arrives.
1276
+ */
1277
+ let failed = false;
1278
+ /**
1279
+ * This attempt is still the live producer: it has not failed (a failure
1280
+ * hands ownership to the retry, or to the caller's rejection) and nothing
1281
+ * has stopped the child. Those two cover every way an attempt stops being
1282
+ * current — `start` only respawns after a rejection.
1283
+ */
1284
+ const isCurrent = () => !this.stopped && !failed;
1285
+ const timeoutMs = this.deps.firstUnitTimeoutMs ?? DEFAULT_FIRST_UNIT_TIMEOUT_MS;
1286
+ const settle = (fail) => {
1287
+ if (settled) return;
1288
+ settled = true;
1289
+ clearTimeout(timer);
1290
+ if (fail) {
1291
+ failed = true;
1292
+ reject(fail);
1293
+ } else resolve();
1294
+ };
1295
+ const timer = setTimeout(() => {
1296
+ settle(/* @__PURE__ */ new Error(`fmp4 fragment child: no fragment within ${timeoutMs}ms`));
1297
+ this.killChild();
1298
+ }, timeoutMs);
1299
+ timer.unref?.();
1300
+ child.stdout?.on("data", (chunk) => {
1301
+ for (const unit of this.splitter.push(chunk)) {
1302
+ this.unitsOut += 1;
1303
+ this.args.plane.publish(unit);
1304
+ if (unit.kind === "init") {
1305
+ this.deps.logger.info("fmp4 fragment child: INIT segment cut", {
1306
+ tags: { deviceId: this.args.deviceId },
1307
+ meta: {
1308
+ sourceId: this.args.sourceId,
1309
+ bytes: unit.data.length
1310
+ }
1311
+ });
1312
+ settle();
1313
+ } else if (this.unitsOut % FRAGMENT_LOG_EVERY === 0) this.deps.logger.info("fmp4 fragment child: fragments still flowing", {
1314
+ tags: { deviceId: this.args.deviceId },
1315
+ meta: {
1316
+ sourceId: this.args.sourceId,
1317
+ unitsOut: this.unitsOut,
1318
+ bytes: unit.data.length,
1319
+ subscribers: this.args.plane.subscriberCount
1320
+ }
1321
+ });
1322
+ }
1323
+ const fault = this.splitter.fault;
1324
+ if (fault !== null) this.onFault(fault, settled, isCurrent(), settle);
1325
+ });
1326
+ child.stderr?.setEncoding("utf8");
1327
+ child.stderr?.on("data", (line) => {
1328
+ this.deps.logger.debug("fmp4 fragment child ffmpeg", {
1329
+ tags: { deviceId: this.args.deviceId },
1330
+ meta: {
1331
+ sourceId: this.args.sourceId,
1332
+ line: line.trim()
1333
+ }
1334
+ });
1335
+ });
1336
+ child.once("error", (err) => {
1337
+ if (!settled) {
1338
+ settle(err);
1339
+ return;
1340
+ }
1341
+ if (!isCurrent()) return;
1342
+ this.args.plane.end("the fragment child errored");
1343
+ this.deps.onChildExit?.(err);
1344
+ });
1345
+ child.once("exit", (code, signal) => {
1346
+ if (!settled) {
1347
+ settle(/* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited before any fragment (code=${code} signal=${signal})`));
1348
+ return;
1349
+ }
1350
+ if (!isCurrent()) return;
1351
+ const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited while live (code=${code} signal=${signal})`);
1352
+ this.deps.logger.warn("fmp4 fragment child: ffmpeg exited while live", {
1353
+ tags: { deviceId: this.args.deviceId },
1354
+ meta: {
1355
+ sourceId: this.args.sourceId,
1356
+ code,
1357
+ signal,
1358
+ unitsOut: this.unitsOut
1359
+ }
1360
+ });
1361
+ this.args.plane.end("the fragment child exited");
1362
+ this.deps.onChildExit?.(error);
1363
+ });
1364
+ });
1365
+ }
1366
+ /**
1367
+ * The byte stream stopped being splittable. Not recoverable — the splitter
1368
+ * cannot resynchronise mid-box — so the child is a corpse and every consumer
1369
+ * has to be told, loudly, with the reason.
1370
+ */
1371
+ onFault(reason, wasLive, current, settle) {
1372
+ const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ${reason}`);
1373
+ this.deps.logger.error("fmp4 fragment child: the ffmpeg output stopped parsing as fMP4", {
1374
+ tags: { deviceId: this.args.deviceId },
1375
+ meta: {
1376
+ sourceId: this.args.sourceId,
1377
+ unitsOut: this.unitsOut,
1378
+ interstitial: this.splitter.discardedInterstitialTypes,
1379
+ reason
1380
+ }
1381
+ });
1382
+ this.killChild();
1383
+ settle(error);
1384
+ if (wasLive && current) {
1385
+ this.args.plane.end("the fragment child produced unsplittable output");
1386
+ this.deps.onChildExit?.(error);
1387
+ }
1388
+ }
1389
+ killChild() {
1390
+ const child = this.child;
1391
+ this.child = null;
1392
+ if (child && !child.killed) try {
1393
+ child.kill("SIGTERM");
1394
+ } catch (err) {
1395
+ this.deps.logger.warn("fmp4 fragment child: kill error", {
1396
+ tags: { deviceId: this.args.deviceId },
1397
+ meta: {
1398
+ sourceId: this.args.sourceId,
1399
+ error: errMsg(err)
1400
+ }
1401
+ });
1402
+ }
1403
+ }
1404
+ };
1405
+ //#endregion
1406
+ export { FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Schedule-window evaluation for the notification-rule vocabulary.
3
+ *
4
+ * Hoisted out of the notification-centre's rule engine when a SECOND
5
+ * consumer appeared (the OSD manager gates an overlay on the same
6
+ * `NcSchedule`). Addons never import each other, so the alternative was a
7
+ * copy — and this repo has already recorded what a second copy of a
8
+ * vocabulary costs (`condition-taxonomy.ts`): two implementations, free to
9
+ * drift, with nothing that fails when they do.
10
+ *
11
+ * Pure: no clock of its own, `atMs` is always supplied by the caller.
12
+ */
13
+ import type { NcSchedule } from '../capabilities/notification-rules.cap.js';
14
+ /**
15
+ * Is the schedule active at `atMs`? No schedule = always active. Windows
16
+ * are OR'd; a window with `startMinute > endMinute` crosses midnight (it
17
+ * starts on a listed day and spills into the next). `invert` flips the
18
+ * result (active OUTSIDE the windows).
19
+ */
20
+ export declare function isScheduleActive(schedule: NcSchedule | undefined, atMs: number): boolean;
@@ -1445,7 +1445,20 @@ var BrokerStatsSchema = zod.z.object({
1445
1445
  sampleRate: zod.z.number(),
1446
1446
  channels: zod.z.number(),
1447
1447
  supported: zod.z.boolean()
1448
- }).nullable().optional()
1448
+ }).nullable().optional(),
1449
+ /**
1450
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
1451
+ * distributing none of the device's audio, on live or recording.
1452
+ *
1453
+ * Present so a silent camera can be told apart from a broken one on the
1454
+ * stream panel itself, without cross-referencing the switch group: a
1455
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
1456
+ * working exactly as asked. `audioMutedDropped` counts the audio units
1457
+ * thrown away since the current dial — it is how you confirm from stats
1458
+ * alone that the mute is on the packet path and not merely persisted.
1459
+ */
1460
+ audioMuted: zod.z.boolean().optional(),
1461
+ audioMutedDropped: zod.z.number().optional()
1449
1462
  });
1450
1463
  /**
1451
1464
  * Exporter-facing "profile restream" entry. Returned by
@@ -3192,6 +3205,19 @@ function createDeviceProxy(api, binding, opts) {
3192
3205
  getMediaRelocateStatus: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getMediaRelocateStatus", "query", input),
3193
3206
  cancelMediaRelocate: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "cancelMediaRelocate", "mutation", input),
3194
3207
  listOpsLog: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listOpsLog", "query", input),
3208
+ getTrainingExportSummary: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrainingExportSummary", "query", input),
3209
+ getTrainingExportUrl: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrainingExportUrl", "query", input),
3210
+ listRetrainStaging: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainStaging", "query", input),
3211
+ listRetrainFrames: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainFrames", "query", input),
3212
+ selectRetrainFrames: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "selectRetrainFrames", "mutation", input),
3213
+ deselectRetrainFrame: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deselectRetrainFrame", "mutation", input),
3214
+ getRetrainFrameImage: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getRetrainFrameImage", "query", input),
3215
+ proposeRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "proposeRetrainAnnotations", "mutation", input),
3216
+ listRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainAnnotations", "query", input),
3217
+ saveRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "saveRetrainAnnotations", "mutation", input),
3218
+ completeRetrainTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "completeRetrainTrack", "mutation", input),
3219
+ restageRetrainTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "restageRetrainTrack", "mutation", input),
3220
+ getRetrainExportUrl: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getRetrainExportUrl", "query", input),
3195
3221
  getEventMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventMedia", "query", input),
3196
3222
  getTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrackMedia", "query", input),
3197
3223
  listTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listTrackMedia", "query", input),
@@ -3400,6 +3426,14 @@ function createDeviceProxy(api, binding, opts) {
3400
3426
  getDeviceStats: (input) => dispatchSystem("networkQuality", "getDeviceStats", "query", input),
3401
3427
  reportClientStats: (input) => dispatchSystem("networkQuality", "reportClientStats", "mutation", input)
3402
3428
  },
3429
+ osdManager: {
3430
+ getDeviceOsd: (input) => dispatchSystem("osdManager", "getDeviceOsd", "query", input),
3431
+ getSourceCatalog: (input) => dispatchSystem("osdManager", "getSourceCatalog", "query", input),
3432
+ setSlotBinding: (input) => dispatchSystem("osdManager", "setSlotBinding", "mutation", input),
3433
+ clearSlotBinding: (input) => dispatchSystem("osdManager", "clearSlotBinding", "mutation", input),
3434
+ previewSlot: (input) => dispatchSystem("osdManager", "previewSlot", "mutation", input),
3435
+ renderDevice: (input) => dispatchSystem("osdManager", "renderDevice", "mutation", input)
3436
+ },
3403
3437
  pipelineExecutor: {
3404
3438
  runPipeline: (input) => dispatchSystem("pipelineExecutor", "runPipeline", "mutation", input),
3405
3439
  runPipelineBatch: (input) => dispatchSystem("pipelineExecutor", "runPipelineBatch", "mutation", input)
@@ -3467,6 +3501,8 @@ function createDeviceProxy(api, binding, opts) {
3467
3501
  unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
3468
3502
  renderPreBufferClip: (input) => dispatchSystem("streamBroker", "renderPreBufferClip", "mutation", input),
3469
3503
  restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
3504
+ getDeviceAudioMute: (input) => dispatchSystem("streamBroker", "getDeviceAudioMute", "query", input),
3505
+ setDeviceAudioMute: (input) => dispatchSystem("streamBroker", "setDeviceAudioMute", "mutation", input),
3470
3506
  getDeviceSettingsContribution: (input) => dispatchSystem("streamBroker", "getDeviceSettingsContribution", "query", input),
3471
3507
  getDeviceLiveContribution: (input) => dispatchSystem("streamBroker", "getDeviceLiveContribution", "query", input),
3472
3508
  applyDeviceSettingsPatch: (input) => dispatchSystem("streamBroker", "applyDeviceSettingsPatch", "mutation", input)
@@ -1445,7 +1445,20 @@ var BrokerStatsSchema = z.object({
1445
1445
  sampleRate: z.number(),
1446
1446
  channels: z.number(),
1447
1447
  supported: z.boolean()
1448
- }).nullable().optional()
1448
+ }).nullable().optional(),
1449
+ /**
1450
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
1451
+ * distributing none of the device's audio, on live or recording.
1452
+ *
1453
+ * Present so a silent camera can be told apart from a broken one on the
1454
+ * stream panel itself, without cross-referencing the switch group: a
1455
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
1456
+ * working exactly as asked. `audioMutedDropped` counts the audio units
1457
+ * thrown away since the current dial — it is how you confirm from stats
1458
+ * alone that the mute is on the packet path and not merely persisted.
1459
+ */
1460
+ audioMuted: z.boolean().optional(),
1461
+ audioMutedDropped: z.number().optional()
1449
1462
  });
1450
1463
  /**
1451
1464
  * Exporter-facing "profile restream" entry. Returned by
@@ -3192,6 +3205,19 @@ function createDeviceProxy(api, binding, opts) {
3192
3205
  getMediaRelocateStatus: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getMediaRelocateStatus", "query", input),
3193
3206
  cancelMediaRelocate: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "cancelMediaRelocate", "mutation", input),
3194
3207
  listOpsLog: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listOpsLog", "query", input),
3208
+ getTrainingExportSummary: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrainingExportSummary", "query", input),
3209
+ getTrainingExportUrl: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrainingExportUrl", "query", input),
3210
+ listRetrainStaging: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainStaging", "query", input),
3211
+ listRetrainFrames: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainFrames", "query", input),
3212
+ selectRetrainFrames: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "selectRetrainFrames", "mutation", input),
3213
+ deselectRetrainFrame: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "deselectRetrainFrame", "mutation", input),
3214
+ getRetrainFrameImage: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getRetrainFrameImage", "query", input),
3215
+ proposeRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "proposeRetrainAnnotations", "mutation", input),
3216
+ listRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listRetrainAnnotations", "query", input),
3217
+ saveRetrainAnnotations: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "saveRetrainAnnotations", "mutation", input),
3218
+ completeRetrainTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "completeRetrainTrack", "mutation", input),
3219
+ restageRetrainTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "restageRetrainTrack", "mutation", input),
3220
+ getRetrainExportUrl: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getRetrainExportUrl", "query", input),
3195
3221
  getEventMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventMedia", "query", input),
3196
3222
  getTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrackMedia", "query", input),
3197
3223
  listTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listTrackMedia", "query", input),
@@ -3400,6 +3426,14 @@ function createDeviceProxy(api, binding, opts) {
3400
3426
  getDeviceStats: (input) => dispatchSystem("networkQuality", "getDeviceStats", "query", input),
3401
3427
  reportClientStats: (input) => dispatchSystem("networkQuality", "reportClientStats", "mutation", input)
3402
3428
  },
3429
+ osdManager: {
3430
+ getDeviceOsd: (input) => dispatchSystem("osdManager", "getDeviceOsd", "query", input),
3431
+ getSourceCatalog: (input) => dispatchSystem("osdManager", "getSourceCatalog", "query", input),
3432
+ setSlotBinding: (input) => dispatchSystem("osdManager", "setSlotBinding", "mutation", input),
3433
+ clearSlotBinding: (input) => dispatchSystem("osdManager", "clearSlotBinding", "mutation", input),
3434
+ previewSlot: (input) => dispatchSystem("osdManager", "previewSlot", "mutation", input),
3435
+ renderDevice: (input) => dispatchSystem("osdManager", "renderDevice", "mutation", input)
3436
+ },
3403
3437
  pipelineExecutor: {
3404
3438
  runPipeline: (input) => dispatchSystem("pipelineExecutor", "runPipeline", "mutation", input),
3405
3439
  runPipelineBatch: (input) => dispatchSystem("pipelineExecutor", "runPipelineBatch", "mutation", input)
@@ -3467,6 +3501,8 @@ function createDeviceProxy(api, binding, opts) {
3467
3501
  unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
3468
3502
  renderPreBufferClip: (input) => dispatchSystem("streamBroker", "renderPreBufferClip", "mutation", input),
3469
3503
  restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
3504
+ getDeviceAudioMute: (input) => dispatchSystem("streamBroker", "getDeviceAudioMute", "query", input),
3505
+ setDeviceAudioMute: (input) => dispatchSystem("streamBroker", "setDeviceAudioMute", "mutation", input),
3470
3506
  getDeviceSettingsContribution: (input) => dispatchSystem("streamBroker", "getDeviceSettingsContribution", "query", input),
3471
3507
  getDeviceLiveContribution: (input) => dispatchSystem("streamBroker", "getDeviceLiveContribution", "query", input),
3472
3508
  applyDeviceSettingsPatch: (input) => dispatchSystem("streamBroker", "applyDeviceSettingsPatch", "mutation", input)