@camstack/addon-pipeline 1.1.24 → 1.1.26

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 (45) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/decoder-ffmpeg/index.js +84 -12
  6. package/dist/decoder-ffmpeg/index.mjs +84 -12
  7. package/dist/detection-pipeline/index.js +149 -27
  8. package/dist/detection-pipeline/index.mjs +149 -27
  9. package/dist/{dist-pd_-3C0T.mjs → dist-CgEP_0OL.mjs} +794 -16
  10. package/dist/{dist-CE3a05qT.js → dist-DAIlCdAx.js} +799 -15
  11. package/dist/frame-handle-plane-Dq20KtKL.mjs +636 -0
  12. package/dist/frame-handle-plane-DtTRX_0n.js +647 -0
  13. package/dist/hub-hostname-DAJXlOgV.js +54 -0
  14. package/dist/hub-hostname-cCknRYKj.mjs +49 -0
  15. package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
  16. package/dist/motion-wasm/index.js +1 -1
  17. package/dist/motion-wasm/index.mjs +1 -1
  18. package/dist/pipeline-runner/index.js +341 -17
  19. package/dist/pipeline-runner/index.mjs +341 -17
  20. package/dist/recorder/index.js +267 -65
  21. package/dist/recorder/index.mjs +267 -65
  22. package/dist/stream-broker/_stub.js +39 -39
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs} +2 -2
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +26 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs +26 -0
  26. package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-DyLqyJaS.mjs} +2 -2
  27. package/dist/stream-broker/index.js +7 -639
  28. package/dist/stream-broker/index.mjs +5 -637
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js} +1 -1
  31. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-CoLjNiUN.js} +1 -1
  32. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js} +1 -1
  33. package/embed-dist/assets/index-C-pL8ETk.js +81 -0
  34. package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
  35. package/embed-dist/index.html +2 -2
  36. package/package.json +1 -1
  37. package/python/inference_pool.py +522 -27
  38. package/python/test_inference_pool_backpressure.py +121 -0
  39. package/python/test_inference_pool_coreml_cache.py +416 -0
  40. package/python/test_inference_pool_device_selection.py +256 -0
  41. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
  42. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
  43. package/embed-dist/assets/index-C7SSikPl.css +0 -2
  44. package/embed-dist/assets/index-DhdVxWXL.js +0 -80
  45. package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
@@ -1,5 +1,6 @@
1
- import { $ as selectAssignedProfileSlots, B as BaseAddon, F as storageEvictableCapability, J as hydrateSchema, K as createDurableState, N as recordingCapability, O as migrateConfigToBands, U as DeviceType, W as EventCategory, d as RecordingConfigSchema, lt as record, ut as string, z as errMsg } from "../dist-pd_-3C0T.mjs";
2
- import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-3Mmeob3l.mjs";
1
+ import { B as errMsg, G as EventCategory, I as storageEvictableCapability, P as recordingCapability, V as BaseAddon, W as DeviceType, Y as hydrateSchema, c as EVENT_PAD_MS, dt as string, et as selectAssignedProfileSlots, f as RecordingConfigSchema, k as migrateConfigToBands, q as createDurableState, ut as record } from "../dist-CgEP_0OL.mjs";
2
+ import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
+ import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
3
4
  import { promises } from "node:fs";
4
5
  import path from "node:path";
5
6
  import { spawn } from "node:child_process";
@@ -205,7 +206,7 @@ var RecordingIndex = class {
205
206
  };
206
207
  //#endregion
207
208
  //#region src/recorder/segment-store.ts
