@camstack/addon-pipeline 1.2.274 → 1.2.276

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 (32) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-CAWhHQo2.js → default-detection-model-6lCvep7n.js} +1 -1
  4. package/dist/{default-detection-model-BXwE-Dvo.mjs → default-detection-model-dQSwHHfx.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-g5MfpiBh.mjs → dist-C1pCVJk7.mjs} +440 -32
  8. package/dist/{dist-k6Vu-AhZ.js → dist-NYjn7hkE.js} +469 -31
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-BXsHQH5h.mjs → node--UY0iepi.mjs} +1 -1
  12. package/dist/{node--u_jUObS.js → node-CVYY8a4y.js} +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-Gl3EowL-.mjs → process-memory-BQOfL3Ig.mjs} +1 -1
  16. package/dist/{process-memory-DN7opum2.js → process-memory-DDvuSEMJ.js} +1 -1
  17. package/dist/recorder/index.js +572 -40
  18. package/dist/recorder/index.mjs +572 -40
  19. package/dist/{segment-demux-js-sYP_jndp.mjs → segment-demux-js-B0_aAzN0.mjs} +1 -1
  20. package/dist/{segment-demux-js-DHX8o8pI.js → segment-demux-js-DVupBBc4.js} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CEy8IryH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D0A4Ra4h.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BV2tQ1w3.mjs +26 -0
  24. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-MU3_q1MX.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BnLVuoDg.mjs} +1 -1
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-UKNN4qes.mjs → hostInit-C8TnCvWj.mjs} +2 -2
  28. package/dist/stream-broker/index.js +526 -69
  29. package/dist/stream-broker/index.mjs +526 -69
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BlXyfeXU.mjs +0 -26
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-k6Vu-AhZ.js");
7
- const require_node = require("../node--u_jUObS.js");
6
+ const require_dist = require("../dist-NYjn7hkE.js");
7
+ const require_node = require("../node-CVYY8a4y.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-DHX8o8pI.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-DVupBBc4.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -26570,6 +26570,21 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
26570
26570
  epoch: require_dist.number(),
26571
26571
  profile: require_dist.string()
26572
26572
  }),
