@camstack/addon-pipeline 1.1.23 → 1.1.25

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