208
- var SegmentStore = class {
209
+ var SegmentStore = class SegmentStore {
209
210
  deps;
210
211
  constructor(deps) {
211
212
  this.deps = deps;
@@ -252,23 +253,68 @@ var SegmentStore = class {
252
253
  * `location` (`row.locationId === location`) — the caller guarantees this (e.g. the evictable
253
254
  * provider feeds rows from `segmentsOnLocation(location)`). Mixed-location rows would be deleted
254
255
  * against the wrong location.
256
+ *
257
+ * Bucket-granular fast path (R4): rows are grouped by their hour-bucket dir
258
+ * (`<dev>/<profile>/Y/M/D/H`). When a group covers EVERY indexed segment of
259
+ * its bucket — and the bucket's hour is fully in the past, so no relocate can
260
+ * still land in it (in-flight staging lives in the separate `.rec-tmp`) — the
261
+ * whole dir goes with ONE `removeDir` instead of hundreds of per-file
262
+ * deletes. Partial buckets, current-hour buckets, and a failed removeDir fall
263
+ * back to the per-file `storage.delete` loop.
255
264
  */
256
265
  async evict(location, rows) {
257
266
  let reclaimed = 0;
258
267
  const removed = [];
259
- for (const r of rows) try {
260
- await this.deps.deletePath({
261
- location,
262
- relativePath: r.path
263
- });
264
- removed.push(r.path);
265
- reclaimed += r.bytes;
266
- } catch (err) {
267
- this.deps.logger.warn("segment delete failed", err);
268
+ const byBucket = /* @__PURE__ */ new Map();
269
+ for (const r of rows) {
270
+ const cut = r.path.lastIndexOf("/");
271
+ const bucketDir = cut > 0 ? r.path.slice(0, cut) : "";
272
+ const group = byBucket.get(bucketDir);
273
+ if (group) group.push(r);
274
+ else byBucket.set(bucketDir, [r]);
275
+ }
276
+ for (const [bucketDir, bucketRows] of byBucket) {
277
+ if (bucketDir !== "" && this.bucketFullyEvicted(bucketDir, bucketRows)) try {
278
+ await this.deps.removeDir(location, bucketDir);
279
+ for (const r of bucketRows) {
280
+ removed.push(r.path);
281
+ reclaimed += r.bytes;
282
+ }
283
+ continue;
284
+ } catch (err) {
285
+ this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", err);
286
+ }
287
+ for (const r of bucketRows) try {
288
+ await this.deps.deletePath({
289
+ location,
290
+ relativePath: r.path
291
+ });
292
+ removed.push(r.path);
293
+ reclaimed += r.bytes;
294
+ } catch (err) {
295
+ this.deps.logger.warn("segment delete failed", err);
296
+ }
268
297
  }
269
298
  if (removed.length > 0) this.deps.index.removeSegments(removed);
270
299
  return reclaimed;
271
300
  }
301
+ /** Hour of one UTC hour-bucket in ms. */
302
+ static HOUR_MS = 36e5;
303
+ /**
304
+ * True when `victims` cover EVERY indexed segment of `bucketDir` AND the
305
+ * bucket's hour has fully elapsed. The current-hour guard matters because
306
+ * `onFinalized` relocates new segments INTO the current bucket concurrently —
307
+ * an rm racing that rename would orphan a just-indexed segment.
308
+ */
309
+ bucketFullyEvicted(bucketDir, victims) {
310
+ const first = victims[0];
311
+ if (!first) return false;
312
+ const bucketStartMs = Math.floor(first.startMs / SegmentStore.HOUR_MS) * SegmentStore.HOUR_MS;
313
+ if ((this.deps.now?.() ?? Date.now()) < bucketStartMs + SegmentStore.HOUR_MS) return false;
314
+ const victimPaths = new Set(victims.map((v) => v.path));
315
+ const indexed = this.deps.index.segments(first.deviceId, first.profile).filter((s) => s.path.startsWith(`${bucketDir}/`));
316
+ return indexed.length === victimPaths.size && indexed.every((s) => victimPaths.has(s.path));
317
+ }
272
318
  };
273
319
  //#endregion
274
320
  //#region src/recorder/evict-select.ts
@@ -470,6 +516,129 @@ function subscribeEventCapture(deps) {
470
516
  });
471
517
  }
472
518
  //#endregion
519
+ //#region src/recorder/codec-probe.ts
520
+ /** Iterate the boxes fully contained in `bytes[start, end)`. Stops at the
521
+ * first malformed or truncated box (the probe only needs the front boxes). */
522
+ function* boxes(bytes, start, end) {
523
+ let off = start;
524
+ while (off + 8 <= end) {
525
+ const view = new DataView(bytes.buffer, bytes.byteOffset + off);
526
+ let size = view.getUint32(0);
527
+ let headerLen = 8;
528
+ if (size === 1) {
529
+ if (off + 16 > end) return;
530
+ const large = view.getBigUint64(8);
531
+ if (large > BigInt(Number.MAX_SAFE_INTEGER)) return;
532
+ size = Number(large);
533
+ headerLen = 16;
534
+ } else if (size === 0) size = end - off;
535
+ if (size < headerLen) return;
536
+ const boxEnd = off + size;
537
+ if (boxEnd > end) return;
538
+ yield {
539
+ type: String.fromCharCode(bytes[off + 4], bytes[off + 5], bytes[off + 6], bytes[off + 7]),
540
+ payloadStart: off + headerLen,
541
+ payloadEnd: boxEnd
542
+ };
543
+ off = boxEnd;
544
+ }
545
+ }
546
+ /** First child box with `type` inside a payload window, or null. */
547
+ function findBox(bytes, start, end, type) {
548
+ for (const b of boxes(bytes, start, end)) if (b.type === type) return b;
549
+ return null;
550
+ }
551
+ /** Descend a container-box path (`moov/trak/mdia/…`), or null when broken. */
552
+ function descend(bytes, path) {
553
+ let start = 0;
554
+ let end = bytes.length;
555
+ let cur = null;
556
+ for (const type of path) {
557
+ cur = findBox(bytes, start, end, type);
558
+ if (!cur) return null;
559
+ start = cur.payloadStart;
560
+ end = cur.payloadEnd;
561
+ }
562
+ return cur;
563
+ }
564
+ /** VisualSampleEntry fixed fields before its child boxes: 6 reserved + 2
565
+ * data_reference_index + 70 bytes (pre_defined/width/height/…/depth). */
566
+ var VISUAL_SAMPLE_ENTRY_FIXED_BYTES = 78;
567
+ /** stsd FullBox payload head: version(1) + flags(3) + entry_count(4). */
568
+ var STSD_HEAD_BYTES = 8;
569
+ var hex2 = (n) => n.toString(16).padStart(2, "0");
570
+ /** `avc1.PPCCLL` from the avcC payload (profile, constraint flags, level). */
571
+ function avcCodecString(bytes, avcC) {
572
+ if (avcC.payloadEnd - avcC.payloadStart < 4) return void 0;
573
+ const p = avcC.payloadStart;
574
+ return `avc1.${hex2(bytes[p + 1])}${hex2(bytes[p + 2])}${hex2(bytes[p + 3])}`;
575
+ }
576
+ /** Reverse the bit order of a 32-bit value (HEVC general_profile_compatibility
577
+ * flags are written MSB-first but the codec string wants them reversed). */
578
+ function reverseBits32(v) {
579
+ let out = 0;
580
+ for (let i = 0; i < 32; i++) out = out << 1 | v >>> i & 1;
581
+ return out >>> 0;
582
+ }
583
+ /** ISO/IEC 14496-15 Annex E HEVC codec string from the hvcC payload. The
584
+ * prefix is the SAMPLE ENTRY code (`hvc1` vs `hev1` — they differ in
585
+ * parameter-set placement, which players honour). */
586
+ function hevcCodecString(bytes, entryType, hvcC) {
587
+ if (hvcC.payloadEnd - hvcC.payloadStart < 13) return void 0;
588
+ const p = hvcC.payloadStart;
589
+ const byte1 = bytes[p + 1];
590
+ const profileSpace = byte1 >> 6 & 3;
591
+ const tier = byte1 >> 5 & 1;
592
+ const profileIdc = byte1 & 31;
593
+ const compat = new DataView(bytes.buffer, bytes.byteOffset + p + 2).getUint32(0);
594
+ const levelIdc = bytes[p + 12];
595
+ const spacePrefix = profileSpace === 0 ? "" : String.fromCharCode(64 + profileSpace);
596
+ const compatHex = reverseBits32(compat).toString(16).toUpperCase();
597
+ let constraints = "";
598
+ for (let i = 5; i >= 0; i--) {
599
+ const b = bytes[p + 6 + i];
600
+ if (b !== 0 || constraints !== "") constraints = `.${b.toString(16).toUpperCase().padStart(2, "0")}${constraints}`;
601
+ }
602
+ return `${entryType}.${spacePrefix}${profileIdc}.${compatHex}.${tier === 0 ? "L" : "H"}${levelIdc}${constraints}`;
603
+ }
604
+ /** Sample-entry codes the probe recognizes, mapped to their config-box type. */
605
+ var VIDEO_SAMPLE_ENTRIES = {
606
+ avc1: "avcC",
607
+ avc3: "avcC",
608
+ hvc1: "hvcC",
609
+ hev1: "hvcC"
610
+ };
611
+ /**
612
+ * Walk an fMP4 fragment's moov for the first recognized video sample entry
613
+ * (avc1/avc3 → avcC, hvc1/hev1 → hvcC) and build its RFC 6381 codec string.
614
+ * Accepts a bounded PREFIX of the fragment (the moov leads the file). Returns
615
+ * `undefined` for anything malformed, truncated, or unrecognized.
616
+ */
617
+ function codecStringFromFragment(bytes) {
618
+ const moov = descend(bytes, ["moov"]);
619
+ if (!moov) return void 0;
620
+ for (const trak of boxes(bytes, moov.payloadStart, moov.payloadEnd)) {
621
+ if (trak.type !== "trak") continue;
622
+ let cur = trak;
623
+ for (const type of [
624
+ "mdia",
625
+ "minf",
626
+ "stbl",
627
+ "stsd"
628
+ ]) cur = cur ? findBox(bytes, cur.payloadStart, cur.payloadEnd, type) : null;
629
+ if (!cur) continue;
630
+ const entriesStart = cur.payloadStart + STSD_HEAD_BYTES;
631
+ for (const entry of boxes(bytes, entriesStart, cur.payloadEnd)) {
632
+ const configType = VIDEO_SAMPLE_ENTRIES[entry.type];
633
+ if (!configType) continue;
634
+ const config = findBox(bytes, entry.payloadStart + VISUAL_SAMPLE_ENTRY_FIXED_BYTES, entry.payloadEnd, configType);
635
+ if (!config) continue;
636
+ const codec = configType === "avcC" ? avcCodecString(bytes, config) : hevcCodecString(bytes, entry.type, config);
637
+ if (codec) return codec;
638
+ }
639
+ }
640
+ }
641
+ //#endregion
473
642
  //#region src/recorder/day-bucket.ts
474
643
  /** Pure helpers to bucket recording segment starts into local calendar days. */
475
644
  var DAY_MS$1 = 1440 * 60 * 1e3;
@@ -673,6 +842,42 @@ function noPlayback(deviceId) {
673
842
  function playbackDirFor(dataDir, deviceId) {
674
843
  return path.join(dataDir, "playback", String(deviceId));
675
844
  }
845
+ /** Prefix window the CODECS probe reads — the fragment's moov (with its
846
+ * avcC/hvcC) leads the file and is ~1-2KB, so 64KB is a generous bound. */
847
+ var PROBE_PREFIX_BYTES = 65536;
848
+ /** Default bounded prefix reader: `fs.open` + one positioned `read`, never the
849
+ * whole file. */
850
+ async function readPrefixFromDisk(absPath, maxBytes) {
851
+ const fh = await promises.open(absPath, "r");
852
+ try {
853
+ const buf = Buffer.alloc(maxBytes);
854
+ const { bytesRead } = await fh.read(buf, 0, maxBytes, 0);
855
+ return buf.subarray(0, bytesRead);
856
+ } finally {
857
+ await fh.close();
858
+ }
859
+ }
860
+ /**
861
+ * RFC 6381 codec string for one variant: probe the first PRESENT fragment's
862
+ * moov once and cache the result. Failures (unreadable file / unrecognized
863
+ * bytes) return `undefined` — the playlist then omits `CODECS` for that
864
+ * variant, exactly today's behaviour — and are NOT cached, so a later healthy
865
+ * fragment can still populate it.
866
+ */
867
+ async function probeVariantCodec(readPrefix, cache, deviceId, profile, firstPresentAbs) {
868
+ const key = `${deviceId}/${profile}`;
869
+ const cached = cache.get(key);
870
+ if (cached !== void 0) return cached;
871
+ if (firstPresentAbs === null) return void 0;
872
+ try {
873
+ const prefix = await readPrefix(firstPresentAbs, PROBE_PREFIX_BYTES);
874
+ const codec = codecStringFromFragment(prefix instanceof Uint8Array ? prefix : new Uint8Array(prefix));
875
+ if (codec !== void 0) cache.set(key, codec);
876
+ return codec;
877
+ } catch {
878
+ return;
879
+ }
880
+ }
676
881
  /**
677
882
  * Build a master + per-profile variant HLS playlist on disk for the requested
678
883
  * window and return a RELATIVE playback URL through the hub's data-plane
@@ -682,9 +887,10 @@ function playbackDirFor(dataDir, deviceId) {
682
887
  * URIs are relative so the master + variants + segments form a self-contained
683
888
  * tree the player resolves against the master URL.
684
889
  */
685
- async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
890
+ async function buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs) {
686
891
  const base = deps.playbackBaseUrl();
687
892
  if (base === null) return noPlayback(deviceId);
893
+ const readPrefix = deps.readFilePrefix ?? readPrefixFromDisk;
688
894
  const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
689
895
  const playbackDir = playbackDirFor(deps.dataDir, deviceId);
690
896
  const profiles = [...new Set(deps.index.segments(deviceId).map((s) => s.profile))];
@@ -693,15 +899,18 @@ async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
693
899
  const segs = deps.index.segments(deviceId, profile).filter((s) => s.startMs < toMs && s.startMs + s.durMs > fromMs).toSorted((a, b) => a.startMs - b.startMs);
694
900
  if (segs.length === 0) continue;
695
901
  const variantSegments = [];
902
+ let firstPresentAbs = null;
696
903
  for (const s of segs) {
697
904
  const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
698
905
  const root = rootById.get(s.locationId);
699
906
  if (root === void 0) continue;
907
+ const abs = path.join(root, rel);
700
908
  try {
701
- await promises.stat(path.join(root, rel));
909
+ await promises.stat(abs);
702
910
  } catch {
703
911
  continue;
704
912
  }
913
+ if (firstPresentAbs === null) firstPresentAbs = abs;
705
914
  variantSegments.push({
706
915
  uri: rel.split("/").slice(1).join("/"),
707
916
  startMs: s.startMs,
@@ -709,12 +918,14 @@ async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
709
918
  });
710
919
  }
711
920
  if (variantSegments.length === 0) continue;
921
+ const codecs = await probeVariantCodec(readPrefix, codecCache, deviceId, profile, firstPresentAbs);
712
922
  await promises.mkdir(playbackDir, { recursive: true });
713
923
  await promises.writeFile(path.join(playbackDir, `${profile}.m3u8`), buildVariantPlaylist(variantSegments), "utf8");
714
924
  variants.push({
715
925
  profile,
716
926
  bandwidth: PROFILE_BANDWIDTH[profile] ?? DEFAULT_BANDWIDTH,
717
- uri: `${profile}.m3u8`
927
+ uri: `${profile}.m3u8`,
928
+ ...codecs !== void 0 ? { codecs } : {}
718
929
  });
719
930
  }
720
931
  if (variants.length === 0) return noPlayback(deviceId);
@@ -756,6 +967,7 @@ function segmentsByLocation(rows) {
756
967
  */
757
968
  function buildRecordingProvider(deps) {
758
969
  const readFile = deps.readFile ?? ((absPath) => promises.readFile(absPath));
970
+ const codecCache = /* @__PURE__ */ new Map();
759
971
  return {
760
972
  getStatus: async ({ deviceId }) => {
761
973
  return statusFor(deps, deviceId, await loadDeviceConfig(deps.configStore, deviceId));
@@ -776,7 +988,7 @@ function buildRecordingProvider(deps) {
776
988
  days: distinctRecordingDays(deps.index.segments(deviceId).map((s) => s.startMs), fromMs, toMs, tzOffsetMinutes)
777
989
  };
778
990
  },
779
- getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, deviceId, fromMs, toMs),
991
+ getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
780
992
  getStorageUsage: async () => {
781
993
  const locations = deps.locations();
782
994
  const devices = [];
@@ -1446,6 +1658,15 @@ var SegmentWriter = class {
1446
1658
  var DEFAULT_WATCH_INTERVAL_MS = 2e3;
1447
1659
  /** Periodic re-evaluation to catch band boundaries crossed with no config change. */
1448
1660
  var TICK_MS = 3e4;
1661
+ /**
1662
+ * Staging dir name, created UNDER each recordings location root
1663
+ * (`<locationRoot>/.rec-tmp/<deviceId>/<profile>`). Per-location staging means
1664
+ * the rec-tmp→final relocate is ALWAYS a same-filesystem rename — never the
1665
+ * EXDEV copy+unlink fallback that rewrites every segment (2x disk writes, F1).
1666
+ * The leading dot keeps it out of the recorder's own hydrate walk (which only
1667
+ * descends `<root>/<deviceId>/`) and `parseSegmentPath` (digits-first layout).
1668
+ */
1669
+ var STAGING_DIR_NAME = ".rec-tmp";
1449
1670
  var RecordingController = class {
1450
1671
  deps;
1451
1672
  active = /* @__PURE__ */ new Map();
@@ -1644,11 +1865,13 @@ var RecordingController = class {
1644
1865
  return selected.length > 0 ? selected : assigned;
1645
1866
  }
1646
1867
  /**
1647
- * Attach one (device, profile): acquire the broker source, mkdir the temp
1648
- * outDir, spawn the SegmentWriter, and start the watcher that feeds finalized
1649
- * flat segments into `SegmentStore.onFinalized`.
1868
+ * Attach one (device, profile): acquire the broker source, mkdir the staging
1869
+ * outDir UNDER the profile's placement root (same-fs relocate see
1870
+ * {@link STAGING_DIR_NAME}), spawn the SegmentWriter, and start the watcher
1871
+ * that feeds finalized flat segments into `SegmentStore.onFinalized`.
1650
1872
  */
1651
1873
  async attachProfile(deviceId, profile, segmentSeconds) {
1874
+ const placement = this.resolvePlacement(profile);
1652
1875
  const source = await this.deps.brokerCall(() => this.deps.api.streamBroker.getStreamWithCodec.mutate({
1653
1876
  deviceId,
1654
1877
  video: "copy",
@@ -1657,7 +1880,7 @@ var RecordingController = class {
1657
1880
  ...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
1658
1881
  tag: `recorder:${deviceId}/${profile}`
1659
1882
  }));
1660
- const outDir = path.join(this.deps.tmpRoot, String(deviceId), profile);
1883
+ const outDir = path.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
1661
1884
  await promises.mkdir(outDir, { recursive: true });
1662
1885
  const writer = new SegmentWriter({
1663
1886
  rtspUrl: source.url,
@@ -1671,7 +1894,6 @@ var RecordingController = class {
1671
1894
  }
1672
1895
  });
1673
1896
  writer.start();
1674
- const placement = this.resolvePlacement(profile);
1675
1897
  return {
1676
1898
  profile,
1677
1899
  writer,
@@ -1796,44 +2018,14 @@ function makeBrokerCall(handle) {
1796
2018
  * Resolve the host a REMOTE recording node should use to dial the hub-only
1797
2019
  * stream-broker's restream port.
1798
2020
  *
1799
- * The broker mints `127.0.0.1` restream URLs (hub-local). When the operator
1800
- * designates an AGENT as the recording node, that agent's recorder must pull
1801
- * from the hub, so it needs the hub's cluster-resolvable host. Resolution
1802
- * order:
1803
- *
1804
- * 1. `override` the `recordingHubHostname` global setting, when the
1805
- * operator set one explicitly (e.g. the auto-derived host isn't reachable
1806
- * for RTSP on the recording interface).
1807
- * 2. `hubUrl` — derived from the agent's `CAMSTACK_HUB_URL` (the address it
1808
- * already uses to reach the hub). The RTSP restream port differs from the
1809
- * hub API port, but the HOST is the same — only the host is extracted.
1810
- *
1811
- * Returns `undefined` when neither yields a host (the caller logs and the pull
1812
- * stays hub-local, which simply won't reach a remote node — never a crash).
1813
- *
1814
- * Pure: the env string is passed in (not read here) so it can be unit-tested.
2021
+ * The implementation was promoted to the package-shared
2022
+ * `src/shared/hub-hostname.ts` in P2d (cross-node Phase 2) so the
2023
+ * pipeline-runner's remote-source leg shares the exact same resolver instead
2024
+ * of copying it. This module keeps the recorder-facing name as a thin alias —
2025
+ * resolution order and semantics are unchanged (operator override first, then
2026
+ * the host extracted from `CAMSTACK_HUB_URL`).
1815
2027
  */
1816
- function resolveRecordingHubHostname(override, hubUrl) {
1817
- const trimmedOverride = override?.trim();
1818
- if (trimmedOverride !== void 0 && trimmedOverride.length > 0) return trimmedOverride;
1819
- return extractHost(hubUrl);
1820
- }
1821
- /** Extract the bare host from a URL/authority string, scheme + port optional. */
1822
- function extractHost(raw) {
1823
- const value = raw?.trim();
1824
- if (value === void 0 || value.length === 0) return void 0;
1825
- try {
1826
- const parsed = new URL(value);
1827
- if (parsed.hostname.length > 0) return parsed.hostname;
1828
- } catch {}
1829
- const authority = value.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").split("/")[0] ?? "";
1830
- if (authority.startsWith("[")) {
1831
- const end = authority.indexOf("]");
1832
- if (end > 0) return authority.slice(0, end + 1);
1833
- }
1834
- const host = authority.split(":")[0] ?? "";
1835
- return host.length > 0 ? host : void 0;
1836
- }
2028
+ var resolveRecordingHubHostname = resolveHubHostname;
1837
2029
  //#endregion
1838
2030
  //#region src/recorder/addon/index.ts
1839
2031
  /**
@@ -1860,13 +2052,18 @@ function extractHost(raw) {
1860
2052
  * file ships dormant (separate tree, not wired into the manifest yet).
1861
2053
  */
1862
2054
  var DEFAULT_CONFIG = {
1863
- segmentSeconds: 4,
2055
+ segmentSeconds: 10,
1864
2056
  watchIntervalMs: 2e3
1865
2057
  };
1866
2058
  /** Playback-marker window padding around a motion event (EventMap only; the
1867
- * controller's events-band gating uses the raw trigger ms, not this pad). */
1868
- var MARKER_PAD_PRE_MS = 2e3;
1869
- var MARKER_PAD_POST_MS = 5e3;
2059
+ * controller's events-band gating uses the raw trigger ms, not this pad).
2060
+ * Derived from the SHARED `EVENT_PAD_MS` (R5/C1): markers are ephemeral
2061
+ * in-RAM and consumed by nothing today, but aligning their window with the
2062
+ * canonical clip pad means that IF they are ever surfaced, their boundaries
2063
+ * match the videoclips/admin-ui clip window for the same event — one pad
2064
+ * convention, not three. */
2065
+ var MARKER_PAD_PRE_MS = EVENT_PAD_MS.preMs;
2066
+ var MARKER_PAD_POST_MS = EVENT_PAD_MS.postMs;
1870
2067
  /** This addon's manifest id + the playback data-plane prefix — together they
1871
2068
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
1872
2069
  var RECORDER_ADDON_ID = "recorder";
@@ -1939,11 +2136,17 @@ var RecorderV2Addon = class extends BaseAddon {
1939
2136
  await promises.unlink(fromAbs);
1940
2137
  }
1941
2138
  },
2139
+ removeDir: async (locationId, relDir) => {
2140
+ const loc = this.resolvedLocations.find((l) => l.id === locationId);
2141
+ if (!loc) throw new Error(`recorder: unknown recordings location ${locationId}`);
2142
+ await promises.rm(path.join(loc.root, relDir), {
2143
+ recursive: true,
2144
+ force: true
2145
+ });
2146
+ },
1942
2147
  logger: { warn: (message, extras) => this.ctx.logger.warn(message, { meta: { extras } }) }
1943
2148
  };
1944
2149
  this.segmentStore = new SegmentStore(segmentStoreDeps);
1945
- const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
1946
- const tmpRoot = path.join(mediaRoot ?? this.ctx.dataDir, "rec-tmp");
1947
2150
  const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(BROKER_HOST_NODE_ID));
1948
2151
  this.controller = new RecordingController({
1949
2152
  api: this.ctx.api,
@@ -1953,7 +2156,6 @@ var RecorderV2Addon = class extends BaseAddon {
1953
2156
  loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
1954
2157
  enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
1955
2158
  locations: () => this.resolvedLocations,
1956
- tmpRoot,
1957
2159
  spawn,
1958
2160
  segmentSeconds: this.config.segmentSeconds,
1959
2161
  watchIntervalMs: this.config.watchIntervalMs,