26573
+ require_dist.object({
26574
+ t: require_dist.literal("playClip"),
26575
+ deviceId: require_dist.number(),
26576
+ clipId: require_dist.string().min(1),
26577
+ offsetMs: require_dist.number().min(0),
26578
+ /**
26579
+ * Which TWIN of the clip — the vendor's own quality pair, on the one
26580
+ * quality scale the system has. `auto` is deliberately not in
26581
+ * `CamProfileSchema`'s domain here for the same reason
26582
+ * `videoclips.getClipPlayback` refuses it: a stored file has no broker
26583
+ * session to resolve an adaptive tier against. Absent ⇒ the provider's own
26584
+ * default; never invented here.
26585
+ */
26586
+ profile: require_dist.CamProfileSchema.optional()
26587
+ }),
26573
26588
  require_dist.object({
26574
26589
  t: require_dist.literal("seek"),
26575
26590
  epoch: require_dist.number()
@@ -29889,6 +29904,18 @@ var TimelineSession = class {
29889
29904
  * old recorded position is not where the viewer is coming from.
29890
29905
  */
29891
29906
  lastPositionMs = null;
29907
+ /**
29908
+ * The clip the pointer is on, or `null` when the recorded source is the
29909
+ * recorder's own footage.
29910
+ *
29911
+ * It is deliberately NOT a third `mode`. Clip playback IS recorded mode with
29912
+ * a different byte source, so `currentMode` still reads `recorded` and every
29913
+ * invariant built on it holds unchanged — the adaptive ladder still suspends
29914
+ * itself while the pointer is off live, and `goLive` still leaves in one
29915
+ * dispatch (D570's go-live terminator: a mode `GO_LIVE` cannot leave is a
29916
+ * loop, and the cheapest way not to have one is not to add the mode).
29917
+ */
29918
+ clip = null;
29892
29919
  constructor(deps) {
29893
29920
  this.deps = deps;
29894
29921
  deps.session.setLiveFeedGate(false);
@@ -29907,13 +29934,26 @@ var TimelineSession = class {
29907
29934
  this.emitLiveProfile();
29908
29935
  switch (msg.t) {
29909
29936
  case "playRecorded":
29937
+ this.leaveClip();
29910
29938
  this.requestedProfile = msg.profile;
29911
29939
  await this.playAt(msg.epoch, msg.profile, { travelFromMs: this.lastPositionMs });
29912
29940
  break;
29941
+ case "playClip":
29942
+ await this.playClip(msg.deviceId, msg.clipId, msg.profile ?? null, msg.offsetMs);
29943
+ break;
29913
29944
  case "seek":
29945
+ if (this.clip) {
29946
+ await this.feeder?.seek(msg.epoch);
29947
+ break;
29948
+ }
29914
29949
  await this.playAt(msg.epoch, this.requestedProfile, { travelFromMs: this.lastPositionMs });
29915
29950
  break;
29916
29951
  case "scrubCommit":
29952
+ if (this.clip) {
29953
+ if (msg.dragId !== void 0) await this.feeder?.seek(msg.epoch, { dragId: msg.dragId });
29954
+ else await this.feeder?.seek(msg.epoch);
29955
+ break;
29956
+ }
29917
29957
  if (msg.dragId !== void 0) await this.playAt(msg.epoch, this.requestedProfile, { dragId: msg.dragId });
29918
29958
  else await this.playAt(msg.epoch, this.requestedProfile);
29919
29959
  break;
@@ -29939,6 +29979,14 @@ var TimelineSession = class {
29939
29979
  if (this.mode === "recorded") this.feeder?.stepFrame(msg.dir);
29940
29980
  break;
29941
29981
  case "setProfile":
29982
+ if (this.clip) {
29983
+ this.deps.onLog?.("clip playback: setProfile ignored — a clip file has no recorded tier", {
29984
+ deviceId: this.clip.deviceId,
29985
+ clipId: this.clip.clipId,
29986
+ requested: msg.profile
29987
+ });
29988
+ break;
29989
+ }
29942
29990
  this.requestedProfile = msg.profile;
29943
29991
  if (msg.profile === this.profile) break;
29944
29992
  this.profile = msg.profile;
@@ -29957,12 +30005,52 @@ var TimelineSession = class {
29957
30005
  this.feeder?.dispose();
29958
30006
  this.deps.session.setLiveFeedGate(true);
29959
30007
  this.mode = "recorded";
29960
- const feeder = this.deps.makeFeeder(this.profile);
30008
+ const clip = this.clip;
30009
+ const makeClip = this.deps.makeClipFeeder;
30010
+ const feeder = clip && makeClip ? makeClip({ handle: clip }) : this.deps.makeFeeder(this.profile);
29961
30011
  if (this.rate !== 1) feeder.setRate(this.rate);
29962
30012
  this.feeder = feeder;
29963
30013
  return feeder;
29964
30014
  }
29965
30015
  /**
30016
+ * Put the pointer on a clip file: same track, same feeder, same control
30017
+ * protocol, on the clip's own axis.
30018
+ *
30019
+ * A session with no clip byte source REFUSES by name — it never gates the
30020
+ * live feed, never builds a feeder and never leaves the operator on a frozen
30021
+ * frame with no explanation (D391: the branch that accepts work and produces
30022
+ * nothing is the same branch as the one that drops it).
30023
+ */
30024
+ async playClip(deviceId, clipId, profile, offsetMs) {
30025
+ if (!this.deps.makeClipFeeder) {
30026
+ this.deps.onLog?.("clip playback: refused — no clip byte source wired on this session (videoclips.readClipBytes absent)", {
30027
+ deviceId,
30028
+ clipId,
30029
+ profile,
30030
+ offsetMs
30031
+ });
30032
+ this._emitState("error");
30033
+ return;
30034
+ }
30035
+ this.clip = {
30036
+ deviceId,
30037
+ clipId,
30038
+ profile
30039
+ };
30040
+ this.lastPositionMs = null;
30041
+ await this.enterRecorded().seek(offsetMs);
30042
+ }
30043
+ /** Drop the clip pointer. Every path back to the recorder's own footage — a
30044
+ * `playRecorded` and `goLive` — goes through here, so there is exactly one
30045
+ * place that can forget to. */
30046
+ leaveClip() {
30047
+ if (!this.clip) return;
30048
+ this.clip = null;
30049
+ this.feeder?.dispose();
30050
+ this.feeder = null;
30051
+ this.mode = "live";
30052
+ }
30053
+ /**
29966
30054
  * Play recorded footage at `epochMs`. If `locate` is wired and the requested
29967
30055
  * profile has no segment, try high → mid → low so events-mode cameras that
29968
30056
  * only kept one tier still play.
@@ -30053,6 +30141,7 @@ var TimelineSession = class {
30053
30141
  }));
30054
30142
  }
30055
30143
  goLive() {
30144
+ this.clip = null;
30056
30145
  this.feeder?.dispose();
30057
30146
  this.feeder = null;
30058
30147
  this.lastPositionMs = null;
@@ -30096,6 +30185,331 @@ var TimelineSession = class {
30096
30185
  }
30097
30186
  };
30098
30187
  //#endregion
30188
+ //#region src/stream-broker/recorded/clip-mp4-index.ts
30189
+ /**
30190
+ * The sample index for a CLIP FILE, so a clip can be another position of the
30191
+ * recorded source pointer (D575).
30192
+ *
30193
+ * `RecordedFeeder` asks its byte source two questions — "is there a segment at
30194
+ * `epochMs`?" and "give me its bytes" — and the recorder answers both from the
30195
+ * segment's own `mfra`. A clip is a progressive `+faststart` MP4: it has a
30196
+ * `moov` at the FRONT and no `mfra` at all, so the time→sample map has to be
30197
+ * read out of `stbl` instead. That is what this module does, and it is the only
30198
+ * place in the clip path that parses MP4 structure.
30199
+ *
30200
+ * **It is a TIME index, not a byte index, and that is deliberate.** The
30201
+ * offsets in a progressive `moov` (`stco`/`stsc`) are absolute to the WHOLE
30202
+ * file, so a GOP-ranged slice of one is not a standalone-demuxable MP4 — making
30203
+ * it one means rewriting `stbl`, i.e. a remux. A clip arrives whole and
30204
+ * already compressed (one bounded by-handle fetch, D9/D18), so the file IS its
30205
+ * byte range, exactly as `RecordedFeederDeps.readGop` says of the playback
30206
+ * path. Parsing offsets nobody reads would be a leftover describing a design
30207
+ * this slice does not have.
30208
+ *
30209
+ * What the index IS for:
30210
+ * 1. **The gate.** A file with no usable sample table cannot join the recorded
30211
+ * path, and is refused BY NAME rather than served as a zero-length
30212
+ * timeline. Measured 2026-09-20: an HKSV clip has `mvhd.duration = 0`,
30213
+ * `mdhd.duration = 0`, no `sidx` and no `mfra` — ffmpeg writes the trailer
30214
+ * only on close and the tee cuts like a pipe.
30215
+ * 2. **The file-relative timeline.** `durationMs` is the segment span the
30216
+ * feeder plays and the bound `ended` is decided on; `firstSyncMs` is where a
30217
+ * seek before the first keyframe snaps to.
30218
+ */
30219
+ var FOURCC_LENGTH = 4;
30220
+ var BOX_HEADER_LENGTH = 8;
30221
+ var LARGE_SIZE_HEADER_LENGTH = 16;
30222
+ /**
30223
+ * Walk the direct children of `[start, end)`. Returns `null` when the region is
30224
+ * structurally impossible (a size that does not fit), which the caller turns
30225
+ * into `malformed` — never a throw, because these bytes come off a camera.
30226
+ */
30227
+ function childBoxes(bytes, start, end) {
30228
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30229
+ const out = [];
30230
+ let off = start;
30231
+ while (off + BOX_HEADER_LENGTH <= end) {
30232
+ const declared = view.getUint32(off);
30233
+ let type = "";
30234
+ for (let i = 0; i < FOURCC_LENGTH; i++) type += String.fromCharCode(bytes[off + 4 + i] ?? 0);
30235
+ let size = declared;
30236
+ let contentStart = off + BOX_HEADER_LENGTH;
30237
+ if (declared === 1) {
30238
+ if (off + LARGE_SIZE_HEADER_LENGTH > end) return null;
30239
+ const hi = view.getUint32(off + 8);
30240
+ const lo = view.getUint32(off + 12);
30241
+ size = hi * 4294967296 + lo;
30242
+ contentStart = off + LARGE_SIZE_HEADER_LENGTH;
30243
+ } else if (declared === 0) size = end - off;
30244
+ if (size < contentStart - off) return null;
30245
+ const boxEnd = off + size;
30246
+ if (boxEnd > end) return null;
30247
+ out.push({
30248
+ type,
30249
+ start: off,
30250
+ end: boxEnd,
30251
+ contentStart
30252
+ });
30253
+ off = boxEnd;
30254
+ }
30255
+ return out;
30256
+ }
30257
+ function findBox(boxes, type) {
30258
+ for (const b of boxes) if (b.type === type) return b;
30259
+ return null;
30260
+ }
30261
+ function readSttsRuns(bytes, box) {
30262
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30263
+ const base = box.contentStart + 4;
30264
+ if (base + 4 > box.end) return null;
30265
+ const entries = view.getUint32(base);
30266
+ if (base + 4 + entries * 8 > box.end) return null;
30267
+ const runs = [];
30268
+ for (let i = 0; i < entries; i++) {
30269
+ const at = base + 4 + i * 8;
30270
+ runs.push({
30271
+ count: view.getUint32(at),
30272
+ delta: view.getUint32(at + 4)
30273
+ });
30274
+ }
30275
+ return runs;
30276
+ }
30277
+ function readSyncSampleNumbers(bytes, box) {
30278
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30279
+ const base = box.contentStart + 4;
30280
+ if (base + 4 > box.end) return null;
30281
+ const entries = view.getUint32(base);
30282
+ if (base + 4 + entries * 4 > box.end) return null;
30283
+ const out = /* @__PURE__ */ new Set();
30284
+ for (let i = 0; i < entries; i++) out.add(view.getUint32(base + 4 + i * 4));
30285
+ return out;
30286
+ }
30287
+ function mediaTimescale(bytes, mdhd) {
30288
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30289
+ const version = bytes[mdhd.contentStart] ?? 0;
30290
+ const at = mdhd.contentStart + 4 + (version === 1 ? 16 : 8);
30291
+ if (at + 4 > mdhd.end) return null;
30292
+ const ts = view.getUint32(at);
30293
+ return ts > 0 ? ts : null;
30294
+ }
30295
+ function handlerType(bytes, hdlr) {
30296
+ const at = hdlr.contentStart + 8;
30297
+ if (at + FOURCC_LENGTH > hdlr.end) return "";
30298
+ let out = "";
30299
+ for (let i = 0; i < FOURCC_LENGTH; i++) out += String.fromCharCode(bytes[at + i] ?? 0);
30300
+ return out;
30301
+ }
30302
+ /** The video `trak` of a `moov`, or `null` when it has none. */
30303
+ function videoTrack(bytes, moov) {
30304
+ const children = childBoxes(bytes, moov.contentStart, moov.end);
30305
+ if (!children) return null;
30306
+ for (const trak of children) {
30307
+ if (trak.type !== "trak") continue;
30308
+ const trakKids = childBoxes(bytes, trak.contentStart, trak.end);
30309
+ const mdia = trakKids ? findBox(trakKids, "mdia") : null;
30310
+ if (!mdia) continue;
30311
+ const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
30312
+ const hdlr = mdiaKids ? findBox(mdiaKids, "hdlr") : null;
30313
+ if (hdlr && handlerType(bytes, hdlr) === "vide") return mdia;
30314
+ }
30315
+ return null;
30316
+ }
30317
+ /**
30318
+ * Read a clip file's time→sample map. Never throws: every structural failure
30319
+ * comes back as a named refusal so the caller can say WHY a clip will not play.
30320
+ */
30321
+ function indexClipMp4(bytes) {
30322
+ const top = childBoxes(bytes, 0, bytes.length);
30323
+ if (!top) return {
30324
+ kind: "refused",
30325
+ reason: "malformed"
30326
+ };
30327
+ const moov = findBox(top, "moov");
30328
+ if (!moov) return {
30329
+ kind: "refused",
30330
+ reason: "no-moov"
30331
+ };
30332
+ const mdia = videoTrack(bytes, moov);
30333
+ if (!mdia) return {
30334
+ kind: "refused",
30335
+ reason: "no-video-track"
30336
+ };
30337
+ const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
30338
+ if (!mdiaKids) return {
30339
+ kind: "refused",
30340
+ reason: "malformed"
30341
+ };
30342
+ const mdhd = findBox(mdiaKids, "mdhd");
30343
+ const minf = findBox(mdiaKids, "minf");
30344
+ if (!mdhd || !minf) return {
30345
+ kind: "refused",
30346
+ reason: "malformed"
30347
+ };
30348
+ const timescale = mediaTimescale(bytes, mdhd);
30349
+ if (timescale === null) return {
30350
+ kind: "refused",
30351
+ reason: "malformed"
30352
+ };
30353
+ const minfKids = childBoxes(bytes, minf.contentStart, minf.end);
30354
+ const stbl = minfKids ? findBox(minfKids, "stbl") : null;
30355
+ if (!stbl) return {
30356
+ kind: "refused",
30357
+ reason: "no-samples"
30358
+ };
30359
+ const stblKids = childBoxes(bytes, stbl.contentStart, stbl.end);
30360
+ if (!stblKids) return {
30361
+ kind: "refused",
30362
+ reason: "malformed"
30363
+ };
30364
+ const sttsBox = findBox(stblKids, "stts");
30365
+ if (!sttsBox) return {
30366
+ kind: "refused",
30367
+ reason: "no-samples"
30368
+ };
30369
+ const runs = readSttsRuns(bytes, sttsBox);
30370
+ if (!runs) return {
30371
+ kind: "refused",
30372
+ reason: "malformed"
30373
+ };
30374
+ const stssBox = findBox(stblKids, "stss");
30375
+ const syncNumbers = stssBox === null ? null : readSyncSampleNumbers(bytes, stssBox);
30376
+ if (stssBox !== null && syncNumbers === null) return {
30377
+ kind: "refused",
30378
+ reason: "malformed"
30379
+ };
30380
+ const samples = [];
30381
+ let ticks = 0;
30382
+ let syncSampleCount = 0;
30383
+ let firstSyncMs = null;
30384
+ for (const run of runs) for (let i = 0; i < run.count; i++) {
30385
+ const sampleNumber = samples.length + 1;
30386
+ const sync = syncNumbers === null ? true : syncNumbers.has(sampleNumber);
30387
+ const startMs = ticks * 1e3 / timescale;
30388
+ const durMs = run.delta * 1e3 / timescale;
30389
+ if (sync) {
30390
+ syncSampleCount++;
30391
+ if (firstSyncMs === null) firstSyncMs = startMs;
30392
+ }
30393
+ samples.push({
30394
+ startMs,
30395
+ durMs,
30396
+ sync
30397
+ });
30398
+ ticks += run.delta;
30399
+ }
30400
+ if (samples.length === 0 || ticks === 0) return {
30401
+ kind: "refused",
30402
+ reason: "no-samples"
30403
+ };
30404
+ return {
30405
+ kind: "index",
30406
+ durationMs: ticks * 1e3 / timescale,
30407
+ firstSyncMs: firstSyncMs ?? 0,
30408
+ syncSampleCount,
30409
+ samples
30410
+ };
30411
+ }
30412
+ //#endregion
30413
+ //#region src/stream-broker/recorded/clip-file-source.ts
30414
+ var ClipSourceError = class extends Error {
30415
+ reason;
30416
+ constructor(reason, message) {
30417
+ super(message);
30418
+ this.reason = reason;
30419
+ this.name = "ClipSourceError";
30420
+ }
30421
+ };
30422
+ function createClipFileSource(deps) {
30423
+ const maxBytes = deps.maxBytes ?? 67108864;
30424
+ const { deviceId, clipId, profile } = deps.handle;
30425
+ let resolved = null;
30426
+ let inFlight = null;
30427
+ /**
30428
+ * A refusal that can never change for this file — it is unindexable, or it
30429
+ * is over the bound. Cached so a drag does not re-fetch a 35 MB file per
30430
+ * pointer move. A FETCH failure is deliberately NOT cached: a sleeping
30431
+ * battery camera is a transient no, and the operator's retry must reach it.
30432
+ */
30433
+ let terminal = null;
30434
+ const refuse = (reason, message, data) => {
30435
+ const err = new ClipSourceError(reason, message);
30436
+ deps.onLog?.(message, {
30437
+ deviceId,
30438
+ clipId,
30439
+ profile,
30440
+ reason,
30441
+ ...data
30442
+ });
30443
+ return err;
30444
+ };
30445
+ const load = async () => {
30446
+ let bytes;
30447
+ try {
30448
+ bytes = await deps.read({
30449
+ deviceId,
30450
+ clipId,
30451
+ profile
30452
+ });
30453
+ } catch (err) {
30454
+ throw refuse("fetch-failed", "clip playback: byte fetch failed", { error: err instanceof Error ? err.message : String(err) });
30455
+ }
30456
+ if (bytes.length > maxBytes) {
30457
+ terminal = refuse("too-large", "clip playback: clip exceeds the broker byte bound", {
30458
+ bytes: bytes.length,
30459
+ maxBytes
30460
+ });
30461
+ throw terminal;
30462
+ }
30463
+ const index = indexClipMp4(bytes);
30464
+ if (index.kind === "refused") {
30465
+ terminal = refuse(index.reason, "clip playback: clip cannot be indexed — it will not play in the recorded path", { bytes: bytes.length });
30466
+ throw terminal;
30467
+ }
30468
+ return {
30469
+ bytes,
30470
+ index
30471
+ };
30472
+ };
30473
+ const ensure = async () => {
30474
+ if (terminal) throw terminal;
30475
+ if (resolved) return resolved;
30476
+ if (!inFlight) inFlight = load().then((r) => {
30477
+ resolved = r;
30478
+ return r;
30479
+ }).finally(() => {
30480
+ inFlight = null;
30481
+ });
30482
+ return inFlight;
30483
+ };
30484
+ return {
30485
+ async locate(args) {
30486
+ const { index, bytes } = await ensure();
30487
+ if (args.epochMs >= index.durationMs) return {
30488
+ kind: "gap",
30489
+ nearestEdgeMs: null,
30490
+ prevEndMs: index.durationMs
30491
+ };
30492
+ if (args.epochMs < index.firstSyncMs) return {
30493
+ kind: "gap",
30494
+ nearestEdgeMs: index.firstSyncMs,
30495
+ prevEndMs: null
30496
+ };
30497
+ return {
30498
+ kind: "segment",
30499
+ startMs: 0,
30500
+ durMs: index.durationMs,
30501
+ bytes: bytes.length
30502
+ };
30503
+ },
30504
+ async readBytes() {
30505
+ return (await ensure()).bytes;
30506
+ },
30507
+ async durationMs() {
30508
+ return (await ensure()).index.durationMs;
30509
+ }
30510
+ };
30511
+ }
30512
+ //#endregion
30099
30513
  //#region src/stream-broker/recorded/demux-off-thread.ts
30100
30514
  /**
30101
30515
  * Off-thread JS demux client — mp4box parse on a `worker_threads` Worker
@@ -30841,6 +31255,7 @@ var BrokerWebrtcServer = class {
30841
31255
  locateRecordedSegment;
30842
31256
  readRecordedSegmentBytes;
30843
31257
  readRecordedGopBytes;
31258
+ readClipBytes;
30844
31259
  resolveFfmpegPath;
30845
31260
  /**
30846
31261
  * THE ffmpeg primitive, for the transcode leg. `null` until the manager wires
@@ -30933,6 +31348,7 @@ var BrokerWebrtcServer = class {
30933
31348
  this.locateRecordedSegment = options.locateRecordedSegment;
30934
31349
  this.readRecordedSegmentBytes = options.readRecordedSegmentBytes;
30935
31350
  this.readRecordedGopBytes = options.readRecordedGopBytes;
31351
+ this.readClipBytes = options.readClipBytes;
30936
31352
  this.resolveFfmpegPath = options.resolveFfmpegPath;
30937
31353
  }
30938
31354
  registerBroker(brokerId, broker) {
@@ -31652,7 +32068,96 @@ var BrokerWebrtcServer = class {
31652
32068
  bytes: res.data
31653
32069
  };
31654
32070
  } : void 0;
32071
+ const readClip = this.readClipBytes;
31655
32072
  let timeline;
32073
+ /**
32074
+ * ONE feeder builder for BOTH sources (D575). `clip === null` is the
32075
+ * recorder's own footage; a clip swaps only the two byte closures —
32076
+ * everything else (demux, sink, audio codec branch, state/position/log
32077
+ * seams) is identical, which is the whole claim the clip path makes.
32078
+ */
32079
+ const buildFeeder = (profile, clip) => {
32080
+ const opusSession = session.isOpusAudio;
32081
+ const audioSink = opusSession ? {
32082
+ pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
32083
+ pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
32084
+ } : {
32085
+ pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
32086
+ pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
32087
+ };
32088
+ const demuxAudio = opusSession ? async (b) => {
32089
+ return (await demuxSegmentAudioOpusPreferJs(b, {
32090
+ ffmpegPath: resolveFfmpeg?.(),
32091
+ onFfmpegFallback: (reason) => {
32092
+ if (audioOpusFallbackLogged) return;
32093
+ audioOpusFallbackLogged = true;
32094
+ sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
32095
+ sessionId,
32096
+ deviceId,
32097
+ profile,
32098
+ reason
32099
+ } });
32100
+ }
32101
+ })).map((au) => ({
32102
+ data: au.data,
32103
+ ptsMs: au.ptsMs
32104
+ }));
32105
+ } : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.());
32106
+ const clipSource = clip && readClip ? createClipFileSource({
32107
+ handle: clip,
32108
+ read: readClip,
32109
+ onLog: (msg, data) => sessionLogger.warn(msg, {
32110
+ tags: { deviceId: clip.deviceId },
32111
+ meta: {
32112
+ sessionId,
32113
+ ...data
32114
+ }
32115
+ })
32116
+ }) : null;
32117
+ return new RecordedFeeder({
32118
+ deviceId: clip ? clip.deviceId : deviceId,
32119
+ profile,
32120
+ locate: clipSource ? clipSource.locate : locateRecorded,
32121
+ readBytes: clipSource ? clipSource.readBytes : readRecorded,
32122
+ ...clipSource ? {} : {
32123
+ readGop,
32124
+ fragmentCache: scrubCache
32125
+ },
32126
+ demux: demuxVideo,
32127
+ demuxTally,
32128
+ demuxAudio,
32129
+ audioClockRateHz: opusSession ? 48e3 : 8e3,
32130
+ sink: audioSink,
32131
+ onState: (t) => {
32132
+ sessionLogger.info("recorded: feeder state", {
32133
+ tags: { deviceId: clip ? clip.deviceId : deviceId },
32134
+ meta: {
32135
+ sessionId,
32136
+ deviceId,
32137
+ profile,
32138
+ clipId: clip?.clipId,
32139
+ from: t.from,
32140
+ to: t.to,
32141
+ reason: t.reason,
32142
+ seekToken: t.seekToken,
32143
+ dragId: t.dragId
32144
+ }
32145
+ });
32146
+ timeline._emitState(t.to);
32147
+ },
32148
+ onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
32149
+ onLog: (msg, data) => sessionLogger.info(msg, {
32150
+ tags: { deviceId: clip ? clip.deviceId : deviceId },
32151
+ meta: {
32152
+ sessionId,
32153
+ deviceId,
32154
+ profile,
32155
+ clipId: clip?.clipId,
32156
+ ...data
32157
+ }
32158
+ })
32159
+ });
32160
+ };
31656
32161
  timeline = new TimelineSession({
31657
32162
  session,
31658
32163
  deviceId,
@@ -31683,71 +32188,15 @@ var BrokerWebrtcServer = class {
31683
32188
  } });
31684
32189
  });
31685
32190
  } : void 0,
31686
- makeFeeder: (profile) => {
31687
- const opusSession = session.isOpusAudio;
31688
- return new RecordedFeeder({
31689
- deviceId,
31690
- profile,
31691
- locate: locateRecorded,
31692
- readBytes: readRecorded,
31693
- readGop,
31694
- fragmentCache: scrubCache,
31695
- demux: demuxVideo,
31696
- demuxTally,
31697
- demuxAudio: opusSession ? async (b) => {
31698
- return (await demuxSegmentAudioOpusPreferJs(b, {
31699
- ffmpegPath: resolveFfmpeg?.(),
31700
- onFfmpegFallback: (reason) => {
31701
- if (audioOpusFallbackLogged) return;
31702
- audioOpusFallbackLogged = true;
31703
- sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
31704
- sessionId,
31705
- deviceId,
31706
- profile,
31707
- reason
31708
- } });
31709
- }
31710
- })).map((au) => ({
31711
- data: au.data,
31712
- ptsMs: au.ptsMs
31713
- }));
31714
- } : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.()),
31715
- audioClockRateHz: opusSession ? 48e3 : 8e3,
31716
- sink: opusSession ? {
31717
- pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
31718
- pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
31719
- } : {
31720
- pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
31721
- pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
31722
- },
31723
- onState: (t) => {
31724
- sessionLogger.info("recorded: feeder state", {
31725
- tags: { deviceId },
31726
- meta: {
31727
- sessionId,
31728
- deviceId,
31729
- profile,
31730
- from: t.from,
31731
- to: t.to,
31732
- reason: t.reason,
31733
- seekToken: t.seekToken,
31734
- dragId: t.dragId
31735
- }
31736
- });
31737
- timeline._emitState(t.to);
31738
- },
31739
- onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
31740
- onLog: (msg, data) => sessionLogger.info(msg, {
31741
- tags: { deviceId },
31742
- meta: {
31743
- sessionId,
31744
- deviceId,
31745
- profile,
31746
- ...data
31747
- }
31748
- })
31749
- });
31750
- }
32191
+ makeFeeder: (profile) => buildFeeder(profile, null),
32192
+ ...readClip ? { makeClipFeeder: ({ handle }) => buildFeeder(handle.profile ?? "clip", handle) } : {},
32193
+ onLog: (msg, data) => sessionLogger.warn(msg, {
32194
+ tags: { deviceId },
32195
+ meta: {
32196
+ sessionId,
32197
+ ...data
32198
+ }
32199
+ })
31751
32200
  });
31752
32201
  entry.timeline = timeline;
31753
32202
  sessionLogger.info("recorded: TimelineSession wired (control accessors present)", { meta: {
@@ -33250,6 +33699,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
33250
33699
  },
33251
33700
  locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
33252
33701
  readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
33702
+ readClipBytes: async (args) => {
33703
+ const res = await this.ctx.api.videoclips.readClipBytes.query({
33704
+ deviceId: args.deviceId,
33705
+ clipId: args.clipId,
33706
+ ...args.profile !== null ? { profile: args.profile } : {}
33707
+ });
33708
+ return new Uint8Array(Buffer.from(res.base64, "base64"));
33709
+ },
33253
33710
  readRecordedGopBytes: async (args) => {
33254
33711
  try {
33255
33712
  return await this.ctx.api.recording.readGopBytes.query({