@camstack/addon-pipeline 0.1.20 → 0.2.1

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 (101) hide show
  1. package/dist/audio-analyzer/index.js +736 -719
  2. package/dist/audio-analyzer/index.mjs +726 -679
  3. package/dist/audio-codec-nodeav/index.js +304 -461
  4. package/dist/audio-codec-nodeav/index.mjs +300 -462
  5. package/dist/chunk-BdkLduGY.mjs +5 -0
  6. package/dist/chunk-D6vf50IK.js +28 -0
  7. package/dist/codec-runtime-BOk-13PN.js +202 -0
  8. package/dist/codec-runtime-BsqlEjPi.mjs +197 -0
  9. package/dist/constants-B_b0a-6h.mjs +3119 -0
  10. package/dist/{index-CMcx_k6Y.js → constants-D65v6yp6.js} +3107 -2935
  11. package/dist/decoder-nodeav/index.js +1374 -1444
  12. package/dist/decoder-nodeav/index.mjs +1369 -1425
  13. package/dist/detection-pipeline/index.js +6462 -5613
  14. package/dist/detection-pipeline/index.mjs +6451 -5574
  15. package/dist/dist-7ewQjTle.js +22454 -0
  16. package/dist/dist-C5jnNl0n.mjs +22089 -0
  17. package/dist/motion-wasm/index.js +469 -467
  18. package/dist/motion-wasm/index.mjs +464 -446
  19. package/dist/pipeline-runner/index.js +2029 -1827
  20. package/dist/pipeline-runner/index.mjs +2025 -1811
  21. package/dist/recorder/index.js +2045 -2157
  22. package/dist/recorder/index.mjs +2042 -2156
  23. package/dist/stream-broker/_stub.js +1806 -1352
  24. package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D4-DHanK.mjs +156 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Tf-HACFd.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9WX5HNw.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs +26 -0
  28. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs +26 -0
  29. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs +26 -0
  30. package/dist/stream-broker/dist-CYZr2fwk.mjs +2726 -0
  31. package/dist/stream-broker/hostInit-Di6vceAU.mjs +129 -0
  32. package/dist/stream-broker/index.js +17778 -15470
  33. package/dist/stream-broker/index.mjs +17769 -15465
  34. package/dist/stream-broker/remoteEntry.js +134 -2973
  35. package/dist/stream-broker/remoteEntry.ssr.js +33 -0
  36. package/dist/stream-broker/virtualExposes-dYNvIwoR.mjs +27 -0
  37. package/dist/stream-broker/virtual_mf-exposes-ssr___mfe_internal__addon_stream_broker_widgets__remoteEntry_js-Cmqfp4i_.mjs +10 -0
  38. package/embed-dist/assets/index-B8VlSD0-.js +150 -0
  39. package/embed-dist/assets/index-ZhDdp1Nd.css +2 -0
  40. package/embed-dist/index.html +13 -0
  41. package/package.json +25 -7
  42. package/wasm/assembly/index.ts +41 -16
  43. package/dist/audio-analyzer/index.js.map +0 -1
  44. package/dist/audio-analyzer/index.mjs.map +0 -1
  45. package/dist/audio-codec-nodeav/index.js.map +0 -1
  46. package/dist/audio-codec-nodeav/index.mjs.map +0 -1
  47. package/dist/decoder-nodeav/index.js.map +0 -1
  48. package/dist/decoder-nodeav/index.mjs.map +0 -1
  49. package/dist/detection-pipeline/index.js.map +0 -1
  50. package/dist/detection-pipeline/index.mjs.map +0 -1
  51. package/dist/index-5aYef068.mjs +0 -17514
  52. package/dist/index-5aYef068.mjs.map +0 -1
  53. package/dist/index-B36NMAdu.js +0 -17513
  54. package/dist/index-B36NMAdu.js.map +0 -1
  55. package/dist/index-CMcx_k6Y.js.map +0 -1
  56. package/dist/index-CYb7cFrv.mjs +0 -5790
  57. package/dist/index-CYb7cFrv.mjs.map +0 -1
  58. package/dist/motion-wasm/index.js.map +0 -1
  59. package/dist/motion-wasm/index.mjs.map +0 -1
  60. package/dist/pipeline-runner/index.js.map +0 -1
  61. package/dist/pipeline-runner/index.mjs.map +0 -1
  62. package/dist/recorder/index.js.map +0 -1
  63. package/dist/recorder/index.mjs.map +0 -1
  64. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/FfmpegParamsField.d.ts +0 -41
  65. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/GeometryBuilder.d.ts +0 -54
  66. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/StreamBrokerPanel.d.ts +0 -21
  67. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/format-ua.d.ts +0 -13
  68. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/index.d.ts +0 -15
  69. package/dist/stream-broker/@mf-types/widgets.d.ts +0 -2
  70. package/dist/stream-broker/@mf-types.d.ts +0 -3
  71. package/dist/stream-broker/@mf-types.zip +0 -0
  72. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-lantnv8e.mjs +0 -12
  73. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-DJ3UNg7O.mjs +0 -30
  74. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-CYXy_bhS.mjs +0 -21
  75. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-U1EUeEPs.mjs +0 -104
  76. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-DeouEaSs.mjs +0 -85
  77. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-DHUwjbb9.mjs +0 -62
  78. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-CaDEYBIU.mjs +0 -89
  79. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-D6EROtlA.mjs +0 -29
  80. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-x6pP3Ghk.mjs +0 -36
  81. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-DYEKzzY-.mjs +0 -45
  82. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CcnN6sbA.mjs +0 -6
  83. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DICOtMTl.mjs +0 -34
  84. package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CL9DR49k.mjs +0 -156
  85. package/dist/stream-broker/client-BvTmMOQu.mjs +0 -9836
  86. package/dist/stream-broker/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +0 -211
  87. package/dist/stream-broker/hostInit-ChmiMPS0.mjs +0 -168
  88. package/dist/stream-broker/index-BxsFuFmE.mjs +0 -2603
  89. package/dist/stream-broker/index-C-248uOU.mjs +0 -725
  90. package/dist/stream-broker/index-C05B6jqp.mjs +0 -185
  91. package/dist/stream-broker/index-CWkKuNLr.mjs +0 -232
  92. package/dist/stream-broker/index-DOJoSShD.mjs +0 -67784
  93. package/dist/stream-broker/index-DtOI1aTU.mjs +0 -18504
  94. package/dist/stream-broker/index-oMq6ilgR.mjs +0 -1641
  95. package/dist/stream-broker/index-vIWZQBIL.mjs +0 -435
  96. package/dist/stream-broker/index-xncRG7-x.mjs +0 -2713
  97. package/dist/stream-broker/index.js.map +0 -1
  98. package/dist/stream-broker/index.mjs.map +0 -1
  99. package/dist/stream-broker/jsx-runtime-BRT_HL0A.mjs +0 -55
  100. package/dist/stream-broker/schemas-B7L0qZtq.mjs +0 -3599
  101. package/dist/stream-broker/virtualExposes-pCd777Rp.mjs +0 -42
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","sources":["../../src/recorder/time-path.ts","../../src/recorder/ram-index.ts","../../src/recorder/retention.ts","../../src/recorder/recording-policy.ts","../../src/recorder/recorder-core.ts","../../src/recorder/retention-orchestration.ts","../../src/recorder/retention-resolve.ts","../../src/recorder/recording-targets.ts","../../src/recorder/recording-device-settings.ts","../../src/recorder/recording-placement.ts","../../src/recorder/ffmpeg-args.ts","../../src/recorder/segment-writer.ts","../../src/recorder/playlist.ts","../../src/recorder/segment-watcher.ts","../../src/recorder/disk-scan.ts","../../src/recorder/disk-usage.ts","../../src/recorder/probe-codec.ts","../../src/recorder/recorder-actions.ts","../../src/recorder/recording-device-config.ts","../../src/recorder/resolve-storage-bytes.ts","../../src/recorder/index.ts"],"sourcesContent":["const pad = (n: number, w = 2): string => String(n).padStart(w, '0')\n\n/**\n * Recording type subtree. Segments kept because continuous recording was active\n * live under `…/continuous/…`; segments kept by a motion/audio trigger live\n * under `…/events/…`. The split lets the two be retained independently (spec\n * §6). The dimension sits between `<profile>` and the time bucket, so a future\n * scrub track (another `<profile>` value) composes with it unchanged.\n */\nexport type Subtree = 'continuous' | 'events'\n\n/** Default subtree for legacy (pre-split) paths that lack the dimension. */\nexport const DEFAULT_SUBTREE: Subtree = 'continuous'\n\n/** `<YYYY>/<MM>/<DD>/<HH>` (UTC) hour bucket for a start epoch (ms). The\n * recorder's segment-watcher relocates each finalized segment into this\n * bucket under its output dir — matching {@link bucketRelPath}, which\n * playback uses — so the on-disk layout and the playback URIs always agree. */\nexport function hourBucketRelPath(startMs: number): string {\n const d = new Date(startMs)\n return `${d.getUTCFullYear()}/${pad(d.getUTCMonth() + 1)}/${pad(d.getUTCDate())}/${pad(d.getUTCHours())}`\n}\n\n/** `<camera>/<profile>/<subtree>/<YYYY>/<MM>/<DD>/<HH>` (UTC) for a start epoch (ms). */\nexport function bucketRelPath(camera: string, profile: string, subtree: Subtree, startMs: number): string {\n return `${camera}/${profile}/${subtree}/${hourBucketRelPath(startMs)}`\n}\n\n/** Full segment relative path: `<bucket>/<startMs>-<durMs>.m4s`. */\nexport function segmentRelPath(camera: string, profile: string, subtree: Subtree, startMs: number, durMs: number): string {\n return `${bucketRelPath(camera, profile, subtree, startMs)}/${startMs}-${durMs}.m4s`\n}\n\nexport interface ParsedSegment {\n camera: string\n profile: string\n subtree: Subtree\n startMs: number\n durMs: number\n}\n\n/**\n * Inverse of `segmentRelPath`. Accepts the current 8-segment layout\n * (`<camera>/<profile>/<subtree>/<Y>/<M>/<D>/<H>/<file>`) and the legacy\n * 7-segment layout (no subtree) — the latter resolves to {@link DEFAULT_SUBTREE}\n * so pre-split recordings (and the A7 legacy root) stay parseable. Returns null\n * for any other shape or a non-segment file name.\n */\nexport function parseSegmentRelPath(rel: string): ParsedSegment | null {\n const parts = rel.split('/')\n const hasSubtree = parts.length === 8\n if (parts.length !== 7 && parts.length !== 8) return null\n const camera = parts[0]!\n const profile = parts[1]!\n const subtreeRaw = hasSubtree ? parts[2]! : DEFAULT_SUBTREE\n if (subtreeRaw !== 'continuous' && subtreeRaw !== 'events') return null\n const file = parts[parts.length - 1]!\n const m = /^(\\d+)-(\\d+)\\.m4s$/.exec(file)\n if (!m) return null\n return { camera, profile, subtree: subtreeRaw, startMs: Number(m[1]), durMs: Number(m[2]) }\n}\n","import { parseSegmentRelPath, hourBucketRelPath, DEFAULT_SUBTREE, type Subtree } from './time-path.js'\nimport type { RetentionBucket } from './retention.js'\n\nconst HOUR_MS = 3_600_000\n\ninterface Seg { startMs: number; durMs: number; bytes: number; retained?: boolean; locationId?: string; subtree: Subtree }\n\n/** An un-retained segment the policy reaper must still decide on. `locationId`\n * is the storage location its file lives on, `subtree` which type subtree it\n * was relocated into — both needed for a location/subtree-aware delete. */\nexport interface PolicyCandidate { profile: string; startMs: number; durMs: number; endMs: number; locationId?: string; subtree: Subtree }\ninterface Range { profile: string; startMs: number; endMs: number }\n\n/** A raw segment returned to playback: enough to resolve its on-disk path. */\nexport interface SegmentRef { startMs: number; durMs: number; locationId?: string; subtree: Subtree }\n\n/** A scanned segment. `locationId` records which storage location's root it was\n * found under (absent for legacy single-root recordings). */\nexport interface ScanEntry { relPath: string; bytes: number; locationId?: string }\n\nexport class RamIndex {\n private readonly byCam = new Map<string, Map<string, Seg[]>>()\n\n static fromScan(entries: readonly ScanEntry[]): RamIndex {\n const idx = new RamIndex()\n for (const e of entries) {\n const p = parseSegmentRelPath(e.relPath)\n if (p) idx.add(p.camera, p.profile, p.startMs, p.durMs, e.bytes, e.locationId, p.subtree)\n }\n return idx\n }\n\n add(camera: string, profile: string, startMs: number, durMs: number, bytes: number, locationId?: string, subtree: Subtree = DEFAULT_SUBTREE): void {\n let profiles = this.byCam.get(camera)\n if (!profiles) { profiles = new Map(); this.byCam.set(camera, profiles) }\n let segs = profiles.get(profile)\n if (!segs) { segs = []; profiles.set(profile, segs) }\n if (segs.some((s) => s.startMs === startMs)) return\n segs.push({ startMs, durMs, bytes, locationId, subtree })\n segs.sort((a, b) => a.startMs - b.startMs)\n }\n\n remove(camera: string, profile: string, startMs: number): void {\n const segs = this.byCam.get(camera)?.get(profile)\n if (!segs) return\n const i = segs.findIndex((s) => s.startMs === startMs)\n if (i >= 0) segs.splice(i, 1)\n }\n\n /** Profiles that have at least one segment for this camera. */\n profiles(camera: string): string[] {\n return [...(this.byCam.get(camera)?.keys() ?? [])]\n }\n\n /** Camera keys with at least one indexed segment. */\n cameras(): string[] {\n return [...this.byCam.keys()]\n }\n\n totalBytes(camera: string): number {\n let total = 0\n for (const segs of this.byCam.get(camera)?.values() ?? []) {\n for (const s of segs) total += s.bytes\n }\n return total\n }\n\n /**\n * Raw segments for a single (camera,profile) overlapping [fromMs,toMs],\n * sorted ascending by start. Unlike `availability` (which merges\n * contiguous segments into ranges) this returns the individual\n * fragments — the input needed to build a VOD variant playlist.\n */\n listSegments(camera: string, profile: string, fromMs: number, toMs: number): SegmentRef[] {\n const segs = this.byCam.get(camera)?.get(profile)\n if (!segs) return []\n const out: SegmentRef[] = []\n for (const s of segs) {\n const segEnd = s.startMs + s.durMs\n if (segEnd <= fromMs || s.startMs >= toMs) continue\n out.push({ startMs: s.startMs, durMs: s.durMs, locationId: s.locationId, subtree: s.subtree })\n }\n return out\n }\n\n /**\n * Aggregate this camera's segments into UTC hour buckets — the unit\n * retention deletes. Each bucket carries its dir rel path, hour-start ms,\n * summed bytes (the RAM size counter the retention math prunes on), the\n * storage `locationId` its segments live on, and the `subtree` (continuous vs\n * events). Each distinct (subtree, hour, location) is a separate bucket — so\n * mixed combinations split apart and each is `rm`'d from the right root under\n * its own per-subtree retention policy. Buckets are per profile, ascending.\n */\n hourBuckets(camera: string): RetentionBucket[] {\n const out: RetentionBucket[] = []\n for (const [profile, segs] of this.byCam.get(camera) ?? []) {\n // Group by (subtree, hour, locationId): one bucket dir per\n // (subtree root x location root x hour), each separately rm'able.\n const byBucket = new Map<string, { hourStartMs: number; locationId?: string; subtree: Subtree; bytes: number; count: number }>()\n for (const s of segs) {\n const hourStartMs = Math.floor(s.startMs / HOUR_MS) * HOUR_MS\n const key = `${s.subtree} ${hourStartMs} ${s.locationId ?? ''}`\n const cur = byBucket.get(key)\n if (cur) { cur.bytes += s.bytes; cur.count += 1 }\n else byBucket.set(key, { hourStartMs, locationId: s.locationId, subtree: s.subtree, bytes: s.bytes, count: 1 })\n }\n for (const b of byBucket.values()) {\n out.push({\n relPath: `${camera}/${profile}/${b.subtree}/${hourBucketRelPath(b.hourStartMs)}`,\n hourStartMs: b.hourStartMs,\n bytes: b.bytes,\n segmentCount: b.count,\n locationId: b.locationId,\n subtree: b.subtree,\n })\n }\n }\n return out\n }\n\n /**\n * Drop every segment in the hour bucket named by `relPath`\n * (`<camera>/<profile>/<subtree>/<Y>/<M>/<D>/<H>`) whose `locationId` matches\n * (exact, incl. `undefined === undefined` for legacy single-root segments).\n * Scoping by both subtree (in the path) and location keeps the hour's\n * other-subtree / other-location segments intact when one bucket is `rm`'d.\n * Returns the count removed; a non-bucket or unknown path is a no-op. The\n * on-disk dir is removed by the sweeper — this keeps the size counter in sync.\n */\n removeBucketByRelPath(relPath: string, locationId?: string): number {\n const parts = relPath.split('/')\n if (parts.length !== 7) return 0\n const [camera, profile, subtree, y, mo, d, h] = parts\n if (subtree !== 'continuous' && subtree !== 'events') return 0\n const hourStartMs = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h))\n if (Number.isNaN(hourStartMs)) return 0\n const segs = this.byCam.get(camera!)?.get(profile!)\n if (!segs) return 0\n let removed = 0\n for (let i = segs.length - 1; i >= 0; i--) {\n const s = segs[i]!\n if (\n Math.floor(s.startMs / HOUR_MS) * HOUR_MS === hourStartMs &&\n s.locationId === locationId &&\n s.subtree === subtree\n ) {\n segs.splice(i, 1)\n removed++\n }\n }\n return removed\n }\n\n /**\n * Un-retained segments (the modes-engine policy reaper's work list). A\n * segment stays a candidate until {@link markRetained} confirms it falls in a\n * trigger window — then the reaper leaves it alone (only retention prunes it).\n */\n policyCandidates(camera: string): PolicyCandidate[] {\n const out: PolicyCandidate[] = []\n for (const [profile, segs] of this.byCam.get(camera) ?? []) {\n for (const s of segs) {\n if (s.retained) continue\n out.push({ profile, startMs: s.startMs, durMs: s.durMs, endMs: s.startMs + s.durMs, locationId: s.locationId, subtree: s.subtree })\n }\n }\n return out\n }\n\n /** Mark a segment as policy-retained so the reaper stops reconsidering it. */\n markRetained(camera: string, profile: string, startMs: number): void {\n const seg = this.byCam.get(camera)?.get(profile)?.find((s) => s.startMs === startMs)\n if (seg) seg.retained = true\n }\n\n /**\n * The earliest `startMs` of any segment for this camera across all profiles\n * and subtrees, or `null` when the camera has no indexed segments.\n */\n oldestStartMs(camera: string): number | null {\n let oldest: number | null = null\n for (const segs of this.byCam.get(camera)?.values() ?? []) {\n for (const s of segs) {\n if (oldest === null || s.startMs < oldest) oldest = s.startMs\n }\n }\n return oldest\n }\n\n /** Merged availability ranges overlapping [fromMs,toMs], per profile. */\n availability(camera: string, fromMs: number, toMs: number): Range[] {\n const out: Range[] = []\n for (const [profile, segs] of this.byCam.get(camera) ?? []) {\n let cur: Range | null = null\n for (const s of segs) {\n const segEnd = s.startMs + s.durMs\n if (segEnd <= fromMs || s.startMs >= toMs) {\n if (cur) { out.push(cur); cur = null }\n continue\n }\n if (cur && s.startMs === cur.endMs) {\n cur.endMs = segEnd\n } else {\n if (cur) out.push(cur)\n cur = { profile, startMs: s.startMs, endMs: segEnd }\n }\n }\n if (cur) out.push(cur)\n }\n return out\n }\n}\n","/**\n * Retention prune math — pure, no I/O.\n *\n * The recorder writes continuously, so without pruning an attached camera\n * fills the disk unbounded. Retention works at HOUR-BUCKET granularity (the\n * spec's deletion unit, §6): bucket directory names encode UTC time, so a\n * whole bucket can be deleted without opening any segment. {@link planRetention}\n * decides WHICH buckets to delete; the sweeper performs the `rm`.\n *\n * Two limits, applied in order:\n * 1. **age** — drop every bucket whose entire hour ended at/before the cutoff\n * (`hourStart + 1h <= now - maxAgeMs`); the bucket overlapping the cutoff\n * is kept so in-range data survives.\n * 2. **size** — if the surviving buckets still exceed `maxSizeBytes`, drop the\n * oldest survivors one at a time until under the cap, but NEVER the single\n * newest bucket (that's the live one being written).\n */\n\nconst HOUR_MS = 3_600_000\n\n/** One hour-bucket directory candidate for retention. */\nexport interface RetentionBucket {\n /** Relative path of the hour-bucket dir, e.g. `7/low/2026/06/10/12`. */\n readonly relPath: string\n /** UTC epoch ms of the bucket's hour start. */\n readonly hourStartMs: number\n /** Total bytes of segments under this bucket. */\n readonly bytes: number\n /** Number of segment files under this hour bucket (for eviction logging). */\n readonly segmentCount: number\n /**\n * Storage location this bucket's segments live under (multi-location). The\n * sweeper resolves the `rm` against this location's root, not a single shared\n * one. Absent for legacy single-root recordings. A given (camera, profile,\n * hour) that spans two locations is emitted as two buckets, one per location.\n */\n readonly locationId?: string\n /**\n * Recording type subtree (`continuous` vs `events`) this bucket belongs to.\n * Retention is applied per subtree (independent age/size policies), so the\n * sweeper filters buckets by this before planning. Absent only for buckets\n * built without the dimension (none in practice — the index always sets it).\n */\n readonly subtree?: 'continuous' | 'events'\n}\n\n/** Per-subtree retention limits. Omitted limits are not enforced. */\nexport interface RetentionPolicy {\n readonly maxAgeMs?: number\n readonly maxSizeBytes?: number\n}\n\n/** Why a bucket was selected for deletion (drives per-camera eviction logs). */\nexport type RetentionReason = 'age' | 'size' | 'free-space'\n\n/** A doomed bucket carrying the reason it was selected for deletion. */\nexport interface DoomedBucket extends RetentionBucket {\n readonly reason: RetentionReason\n}\n\n/** The buckets to delete and how much that reclaims. */\nexport interface RetentionPlan {\n /**\n * Doomed hour buckets, oldest-first. Each carries its `locationId` so the\n * sweeper resolves the `rm` against the right storage-location root (a single\n * `relPath` is no longer unique across locations) and its `reason` for the\n * per-camera eviction log.\n */\n readonly deleteBuckets: readonly DoomedBucket[]\n readonly reclaimedBytes: number\n}\n\n/**\n * Decide which hour buckets to delete for one subtree. Pure: same inputs →\n * same plan. `deleteBuckets` is oldest-first.\n */\nexport function planRetention(\n buckets: readonly RetentionBucket[],\n policy: RetentionPolicy,\n nowMs: number,\n): RetentionPlan {\n const sorted = [...buckets].sort((a, b) => a.hourStartMs - b.hourStartMs)\n const survivors: RetentionBucket[] = []\n const doomed: DoomedBucket[] = []\n\n // 1. Age prune — a bucket fully expires once its hour ends at/before cutoff.\n if (policy.maxAgeMs != null) {\n const cutoff = nowMs - policy.maxAgeMs\n for (const b of sorted) {\n if (b.hourStartMs + HOUR_MS <= cutoff) doomed.push({ ...b, reason: 'age' })\n else survivors.push(b)\n }\n } else {\n survivors.push(...sorted)\n }\n\n // 2. Size prune — drop oldest survivors until under cap, keeping >=1 (newest).\n if (policy.maxSizeBytes != null) {\n let total = survivors.reduce((sum, b) => sum + b.bytes, 0)\n while (total > policy.maxSizeBytes && survivors.length > 1) {\n const oldest = survivors.shift()!\n total -= oldest.bytes\n doomed.push({ ...oldest, reason: 'size' })\n }\n }\n\n doomed.sort((a, b) => a.hourStartMs - b.hourStartMs)\n return {\n deleteBuckets: doomed,\n reclaimedBytes: doomed.reduce((sum, b) => sum + b.bytes, 0),\n }\n}\n\n/**\n * Free-space guard selection: choose the OLDEST `hours` distinct hours of one\n * device's footage to evict, returning every profile's bucket in those hours\n * (so \"2 hours\" frees 2 full hours across all profiles, not 2 per-profile\n * bucket entries). The single newest hour is always preserved so a disk-full\n * sweep never wipes a camera's most recent footage entirely. Pure: same inputs\n * → same plan, oldest-first.\n */\nexport function selectOldestHoursToEvict(\n buckets: readonly RetentionBucket[],\n hours: number,\n): RetentionPlan {\n if (hours <= 0 || buckets.length === 0) return { deleteBuckets: [], reclaimedBytes: 0 }\n const distinctHours = [...new Set(buckets.map((b) => b.hourStartMs))].sort((a, b) => a - b)\n // Preserve the newest hour; evict from the remaining oldest hours.\n const evictable = distinctHours.slice(0, -1).slice(0, hours)\n const targets = new Set(evictable)\n const doomed: DoomedBucket[] = buckets\n .filter((b) => targets.has(b.hourStartMs))\n .sort((a, b) => a.hourStartMs - b.hourStartMs)\n .map((b) => ({ ...b, reason: 'free-space' as const }))\n return {\n deleteBuckets: doomed,\n reclaimedBytes: doomed.reduce((sum, b) => sum + b.bytes, 0),\n }\n}\n","/**\n * Recording policy / modes engine — pure, no I/O, no clock.\n *\n * A camera's policy is an ordered list of {@link RecordingRule}s; effective\n * demand is the OR over every rule whose schedule is currently active (spec\n * §5). A rule pairs a schedule (always / time-of-day window, optional weekday\n * filter) with a mode:\n * - `continuous` — demand whenever the schedule is active.\n * - `onMotion` / `onAudioThreshold` — demand only within `postBufferSec` of\n * the last qualifying trigger. The threshold test (motion happened / audio\n * over `thresholdDbfs`) is applied by the caller when it records the trigger\n * timestamp, so this layer just compares timestamps — keeping it pure.\n *\n * `preBufferSec` is intentionally NOT consulted here: it's retroactive (retain\n * already-recorded segments BEFORE a trigger) and is applied at segment\n * keep/discard time, not in the live \"is recording demanded now\" gate.\n *\n * The clock is injected as {@link ScheduleClock} (weekday + minute-of-day) so\n * the caller decides local-vs-UTC; this module never reads `Date`.\n */\nimport type { RecordingSchedule, RecordingRule } from '@camstack/types'\n\nexport {\n RecordingWeekdaySchema as WeekdaySchema,\n RecordingScheduleSchema,\n RecordingModeSchema,\n RecordingRuleSchema,\n} from '@camstack/types'\nexport type {\n RecordingWeekday as Weekday,\n RecordingSchedule,\n RecordingMode,\n RecordingRule,\n} from '@camstack/types'\n\n/** Injected clock: weekday (0=Sun) + minute-of-day (0..1439), caller's tz. */\nexport interface ScheduleClock {\n readonly weekday: number\n readonly minutesOfDay: number\n}\n\n/** Latest qualifying trigger timestamps (ms), set by the caller on each event. */\nexport interface TriggerState {\n readonly lastMotionMs?: number\n readonly lastAudioMs?: number\n}\n\nfunction toMinutes(hhmm: string): number {\n const [h, m] = hhmm.split(':')\n return Number(h) * 60 + Number(m)\n}\n\n/** Is this schedule active at the given clock instant? */\nexport function isScheduleActive(schedule: RecordingSchedule, clock: ScheduleClock): boolean {\n if (schedule.kind === 'always') return true\n if (schedule.days && !schedule.days.includes(clock.weekday)) return false\n const start = toMinutes(schedule.start)\n const end = toMinutes(schedule.end)\n const m = clock.minutesOfDay\n if (start === end) return false // zero-length window\n if (start < end) return m >= start && m < end\n // Overnight window (e.g. 22:00–06:00): active before midnight OR after.\n return m >= start || m < end\n}\n\n/**\n * OR over all rules whose schedule is active: `continuous` demands always;\n * trigger modes demand only within `postBufferSec` of their last trigger.\n */\nexport function isRecordingDemanded(\n rules: readonly RecordingRule[],\n clock: ScheduleClock,\n triggers: TriggerState,\n nowMs: number,\n): boolean {\n for (const r of rules) {\n if (!isScheduleActive(r.schedule, clock)) continue\n if (r.mode === 'continuous') return true\n const lastMs = r.mode === 'onMotion' ? triggers.lastMotionMs : triggers.lastAudioMs\n if (lastMs != null && nowMs - lastMs <= r.postBufferSec * 1000) return true\n }\n return false\n}\n\n/**\n * Is CONTINUOUS recording active at the given clock instant — i.e. does some\n * `continuous`-mode rule's schedule currently apply? Used to classify a\n * finalized segment into the `continuous` vs `events` subtree: a segment\n * recorded while continuous is active is continuous footage; one kept only by a\n * motion/audio trigger is event footage. A camera with NO rules is pure\n * continuous, handled by the caller (this returns false for an empty rule set).\n */\nexport function isContinuousActive(rules: readonly RecordingRule[], clock: ScheduleClock): boolean {\n return rules.some((r) => r.mode === 'continuous' && isScheduleActive(r.schedule, clock))\n}\n\n/**\n * Should a finalized segment `[segStartMs, segEndMs]` be kept? Like\n * {@link isRecordingDemanded} but evaluates the whole segment against each\n * trigger's full demand window `[trigger - preBufferSec, trigger + postBufferSec]`\n * — so it captures `preBufferSec` (segments BEFORE the trigger) which the live\n * point-in-time check can't. The policy reaper uses this to decide retain vs\n * discard for held segments.\n */\nexport function isSegmentDemanded(\n rules: readonly RecordingRule[],\n clock: ScheduleClock,\n triggers: TriggerState,\n segStartMs: number,\n segEndMs: number,\n): boolean {\n for (const r of rules) {\n if (!isScheduleActive(r.schedule, clock)) continue\n if (r.mode === 'continuous') return true\n const lastMs = r.mode === 'onMotion' ? triggers.lastMotionMs : triggers.lastAudioMs\n if (lastMs == null) continue\n const winStart = lastMs - r.preBufferSec * 1000\n const winEnd = lastMs + r.postBufferSec * 1000\n if (segEndMs >= winStart && segStartMs <= winEnd) return true // segment overlaps the window\n }\n return false\n}\n","import type { LogExtras, CamProfile } from '@camstack/types'\nimport { RamIndex, type ScanEntry, type SegmentRef } from './ram-index.js'\nimport { planRetention, type RetentionPolicy, type RetentionBucket, type RetentionReason } from './retention.js'\nimport { isRecordingDemanded, isSegmentDemanded, isContinuousActive, type RecordingRule, type ScheduleClock } from './recording-policy.js'\nimport type { Subtree } from './time-path.js'\nimport type { PassthroughArgs } from './ffmpeg-args.js'\n\n/**\n * One segment from a sized disk scan, ready to be seeded into a live index.\n * Mirrors the `onSegmentFinalized` parameters so callers can convert a\n * `ScanEntry` (which carries `relPath`) via `parseSegmentRelPath` and pass the\n * result directly.\n */\nexport interface SeedSegment {\n readonly profile: string\n readonly startMs: number\n readonly durMs: number\n readonly bytes: number\n readonly locationId?: string\n readonly subtree: Subtree\n}\n\n/**\n * Transport-free recorder core.\n *\n * Holds one `RamIndex` + one writer per profile per attached camera and\n * answers the `recording` cap queries (availability / status / segments).\n * All I/O — RTSP resolution, writer construction, logging — is injected so\n * the core stays unit-testable without ffmpeg, the broker, or the bus.\n */\n\n/** Minimal writer contract the core drives. Concrete impl = `SegmentWriter`. */\nexport interface RecorderWriter {\n start(): void\n stop(): void\n}\n\n/** Config the core hands `makeWriter` for one (camera,profile). */\nexport type RecorderWriterConfig = PassthroughArgs\n\n/** An acquired stream source: a pull URL plus the broker refcount key. */\nexport interface AcquiredSource {\n readonly url: string\n readonly pipelineKey: string\n}\n\nexport interface RecorderCoreDeps {\n readonly dataDir: string\n readonly nodeId: string\n /**\n * Acquire a demand-counted broker stream for a (deviceId, profile) via\n * `streamBroker.getStreamWithCodec`. The returned `pipelineKey` is released\n * on detach so the broker can drop the source dial when no one else needs\n * it — this is what makes the recorder ride the single source pull.\n */\n readonly acquireSource: (deviceId: number, profile: CamProfile) => Promise<AcquiredSource>\n /** Release a previously acquired source (`releaseStreamWithCodec`). */\n readonly releaseSource: (pipelineKey: string) => Promise<void>\n /** Construct (but do not start) a writer for one profile. */\n readonly makeWriter: (cfg: RecorderWriterConfig) => RecorderWriter\n readonly logger: {\n warn: (m: string, extras?: LogExtras) => void\n info: (m: string, extras?: LogExtras) => void\n }\n}\n\nexport interface AttachInput {\n readonly deviceId: number\n readonly profiles: readonly CamProfile[]\n readonly segmentSeconds: number\n /** Policy rules; empty/omitted = continuous (always record). */\n readonly rules?: readonly RecordingRule[]\n /**\n * Per-profile absolute output dir (the chosen storage location's root +\n * `<deviceId>/<profile>`). When omitted for a profile, falls back to\n * `dataDir/<deviceId>/<profile>` (single-root legacy). The addon resolves\n * these from the placement config — the core stays storage-cap-agnostic.\n */\n readonly outDirs?: Readonly<Record<string, string>>\n}\n\n/** Trigger kind that can satisfy a `onMotion` / `onAudioThreshold` rule. */\nexport type TriggerKind = 'motion' | 'audio'\n\nexport interface RecorderStatus {\n readonly deviceId: number\n readonly enabled: boolean\n readonly activeMode: 'off' | 'continuous'\n readonly nodeId: string\n readonly storageBytes: number\n}\n\nexport interface RecorderAvailability {\n readonly deviceId: number\n readonly ranges: ReturnType<RamIndex['availability']>\n}\n\n/** Per-device retention outcome of one sweep. */\nexport interface RetentionSweepResult {\n readonly deviceId: number\n readonly deletedBuckets: number\n readonly reclaimedBytes: number\n}\n\n/** Aggregate outcome of one free-space guard sweep. */\nexport interface FreeSpaceSweepResult {\n readonly rounds: number\n readonly deletedBuckets: number\n readonly reclaimedBytes: number\n readonly finalFreePercent: number\n}\n\n/** Resolve the retention policy for a device's `continuous` vs `events` subtree\n * (per-device ?? per-subtree ?? global). */\nexport type RetentionPolicyResolver = (deviceId: number, subtree: Subtree) => RetentionPolicy\n\n/**\n * Remove an hour-bucket directory. `relPath` is the bucket's path\n * (`<deviceId>/<profile>/<Y>/<M>/<D>/<H>`); `locationId` is the storage location\n * it lives on, so the addon resolves the `rm` against the right root (absent for\n * legacy single-root recordings).\n */\nexport type RemoveBucketDir = (relPath: string, locationId?: string) => Promise<void>\n\n/** Delete one relocated segment file (the modes-engine reaper's discard).\n * `locationId` is the storage location the file lives on, `subtree` the\n * continuous/events subtree it was relocated into — both needed to resolve its\n * on-disk path. */\nexport type DeleteSegment = (deviceId: number, profile: string, startMs: number, durMs: number, locationId: string | undefined, subtree: Subtree) => Promise<void>\n\ninterface TriggerState {\n lastMotionMs?: number\n lastAudioMs?: number\n}\n\n/** Per-(reason) eviction tally accumulated while pruning one subtree. */\ninterface EvictionTally {\n readonly deletedBuckets: number\n readonly deletedSegments: number\n readonly reclaimedBytes: number\n}\n\ninterface CameraState {\n readonly index: RamIndex\n readonly writers: RecorderWriter[]\n readonly profiles: readonly string[]\n /** Broker pipeline keys to release on detach (one per writer). */\n readonly pipelineKeys: readonly string[]\n /** Policy rules; empty = continuous (always record). */\n readonly rules: readonly RecordingRule[]\n /** Latest motion/audio trigger timestamps driving onMotion/onAudioThreshold. */\n readonly triggers: TriggerState\n}\n\nexport class RecorderCore {\n private readonly cameras = new Map<number, CameraState>()\n /**\n * Index of ALL recordings on disk (attached or not), rebuilt from a periodic\n * disk scan. Read-only fallback for queries about a camera that isn't\n * currently attached — so past recordings stay playable after detach/restart.\n */\n private playbackIndex = new RamIndex()\n\n constructor(private readonly deps: RecorderCoreDeps) {}\n\n /** Replace the playback index from a fresh disk scan (boot + periodic). */\n setPlaybackIndex(entries: readonly ScanEntry[]): void {\n this.playbackIndex = RamIndex.fromScan(entries)\n }\n\n /** The index that answers queries for a camera: its live one if attached,\n * else the scanned playback index. */\n private indexFor(deviceId: number): RamIndex {\n return this.cameras.get(deviceId)?.index ?? this.playbackIndex\n }\n\n /**\n * Start continuous recording for a camera: one passthrough writer per\n * profile, each pulling its resolved RTSP url into a per-profile output\n * directory. Idempotent-ish — re-attaching a live camera detaches first.\n */\n async attach(input: AttachInput): Promise<void> {\n if (input.profiles.length === 0) {\n throw new Error(`RecorderCore.attach: no profiles for device ${input.deviceId}`)\n }\n if (this.cameras.has(input.deviceId)) await this.detach(input.deviceId)\n\n const index = new RamIndex()\n const writers: RecorderWriter[] = []\n const pipelineKeys: string[] = []\n for (const profile of input.profiles) {\n const source = await this.deps.acquireSource(input.deviceId, profile)\n const outDir = input.outDirs?.[profile] ?? `${this.deps.dataDir}/${input.deviceId}/${profile}`\n const writer = this.deps.makeWriter({ rtspUrl: source.url, outDir, segmentSeconds: input.segmentSeconds })\n writer.start()\n writers.push(writer)\n pipelineKeys.push(source.pipelineKey)\n }\n this.cameras.set(input.deviceId, {\n index, writers, profiles: [...input.profiles], pipelineKeys,\n rules: input.rules ? [...input.rules] : [],\n triggers: {},\n })\n this.deps.logger.info('recorder attached', {\n meta: {\n deviceId: input.deviceId,\n profiles: input.profiles,\n segmentSeconds: input.segmentSeconds,\n ruleCount: input.rules?.length ?? 0,\n },\n })\n }\n\n /**\n * Record a motion / audio trigger for a camera (drives `onMotion` /\n * `onAudioThreshold` rules). The caller applies the threshold test (motion\n * happened / audio over `thresholdDbfs`) before calling — the core only\n * stores the latest qualifying timestamp. No-op for an unattached camera.\n */\n recordTrigger(deviceId: number, kind: TriggerKind, tsMs: number): void {\n const cam = this.cameras.get(deviceId)\n if (!cam) return\n if (kind === 'motion') cam.triggers.lastMotionMs = tsMs\n else cam.triggers.lastAudioMs = tsMs\n }\n\n /**\n * Feed an audio level (dBFS) sample for a camera. Records an audio trigger\n * iff some `onAudioThreshold` rule's `thresholdDbfs` is met (louder = dBFS\n * closer to 0, so `dbfs >= thresholdDbfs`). The threshold lives on the rule,\n * so the comparison stays here rather than in the event-bus glue. No-op for an\n * unattached camera or one with no audio rule.\n */\n onAudioLevel(deviceId: number, dbfs: number, tsMs: number): void {\n const cam = this.cameras.get(deviceId)\n if (!cam) return\n const over = cam.rules.some(\n (r) => r.mode === 'onAudioThreshold' && r.thresholdDbfs != null && dbfs >= r.thresholdDbfs,\n )\n if (over) cam.triggers.lastAudioMs = tsMs\n }\n\n /**\n * Decide whether a finalized segment `[segStartMs, segEndMs]` should be\n * kept (relocated into the permanent tree + indexed) or discarded. No rules\n * = continuous (always keep). Trigger modes keep only when recording is\n * demanded at the segment's end (within `postBufferSec` of a trigger).\n * `preBufferSec` (retroactive retain BEFORE a trigger) is NOT handled here —\n * that's increment B. Unknown camera → discard.\n */\n shouldRetainSegment(deviceId: number, _segStartMs: number, segEndMs: number, clock: ScheduleClock): boolean {\n const cam = this.cameras.get(deviceId)\n if (!cam) return false\n if (cam.rules.length === 0) return true\n return isRecordingDemanded(cam.rules, clock, cam.triggers, segEndMs)\n }\n\n /** Mark an indexed segment policy-retained (reaper stops reconsidering it). */\n markSegmentRetained(deviceId: number, profile: string, startMs: number): void {\n this.cameras.get(deviceId)?.index.markRetained(String(deviceId), profile, startMs)\n }\n\n /**\n * Policy reaper — the pre/post-buffer enforcement (modes engine increment B).\n * For every rules-camera, walk its un-retained (candidate) segments:\n * - inside a trigger window (incl. `preBufferSec` BEFORE a trigger) → mark\n * retained so it survives (only retention prunes it after);\n * - else, once the segment is older than the longest `preBufferSec` (so a\n * late trigger can no longer claim it retroactively) → delete it;\n * - otherwise hold it — re-checked next sweep.\n * Cameras with NO rules are pure-continuous: skipped here (retention-only).\n */\n async sweepPolicy(toClock: (ms: number) => ScheduleClock, nowMs: number, deleteSegment: DeleteSegment): Promise<void> {\n for (const [deviceId, cam] of this.cameras) {\n if (cam.rules.length === 0) continue\n const maxPreBufferMs = Math.max(0, ...cam.rules.map((r) => r.preBufferSec)) * 1000\n const camKey = String(deviceId)\n for (const c of cam.index.policyCandidates(camKey)) {\n if (isSegmentDemanded(cam.rules, toClock(c.endMs), cam.triggers, c.startMs, c.endMs)) {\n cam.index.markRetained(camKey, c.profile, c.startMs)\n } else if (c.endMs + maxPreBufferMs <= nowMs) {\n try {\n await deleteSegment(deviceId, c.profile, c.startMs, c.durMs, c.locationId, c.subtree)\n cam.index.remove(camKey, c.profile, c.startMs)\n } catch (err) {\n this.deps.logger.warn('recorder policy reap rm failed', {\n tags: { deviceId },\n meta: { deviceId, profile: c.profile, startMs: c.startMs, error: String(err) },\n })\n }\n }\n }\n }\n }\n\n /** Record a finalized segment into the camera's in-memory index. `locationId`\n * is the storage location its file was written to; `subtree` the\n * continuous/events subtree it was relocated into (per-subtree retention +\n * path resolution). */\n onSegmentFinalized(deviceId: number, profile: string, startMs: number, durMs: number, bytes: number, locationId?: string, subtree: Subtree = 'continuous'): void {\n this.cameras.get(deviceId)?.index.add(String(deviceId), profile, startMs, durMs, bytes, locationId, subtree)\n }\n\n /**\n * Seed a camera's live index with pre-existing on-disk segments discovered by\n * a sized scan. Idempotent: `RamIndex.add` already dedupes by `(profile,\n * startMs)` — re-seeding or a concurrent `onSegmentFinalized` for the same\n * start timestamp is silently ignored, so there is no double-count.\n *\n * MUST be called AFTER `core.attach(deviceId, …)` so the seed lands in the\n * camera's live index. If the camera is not attached (i.e. `attach` was never\n * called or already detached), this is a no-op — warn and return.\n */\n seedIndexFromScan(deviceId: number, entries: readonly SeedSegment[]): void {\n const cam = this.cameras.get(deviceId)\n if (!cam) {\n this.deps.logger.warn('recorder seedIndexFromScan: device not attached, skipping seed', { tags: { deviceId }, meta: { deviceId } })\n return\n }\n const camKey = String(deviceId)\n for (const e of entries) {\n cam.index.add(camKey, e.profile, e.startMs, e.durMs, e.bytes, e.locationId, e.subtree)\n }\n }\n\n /** Raw segments for one (camera,profile) overlapping [fromMs,toMs]. Falls\n * back to the scanned playback index when the camera isn't attached. Each\n * ref carries its `locationId` + `subtree` so playback resolves the path. */\n listSegments(deviceId: number, profile: string, fromMs: number, toMs: number): SegmentRef[] {\n return this.indexFor(deviceId).listSegments(String(deviceId), profile, fromMs, toMs)\n }\n\n /**\n * Classify a finalized segment ending at `segEndMs` into its on-disk subtree:\n * `continuous` when continuous recording is active at that time (a continuous\n * rule's schedule applies, or the camera has NO rules = pure continuous),\n * else `events` (kept only by a motion/audio trigger). Drives where the\n * watcher relocates the segment and how it's retained. Unknown camera →\n * `continuous` (harmless default; an unattached camera never finalizes).\n */\n classifySubtree(deviceId: number, _segEndMs: number, clock: ScheduleClock): Subtree {\n const cam = this.cameras.get(deviceId)\n if (!cam || cam.rules.length === 0) return 'continuous'\n return isContinuousActive(cam.rules, clock) ? 'continuous' : 'events'\n }\n\n /** Profiles this camera has recordings for — live profiles if attached, else\n * whatever the disk scan found. */\n getProfiles(deviceId: number): readonly string[] {\n const cam = this.cameras.get(deviceId)\n return cam ? cam.profiles : this.playbackIndex.profiles(String(deviceId))\n }\n\n getAvailability(deviceId: number, fromMs: number, toMs: number): RecorderAvailability {\n return { deviceId, ranges: this.indexFor(deviceId).availability(String(deviceId), fromMs, toMs) }\n }\n\n getStatus(deviceId: number): RecorderStatus {\n const cam = this.cameras.get(deviceId)\n const enabled = (cam?.writers.length ?? 0) > 0\n return {\n deviceId,\n enabled,\n activeMode: enabled ? 'continuous' : 'off',\n nodeId: this.deps.nodeId,\n storageBytes: this.indexFor(deviceId).totalBytes(String(deviceId)),\n }\n }\n\n /**\n * Prune aged-out / over-cap hour buckets for every attached camera, applying\n * the `continuous` and `events` subtree policies INDEPENDENTLY (spec §6 —\n * event footage usually outlives continuous).\n *\n * Per camera: aggregate the RAM index into hour buckets, split them by\n * subtree, ask {@link planRetention} which to delete using that subtree's\n * policy, `rm` each bucket dir, and — only on a successful `rm` — drop its\n * segments from the index so the storage counter stays truthful. A failed\n * `rm` leaves the bucket in the index for the next sweep to retry (no silent\n * byte-counter drift). Pure decision, injected I/O.\n */\n async sweepRetention(\n resolvePolicy: RetentionPolicyResolver,\n nowMs: number,\n removeBucketDir: RemoveBucketDir,\n ): Promise<RetentionSweepResult[]> {\n const results: RetentionSweepResult[] = []\n for (const [deviceId, cam] of this.cameras) {\n const { deletedBuckets, reclaimedBytes } = await this.pruneSubtrees(\n deviceId, cam.index, resolvePolicy, nowMs, removeBucketDir, 'recorder retention rm failed',\n )\n results.push({ deviceId, deletedBuckets, reclaimedBytes })\n }\n return results\n }\n\n /**\n * Shared per-device retention prune used by {@link sweepRetention} and\n * {@link pruneFootageForDevice}: split the camera's hour buckets by subtree,\n * `rm` each doomed bucket, sync the index only on a successful `rm`, and emit\n * one structured `recorder eviction` INFO line per (subtree, reason) group\n * that actually deleted something — carrying the reason, segment + byte\n * totals, and the policy condition that triggered the eviction.\n */\n private async pruneSubtrees(\n deviceId: number,\n index: RamIndex,\n resolvePolicy: RetentionPolicyResolver,\n nowMs: number,\n removeBucketDir: RemoveBucketDir,\n rmFailedMsg: string,\n ): Promise<{ deletedBuckets: number; reclaimedBytes: number }> {\n const camKey = String(deviceId)\n let deletedBuckets = 0\n let reclaimedBytes = 0\n // Split this camera's buckets by subtree and prune each with its own\n // policy. `subtree` is always set by the index; default defensively.\n const bySubtree = new Map<Subtree, RetentionBucket[]>()\n for (const b of index.hourBuckets(camKey)) {\n const st = b.subtree ?? 'continuous'\n const arr = bySubtree.get(st)\n if (arr) arr.push(b)\n else bySubtree.set(st, [b])\n }\n for (const [subtree, buckets] of bySubtree) {\n const policy = resolvePolicy(deviceId, subtree)\n // Snapshot the device's footprint before this subtree's deletions so the\n // `size` condition can report how full the camera was when it overflowed.\n const usedBytesBefore = index.totalBytes(camKey)\n const plan = planRetention(buckets, policy, nowMs)\n const byReason = new Map<RetentionReason, EvictionTally>()\n for (const b of plan.deleteBuckets) {\n try {\n await removeBucketDir(b.relPath, b.locationId)\n } catch (err) {\n this.deps.logger.warn(rmFailedMsg, {\n tags: { deviceId },\n meta: { deviceId, relPath: b.relPath, locationId: b.locationId, error: String(err) },\n })\n continue\n }\n const before = index.totalBytes(camKey)\n index.removeBucketByRelPath(b.relPath, b.locationId)\n const reclaimed = before - index.totalBytes(camKey)\n reclaimedBytes += reclaimed\n deletedBuckets++\n const prev = byReason.get(b.reason)\n byReason.set(b.reason, {\n deletedBuckets: (prev?.deletedBuckets ?? 0) + 1,\n deletedSegments: (prev?.deletedSegments ?? 0) + b.segmentCount,\n reclaimedBytes: (prev?.reclaimedBytes ?? 0) + reclaimed,\n })\n }\n for (const [reason, tally] of byReason) {\n this.deps.logger.info('recorder eviction', {\n tags: { deviceId },\n meta: {\n deviceId,\n reason,\n subtree,\n deletedBuckets: tally.deletedBuckets,\n deletedSegments: tally.deletedSegments,\n reclaimedBytes: tally.reclaimedBytes,\n condition: reason === 'age'\n ? { maxAgeDays: policy.maxAgeMs != null ? policy.maxAgeMs / 86_400_000 : null }\n : { maxSizeGb: policy.maxSizeBytes != null ? policy.maxSizeBytes / 1_000_000_000 : null, usedBytesBefore },\n },\n })\n }\n }\n return { deletedBuckets, reclaimedBytes }\n }\n\n /**\n * Apply this device's retention policy to its footage RIGHT NOW for one\n * device, returning the oldest surviving segment start (the \"retention\n * floor\") plus deletion accounting. The floor is the key output B3 uses to\n * prune analytics events to the same boundary.\n *\n * Mirrors the per-device inner loop of {@link sweepRetention} but operates\n * on a single camera and exposes the result — reuses the same\n * `planRetention` + `removeBucketDir` path so the math stays consistent.\n *\n * @param deviceId Camera to prune.\n * @param nowMs Wall-clock time for the age cutoff.\n * @param resolvePolicy Returns the retention policy for each subtree.\n * @param removeBucketDir Injected `rm -rf` for one hour-bucket dir.\n */\n async pruneFootageForDevice(\n deviceId: number,\n nowMs: number,\n resolvePolicy: RetentionPolicyResolver,\n removeBucketDir: RemoveBucketDir,\n ): Promise<{ floorMs: number | null; deletedBuckets: number; reclaimedBytes: number }> {\n const cam = this.cameras.get(deviceId)\n // Device not attached → nothing to prune (no live index); playback index\n // is read-only so we don't mutate it here.\n if (!cam) {\n return { floorMs: null, deletedBuckets: 0, reclaimedBytes: 0 }\n }\n const camKey = String(deviceId)\n const { deletedBuckets, reclaimedBytes } = await this.pruneSubtrees(\n deviceId, cam.index, resolvePolicy, nowMs, removeBucketDir, 'recorder pruneFootage rm failed',\n )\n\n // floorMs = oldest surviving segment across ALL profiles and subtrees.\n const floorMs = cam.index.oldestStartMs(camKey)\n return { floorMs, deletedBuckets, reclaimedBytes }\n }\n\n /**\n * Every device with recordings on this node — attached cameras (live RAM\n * index) plus detached ones surfaced by the disk-scan playback index. The\n * free-space guard frees space \"from every device\", so it walks the union.\n */\n private recordedDeviceIds(): number[] {\n const ids = new Set<number>()\n for (const id of this.cameras.keys()) ids.add(id)\n for (const cam of this.playbackIndex.cameras()) {\n const n = Number(cam)\n if (Number.isFinite(n)) ids.add(n)\n }\n return [...ids]\n }\n\n /**\n * Total footage bytes stored on a location, across every recorded device.\n * Backs the `storage-evictable` provider's `getEvictableUsage` so the core\n * StoragePressureManager can fan out a deficit proportionally.\n */\n evictableBytesOnLocation(locationId: string): number {\n let total = 0\n for (const deviceId of this.recordedDeviceIds()) {\n const camKey = String(deviceId)\n for (const b of this.indexFor(deviceId).hourBuckets(camKey)) {\n if (b.locationId === locationId) total += b.bytes\n }\n }\n return total\n }\n\n /**\n * Free ~`targetBytes` of footage on a location by deleting the OLDEST hour\n * buckets first, globally across devices (oldest-anywhere wins). Backs the\n * `storage-evictable` provider's `evict`. Only successfully-removed buckets\n * count toward `reclaimedBytes`. `exhausted` = ran out of buckets before\n * hitting the target. Per-device eviction is logged device-tagged.\n */\n async evictBytesOnLocation(\n locationId: string,\n targetBytes: number,\n removeBucketDir: RemoveBucketDir,\n ): Promise<{ reclaimedBytes: number; exhausted: boolean }> {\n const candidates: { deviceId: number; bucket: RetentionBucket }[] = []\n for (const deviceId of this.recordedDeviceIds()) {\n const camKey = String(deviceId)\n for (const b of this.indexFor(deviceId).hourBuckets(camKey)) {\n if (b.locationId === locationId) candidates.push({ deviceId, bucket: b })\n }\n }\n // Oldest-anywhere first so disk pressure trims the globally-stalest footage.\n candidates.sort((a, b) => a.bucket.hourStartMs - b.bucket.hourStartMs)\n\n let reclaimed = 0\n const perDevice = new Map<number, { buckets: number; bytes: number }>()\n let hitTarget = false\n for (const { deviceId, bucket } of candidates) {\n if (reclaimed >= targetBytes) { hitTarget = true; break }\n try {\n await removeBucketDir(bucket.relPath, bucket.locationId)\n } catch (err) {\n this.deps.logger.warn('recorder free-space rm failed', {\n tags: { deviceId },\n meta: { deviceId, relPath: bucket.relPath, locationId: bucket.locationId, error: String(err) },\n })\n continue\n }\n const index = this.indexFor(deviceId)\n const camKey = String(deviceId)\n const before = index.totalBytes(camKey)\n index.removeBucketByRelPath(bucket.relPath, bucket.locationId)\n const r = before - index.totalBytes(camKey)\n reclaimed += r\n const tally = perDevice.get(deviceId) ?? { buckets: 0, bytes: 0 }\n tally.buckets += 1\n tally.bytes += r\n perDevice.set(deviceId, tally)\n }\n\n for (const [deviceId, tally] of perDevice) {\n if (tally.buckets > 0) {\n this.deps.logger.info('recorder eviction', {\n tags: { deviceId },\n meta: { deviceId, reason: 'free-space', locationId, deletedBuckets: tally.buckets, reclaimedBytes: tally.bytes },\n })\n }\n }\n\n return { reclaimedBytes: reclaimed, exhausted: !hitTarget }\n }\n\n\n /**\n * Clear the live index for an attached camera so it can be re-seeded from a\n * fresh sized disk scan. All writers and policy state are preserved — only the\n * segment byte accounting is reset. No-op when the camera is not attached\n * (matches the `seedIndexFromScan` contract: seeding a non-attached device is\n * also a no-op, so a clear+reseed pair is always safe to call unconditionally).\n *\n * Intended usage (see `rescanStorage` provider in `index.ts`):\n * 1. `core.rescanDevice(deviceId)` — drop stale index entries\n * 2. `await seedIndexForDevice(core, deviceId)` — re-populate from disk\n * 3. `core.getStatus(deviceId)` — now reflects true on-disk bytes\n */\n rescanDevice(deviceId: number): void {\n const cam = this.cameras.get(deviceId)\n if (!cam) return\n // Replace the index object in-place so in-flight onSegmentFinalized calls\n // that arrive just after the clear land in the new index, not the old one.\n this.cameras.set(deviceId, { ...cam, index: new RamIndex() })\n }\n\n /**\n * Stop all writers for a camera, release its broker sources, and drop its\n * index. Writers are stopped FIRST (ffmpeg disconnects from the restream)\n * before the broker demand is released, so the broker can idle cleanly.\n */\n async detach(deviceId: number): Promise<void> {\n const cam = this.cameras.get(deviceId)\n if (!cam) return\n for (const w of cam.writers) w.stop()\n this.cameras.delete(deviceId)\n const releases = await Promise.allSettled(\n cam.pipelineKeys.map((key) => this.deps.releaseSource(key)),\n )\n for (const r of releases) {\n if (r.status === 'rejected') {\n this.deps.logger.warn('recorder source release failed', {\n tags: { deviceId },\n meta: { deviceId, error: String(r.reason) },\n })\n }\n }\n this.deps.logger.info('recorder detached', { tags: { deviceId }, meta: { deviceId } })\n }\n\n /** Stop every camera — used on addon shutdown. */\n async detachAll(): Promise<void> {\n await Promise.all([...this.cameras.keys()].map((deviceId) => this.detach(deviceId)))\n }\n\n /** Device ids currently recording. */\n attachedDevices(): readonly number[] {\n return [...this.cameras.keys()]\n }\n}\n","/**\n * retention-orchestration — B3: event pruning coordinated with footage retention.\n *\n * After pruning a device's footage via `RecorderCore.pruneFootageForDevice`, the\n * resulting floor (oldest surviving segment start) is forwarded to\n * `pipelineAnalytics.pruneEventsBefore` so analytics events + thumbnails never\n * outlive footage. The analytics call is best-effort: a missing or failing\n * `pipelineAnalytics` cap MUST NOT abort the footage sweep.\n */\nimport type { RecorderCore, RetentionPolicyResolver, RemoveBucketDir } from './recorder-core.js'\nimport { errMsg } from '@camstack/types'\n\n/** Minimal slice of ctx.api that the orchestration needs (analytics cap). */\nexport interface RetentionOrchestrationApi {\n readonly pipelineAnalytics: {\n readonly pruneEventsBefore: {\n readonly mutate: (input: { readonly deviceId: number; readonly cutoffMs: number }) => Promise<{\n readonly motion: number\n readonly object: number\n readonly audio: number\n }>\n }\n }\n}\n\n/** Minimal logger slice (mirrors the `IScopedLogger` subset used in the sweep). */\nexport interface RetentionOrchestrationLogger {\n info(message: string, extras: { meta: Record<string, unknown> }): void\n warn(message: string, extras: { meta: Record<string, unknown> }): void\n}\n\n/**\n * Prune footage for one device, then — if footage survives — prune analytics\n * events to the same floor (best-effort).\n *\n * @param core Recorder core holding the per-device RAM index.\n * @param deviceId Camera to prune.\n * @param nowMs Wall-clock time for the age cutoff.\n * @param resolvePolicy Returns the retention policy for each (device, subtree).\n * @param removeBucketDir Injected `rm -rf` for one hour-bucket dir.\n * @param api `ctx.api` slice — analytics cap may be absent.\n * @param logger Scoped logger (warn on analytics failure).\n */\nexport async function pruneDeviceWithEvents(\n core: RecorderCore,\n deviceId: number,\n nowMs: number,\n resolvePolicy: RetentionPolicyResolver,\n removeBucketDir: RemoveBucketDir,\n api: RetentionOrchestrationApi,\n logger: RetentionOrchestrationLogger,\n): Promise<void> {\n const result = await core.pruneFootageForDevice(\n deviceId,\n nowMs,\n resolvePolicy,\n removeBucketDir,\n )\n\n // Emit per-device INFO log when footage was actually evicted — lets operators\n // see exactly why recordings are being cut without scanning DEBUG noise.\n if (result.deletedBuckets > 0) {\n logger.info('recorder footage eviction', {\n meta: {\n deviceId,\n deletedBuckets: result.deletedBuckets,\n reclaimedBytes: result.reclaimedBytes,\n floorMs: result.floorMs,\n },\n })\n }\n\n // No surviving footage → nothing to align analytics to.\n if (result.floorMs == null) return\n\n try {\n await api.pipelineAnalytics.pruneEventsBefore.mutate({ deviceId, cutoffMs: result.floorMs })\n } catch (err: unknown) {\n logger.warn('recorder: pruneEventsBefore failed', {\n meta: { deviceId, error: errMsg(err) },\n })\n }\n}\n","import type { RecordingRetention } from '@camstack/types'\nimport type { RetentionPolicy } from './retention.js'\n\nconst MS_PER_DAY = 86_400_000\nconst BYTES_PER_GB = 1_000_000_000\n\n/** Per-device override (`> 0`) wins over the resolved node default. */\nexport function resolveDeviceRetention(\n override: RecordingRetention | undefined,\n defaultAgeDays: number,\n defaultSizeGb: number,\n): RetentionPolicy {\n const ageDays = override?.maxAgeDays && override.maxAgeDays > 0 ? override.maxAgeDays : defaultAgeDays\n const sizeGb = override?.maxSizeGb && override.maxSizeGb > 0 ? override.maxSizeGb : defaultSizeGb\n return {\n maxAgeMs: ageDays > 0 ? ageDays * MS_PER_DAY : undefined,\n maxSizeBytes: sizeGb > 0 ? sizeGb * BYTES_PER_GB : undefined,\n }\n}\n","/**\n * Durable per-camera recording intent.\n *\n * P1 `attachCamera` was ephemeral — a hub/addon restart (or a redeploy) lost\n * every recording. This persists which cameras the operator enabled (plus the\n * profiles / segment length they chose) in the addon-level settings store, so\n * the recorder can re-attach them on boot. The map lives under a single\n * top-level key (`writeAddonStore` merges, so it never clobbers the global\n * config keys), mirroring how `device-manager` persists `deviceBindings`.\n *\n * Default = no recording: an empty / absent map means nothing records until\n * the operator (or, later, the orchestrator) enables a camera.\n *\n * Persistence is routed through a {@link createDurableState} handle so the\n * WHOLE Zod-validated blob round-trips on every save — the schema\n * (`RecordingConfigSchema`, i.e. EVERY field of a recording target) is the\n * single source of truth for what is written. No hand-listed field set can\n * silently drop a target field (e.g. `rules` / `retention`) on persist.\n */\nimport { z } from 'zod'\nimport { RecordingConfigSchema, createDurableState } from '@camstack/types'\nimport type { RecordingConfig } from '@camstack/types'\n\nexport const RecordingTargetSchema = RecordingConfigSchema\nexport type RecordingTarget = RecordingConfig\n\n/**\n * The persisted shape under {@link RECORDING_TARGETS_KEY}: a record keyed by\n * (stringified) numeric deviceId → the FULL recording target. Backward\n * compatible — the previous code persisted exactly this shape.\n */\nexport const RecordingTargetsBlobSchema = z.record(z.string(), RecordingTargetSchema)\nexport type RecordingTargetsBlob = z.infer<typeof RecordingTargetsBlobSchema>\n\nexport const RECORDING_TARGETS_KEY = 'recordingTargets'\n\n/** The slice of `ctx.settings` this module needs (also satisfied by test fakes). */\nexport interface AddonStoreLike {\n readAddonStore(): Promise<Record<string, unknown>>\n writeAddonStore(patch: Record<string, unknown>): Promise<void>\n}\n\n/**\n * Build the durable handle over the recording-targets key for a given store.\n * The whole `RecordingTargetsBlobSchema` value round-trips on read/write, so\n * no per-target field can be dropped on persist. A blob that fails validation\n * (e.g. a partially-corrupted entry) reads back as the EMPTY fallback rather\n * than throwing — a bad settings row must never crash the boot restore.\n */\nfunction targetsState(store: AddonStoreLike) {\n return createDurableState<RecordingTargetsBlob>({\n key: RECORDING_TARGETS_KEY,\n schema: RecordingTargetsBlobSchema,\n fallback: {},\n read: () => store.readAddonStore(),\n write: (patch) => store.writeAddonStore(patch),\n })\n}\n\n/**\n * Read the persisted recording targets, keyed by numeric deviceId. A\n * malformed blob (e.g. a partially-corrupted entry) parses to an EMPTY map\n * rather than throwing — a bad settings row must never crash the boot restore.\n */\nexport async function readRecordingTargets(store: AddonStoreLike): Promise<Map<number, RecordingTarget>> {\n const blob = await targetsState(store).get()\n const map = new Map<number, RecordingTarget>()\n for (const [key, target] of Object.entries(blob)) {\n const id = Number(key)\n if (Number.isInteger(id)) map.set(id, target)\n }\n return map\n}\n\n/** Persist (insert or replace) one camera's recording target. */\nexport async function upsertRecordingTarget(\n store: AddonStoreLike,\n deviceId: number,\n target: RecordingTarget,\n): Promise<void> {\n const map = await readRecordingTargets(store)\n map.set(deviceId, target)\n await targetsState(store).set(serialize(map))\n}\n\n/** Drop one camera's recording target. No-op when absent. */\nexport async function removeRecordingTarget(store: AddonStoreLike, deviceId: number): Promise<void> {\n const map = await readRecordingTargets(store)\n if (!map.delete(deviceId)) return\n await targetsState(store).set(serialize(map))\n}\n\nfunction serialize(map: Map<number, RecordingTarget>): RecordingTargetsBlob {\n const out: RecordingTargetsBlob = {}\n for (const [id, target] of map) out[String(id)] = target\n return out\n}\n","/**\n * Per-device recording contribution for the `recording` cap: a single top-tab\n * section that mounts the `host/recording-panel` widget. The widget now owns\n * ALL recording settings (enable / profiles / segment / rules / retention) and\n * playback natively, reading/writing via the `recording.getDeviceConfig` /\n * `setDeviceConfig` cap methods — so no schema-driven settings fields remain here.\n */\nimport type { ConfigUISchema } from '@camstack/types'\n\nexport function buildRecordingDeviceSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'recording-playback',\n tab: 'recording',\n location: 'top-tab',\n title: 'Recording',\n order: 10,\n fields: [\n { type: 'widget' as const, key: '_recordingPanel', label: '', widgetId: 'host/recording-panel' },\n ],\n },\n ],\n }\n}\n","/**\n * Recording placement — pure resolution of WHERE a profile/track's segments\n * are written, across operator-assigned storage locations. No I/O.\n *\n * Placement is operator-driven with NO default/fallback: the operator MUST\n * assign one or more `StorageLocation` ids to each profile/track key\n * (`low`/`mid`/`high`/`scrub`). When a key has several locations, the recorder\n * STRIPES across them by free space (capacity spreading, not redundancy) —\n * {@link chooseWriteLocation} picks the least-full. An UNASSIGNED key resolves\n * to none: recording for it must FAIL EXPLICITLY (never silently write\n * elsewhere).\n */\n\nimport { z } from 'zod'\n\n/** Operator placement config: profile/track key → ordered StorageLocation ids. */\nexport interface RecordingPlacement {\n readonly assignments: Readonly<Record<string, readonly string[]>>\n}\n\n/**\n * Persisted shape of the operator placement, stored under the\n * `recordingPlacement` addon-store key. `assignments` is the only field today\n * and is `.optional()` so a legacy blob (`{}` or `{ assignments: {...} }`)\n * still loads. Routing load+save through one durable handle (the whole blob\n * round-trips on every save) means any future sibling field added here can\n * never be silently dropped on persist.\n */\nexport const RecordingPlacementBlobSchema = z.object({\n assignments: z.record(z.string(), z.array(z.string())).optional(),\n})\nexport type RecordingPlacementBlob = z.infer<typeof RecordingPlacementBlobSchema>\n\n/**\n * Default placement: high/mid go to the `recordings` family, low/scrub go to\n * the lower-bitrate `recordingsLow` family. Seeded at the declaration level so\n * both DEFAULT_CONFIG and tests can reference the same frozen value.\n */\nexport const DEFAULT_RECORDING_PLACEMENT: Readonly<Record<string, readonly string[]>> = {\n high: ['recordings:default'],\n mid: ['recordings:default'],\n low: ['recordingsLow:default'],\n scrub: ['recordingsLow:default'],\n}\n\n/** A location's currently-available free space (bytes); `null` = unknown. */\nexport interface LocationFreeSpace {\n readonly locationId: string\n readonly availableBytes: number | null\n}\n\n/**\n * The storage locations a profile/track writes to. Returns the explicit\n * assignment, or `[]` when unassigned — there is NO fallback; an empty result\n * means the caller must fail the recording for this profile explicitly.\n */\nexport function resolveAssignedLocations(\n placement: RecordingPlacement,\n key: string,\n): readonly string[] {\n return placement.assignments[key] ?? []\n}\n\n/**\n * Choose ONE location to write the next segment-run into: the candidate with\n * the most free space (least-full striping). Locations whose free space is\n * unknown (`null` / no entry) are deprioritized; if every candidate is unknown,\n * the first (assignment-order) wins. Returns `null` only when `locationIds` is\n * empty.\n */\nexport function chooseWriteLocation(\n locationIds: readonly string[],\n free: readonly LocationFreeSpace[],\n): string | null {\n let best: string | null = null\n let bestBytes = -Infinity\n for (const id of locationIds) {\n const entry = free.find((f) => f.locationId === id)\n const bytes = entry?.availableBytes ?? -1\n if (best === null || bytes > bestBytes) {\n best = id\n bestBytes = bytes\n }\n }\n return best\n}\n","export interface PassthroughArgs {\n rtspUrl: string\n outDir: string\n segmentSeconds: number\n}\n\n/** ffmpeg argv: pull RTSP (TCP), copy codec, fMP4 fragmented segments named by\n * epoch seconds FLAT under `outDir`, plus a rolling playlist. The recorder's\n * segment-watcher relocates each finalized segment into its UTC\n * `<Y>/<M>/<D>/<H>/<startMs>-<durMs>.m4s` bucket (matching `segmentRelPath`,\n * used by playback) + maintains the VOD playlist itself. */\nexport function buildPassthroughArgs(a: PassthroughArgs): string[] {\n return [\n '-rtsp_transport', 'tcp',\n '-i', a.rtspUrl,\n // Map ALL input streams (video + audio). Without this the segment muxer\n // selects nothing and ffmpeg aborts with \"Output file does not contain\n // any stream\".\n '-map', '0',\n // Copy the (already-fragmentable) video, but always re-encode audio to AAC.\n // The MP4 segment muxer rejects raw G.711 (pcm_mulaw/pcm_alaw, common on\n // Hikvision/ONVIF) with \"codec not currently supported in container\",\n // which made ffmpeg exit before writing a single frame → 0-byte segments,\n // no playlist, empty availability. The broker cannot report the source\n // audio codec reliably (it advertises AAC even for µ-law streams), so we\n // unconditionally transcode audio to AAC: cheap (low-bitrate mono/stereo)\n // and muxable across every vendor. An `-c:a` with no audio stream is a no-op.\n '-c:v', 'copy',\n '-c:a', 'aac',\n '-f', 'segment',\n '-segment_time', String(a.segmentSeconds),\n '-segment_format', 'mp4',\n '-segment_format_options', 'movflags=+frag_keyframe+empty_moov+default_base_moof',\n '-reset_timestamps', '1',\n '-strftime', '1',\n '-segment_list', `${a.outDir}/live.m3u8`,\n '-segment_list_type', 'm3u8',\n // FLAT epoch-named segments directly under `outDir`. We deliberately do\n // NOT let ffmpeg write the date buckets: its segment muxer cannot create\n // directories on this ffmpeg build (`-strftime_mkdir` is absent) and its\n // `-strftime` expands in LOCAL time, whereas the playback layout\n // (`segmentRelPath`) is UTC. The watcher relocates into the UTC bucket so\n // disk + playback URIs stay consistent.\n `${a.outDir}/%s.m4s`,\n ]\n}\n","import type { LogExtras } from '@camstack/types'\nimport { buildPassthroughArgs, type PassthroughArgs } from './ffmpeg-args.js'\n\ninterface ProcLike {\n kill: () => void\n on: (ev: string, cb: (...a: unknown[]) => void) => void\n /** Default child_process.spawn pipes stderr; we read it for diagnosability. */\n stderr?: { on: (ev: string, cb: (...a: unknown[]) => void) => void } | null\n}\ninterface Deps {\n spawn: (cmd: string, args: readonly string[]) => ProcLike\n logger: { warn: (m: string, extras?: LogExtras) => void; info: (m: string, extras?: LogExtras) => void }\n ffmpegPath?: string\n}\n\nconst MAX_RESTARTS = 10\n/** How many trailing ffmpeg stderr lines to keep for the give-up diagnostic. */\nconst STDERR_TAIL_LINES = 12\n\n/** Supervises one passthrough ffmpeg for a single (camera,profile). */\nexport class SegmentWriter {\n private proc: ProcLike | null = null\n private stopped = false\n private restarts = 0\n constructor(private readonly cfg: PassthroughArgs, private readonly deps: Deps) {}\n\n start(): void {\n if (this.stopped || this.proc) return\n const args = buildPassthroughArgs(this.cfg)\n const proc = this.deps.spawn(this.deps.ffmpegPath ?? 'ffmpeg', args)\n this.proc = proc\n\n // Keep a small ring of ffmpeg's stderr so a fatal exit reports WHY\n // (e.g. \"codec pcm_mulaw not supported in container\") instead of a bare\n // exit code. stderr is per-spawn, so this resets on every restart.\n const stderrTail: string[] = []\n proc.stderr?.on('data', (chunk: unknown) => {\n const text = chunk instanceof Buffer ? chunk.toString('utf8') : String(chunk)\n for (const line of text.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n stderrTail.push(trimmed)\n if (stderrTail.length > STDERR_TAIL_LINES) stderrTail.shift()\n }\n })\n\n proc.on('exit', (code) => {\n this.proc = null\n if (this.stopped) return\n if (this.restarts >= MAX_RESTARTS) {\n this.deps.logger.warn('SegmentWriter giving up after max restarts', {\n meta: { outDir: this.cfg.outDir, code, stderrTail: stderrTail.join(' | ') },\n })\n return\n }\n this.restarts++\n this.deps.logger.info('SegmentWriter restarting', { meta: { outDir: this.cfg.outDir, attempt: this.restarts } })\n this.start()\n })\n }\n\n stop(): void {\n this.stopped = true\n this.proc?.kill()\n this.proc = null\n }\n}\n","export interface VariantSegment { uri: string; startMs: number; durMs: number }\n\n/** A VOD variant playlist with an absolute wall-clock anchor per segment. */\nexport function buildVariantPlaylist(segments: readonly VariantSegment[]): string {\n const maxDur = segments.reduce((m, s) => Math.max(m, s.durMs), 0)\n const lines: string[] = [\n '#EXTM3U',\n '#EXT-X-VERSION:7',\n '#EXT-X-PLAYLIST-TYPE:VOD',\n `#EXT-X-TARGETDURATION:${Math.ceil(maxDur / 1000)}`,\n '#EXT-X-MEDIA-SEQUENCE:0',\n ]\n segments.forEach((s, i) => {\n // Each segment is muxed with `-reset_timestamps 1` (ffmpeg-args.ts), so its\n // PTS restarts at ~0. Concatenated without a marker, hls.js expects monotonic\n // timestamps and stalls at the first segment's end instead of advancing. A\n // discontinuity before every boundary (not the first segment) tells the player\n // each segment begins a fresh timeline, so VOD plays straight through.\n if (i > 0) lines.push('#EXT-X-DISCONTINUITY')\n lines.push(`#EXT-X-PROGRAM-DATE-TIME:${new Date(s.startMs).toISOString()}`)\n lines.push(`#EXTINF:${(s.durMs / 1000).toFixed(3)},`)\n lines.push(s.uri)\n })\n lines.push('#EXT-X-ENDLIST')\n return lines.join('\\n') + '\\n'\n}\n\nexport interface MasterVariant {\n profile: string\n bandwidth: number\n uri: string\n /** RFC 6381 codec string (`avc1.640033` / `hev1.1.6.L120.B0`). When present,\n * hls.js calls `MediaSource.isTypeSupported` on it and DROPS variants the\n * browser can't decode — so Chrome skips HEVC and picks an H.264 variant. */\n codecs?: string\n /** Video resolution `WxH` for the ABR ladder. */\n resolution?: { width: number; height: number }\n}\n\n/** A master (multi-variant) playlist enabling client ABR / profile selection. */\nexport function buildMasterPlaylist(variants: readonly MasterVariant[]): string {\n const lines: string[] = ['#EXTM3U', '#EXT-X-VERSION:7']\n for (const v of variants) {\n const attrs = [`BANDWIDTH=${v.bandwidth}`]\n if (v.resolution) attrs.push(`RESOLUTION=${v.resolution.width}x${v.resolution.height}`)\n if (v.codecs) attrs.push(`CODECS=\"${v.codecs}\"`)\n attrs.push(`NAME=\"${v.profile}\"`)\n lines.push(`#EXT-X-STREAM-INF:${attrs.join(',')}`)\n lines.push(v.uri)\n }\n return lines.join('\\n') + '\\n'\n}\n","import { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport type { LogExtras } from '@camstack/types'\nimport { hourBucketRelPath, type Subtree } from './time-path.js'\n\n/**\n * Poll-based tail of an ffmpeg `-segment_list` (`live.m3u8`) for one\n * (camera,profile) output dir.\n *\n * ffmpeg (see `buildPassthroughArgs`) writes finalized segments FLAT under\n * `<outDir>/<epochSec>.m4s` and appends each one to `<outDir>/live.m3u8` as\n * an `#EXTINF:<dur>,` line followed by the segment path line. We tail that\n * playlist every `intervalMs`, and for each NEW `(durSeconds, segPath)`:\n *\n * - derive `startMs = epochSec * 1000` from the filename,\n * `durMs = round(durSeconds * 1000)`,\n * - relocate the file into its UTC `<Y>/<M>/<D>/<H>/<startMs>-<durMs>.m4s`\n * bucket under `outDir` (mkdir -p the bucket first) so disk matches the\n * `segmentRelPath` layout playback serves,\n * - `fs.stat` the relocated file for `bytes`,\n * - invoke `onSegment(startMs, durMs, bytes)`.\n *\n * The watcher is intentionally simple and idempotent: it tracks how many\n * playlist entries it has already processed (`processed`) and only acts on\n * entries beyond that high-water mark. It lives in the addon (not the pure\n * `RecorderCore`) so the core stays I/O-free.\n */\n\ninterface SegmentWatcherLogger {\n warn: (m: string, extras?: LogExtras) => void\n debug?: (m: string, extras?: LogExtras) => void\n}\n\n/** Classify a finalized segment into its `continuous`/`events` subtree, given\n * its start + corrected duration. Supplied by the addon (delegates to\n * `RecorderCore.classifySubtree` with a node-local clock). */\nexport type ClassifySubtree = (startMs: number, durMs: number) => Subtree\n\nexport interface SegmentWatcherDeps {\n readonly outDir: string\n readonly intervalMs: number\n readonly classify: ClassifySubtree\n readonly onSegment: (startMs: number, durMs: number, bytes: number, subtree: Subtree) => void\n readonly logger: SegmentWatcherLogger\n /** Injectable clock for tests. */\n readonly now?: () => number\n}\n\nexport interface ParsedEntry {\n readonly durSeconds: number\n readonly segPath: string\n}\n\nconst EPOCH_NAME_RE = /^(\\d+)\\.m4s$/\n\n/** How many consecutive ticks to wait for a finalized segment's flat file to\n * land before giving up on it (a genuine phantom). At a 2s watch interval this\n * is ~16s — far longer than ffmpeg's slow-first-segment flush. */\nconst MAX_PENDING_TICKS = 8\n\n/**\n * Derive `startMs` (epoch milliseconds) from a flat ffmpeg segment path\n * (`<epochSec>.m4s`, possibly absolute). Returns null for an already-relocated\n * `<startMs>-<durMs>.m4s` name or any non-segment path.\n */\nexport function parseEpochStartMs(segPath: string): number | null {\n const m = EPOCH_NAME_RE.exec(path.basename(segPath))\n if (!m) return null\n const epochSec = Number(m[1])\n return Number.isFinite(epochSec) ? epochSec * 1000 : null\n}\n\n/**\n * Correct a finalized segment's duration. ffmpeg's first-segment `#EXTINF` is\n * unreliable under `-c copy` from RTSP — it is inflated by the initial PTS\n * offset (observed: 87.961s for a segment whose successor starts only 12s\n * later). A segment physically cannot outlast the start of the next segment, so\n * when a successor exists we clamp the rounded EXTINF to the inter-segment gap.\n * For normal segments EXTINF (millisecond-accurate) is below the\n * integer-second start gap and wins, preserving sub-second precision. When\n * there is no successor (genuine last segment) or the gap is non-positive\n * (restart / same-second), we trust EXTINF.\n */\nexport function correctedDurMs(durSeconds: number, startMs: number, nextStartMs: number | null): number {\n const extinfMs = Math.round(durSeconds * 1000)\n if (nextStartMs === null) return extinfMs\n const gapMs = nextStartMs - startMs\n if (gapMs <= 0) return extinfMs\n return Math.min(extinfMs, gapMs)\n}\n\n/** Parse a `live.m3u8` body into ordered `#EXTINF` → segment-path pairs. */\nexport function parseLivePlaylist(body: string): ParsedEntry[] {\n const lines = body.split('\\n')\n const out: ParsedEntry[] = []\n let pendingDur: number | null = null\n for (const raw of lines) {\n const line = raw.trim()\n if (line.length === 0) continue\n if (line.startsWith('#EXTINF:')) {\n // `#EXTINF:4.000,` → 4.000\n const v = line.slice('#EXTINF:'.length).replace(/,.*$/, '')\n const dur = Number(v)\n pendingDur = Number.isFinite(dur) ? dur : null\n continue\n }\n if (line.startsWith('#')) continue\n if (pendingDur !== null) {\n out.push({ durSeconds: pendingDur, segPath: line })\n pendingDur = null\n }\n }\n return out\n}\n\n/**\n * Derive `(startMs, durMs)` and the RELOCATED path for one finalized segment\n * whose ffmpeg filename is epoch-seconds (`<epochSec>.m4s`). `durMs` is the\n * already-corrected duration (see `correctedDurMs`), NOT the raw EXTINF. The\n * new path is the UTC `<Y>/<M>/<D>/<H>/<startMs>-<durMs>.m4s` bucket under\n * `outDir` (matching `segmentRelPath`, which playback serves). Returns null\n * when the filename is not the expected strftime shape (e.g. already relocated\n * by a previous tick).\n */\nexport function resolveFinalizedSegment(\n outDir: string,\n segPath: string,\n durMs: number,\n subtree: Subtree,\n): { startMs: number; durMs: number; oldAbs: string; newAbs: string } | null {\n // The playlist path may be relative (to outDir) or absolute depending on\n // ffmpeg invocation; normalize against outDir.\n const abs = path.isAbsolute(segPath) ? segPath : path.join(outDir, segPath)\n const startMs = parseEpochStartMs(abs)\n if (startMs === null) return null\n // The subtree (continuous/events) sits between outDir (=…/<dev>/<profile>) and\n // the UTC bucket, matching `segmentRelPath`.\n const newAbs = path.join(outDir, subtree, hourBucketRelPath(startMs), `${startMs}-${durMs}.m4s`)\n return { startMs, durMs, oldAbs: abs, newAbs }\n}\n\n/** `fs.stat` size, or null when the path does not exist. */\nasync function statSize(p: string): Promise<number | null> {\n try {\n return (await fs.stat(p)).size\n } catch {\n return null\n }\n}\n\n/**\n * Process ONE finalized playlist entry: relocate the flat segment into its UTC\n * bucket, stat for bytes, and call `onSegment` (which indexes it). Returns:\n * - `'retained'` — relocated (or already relocated) and indexed;\n * - `'skipped'` — not a relocatable segment name (caller advances past it);\n * - `'pending'` — a valid segment whose flat file has NOT landed on disk\n * yet. ffmpeg's first segment is slow to flush, so the caller must RETRY\n * this entry on a later tick instead of orphaning it.\n *\n * Every finalized segment is indexed unconditionally; the modes-engine\n * keep/discard happens AFTER, in `RecorderCore.sweepPolicy` (so `preBufferSec`\n * can retroactively retain segments recorded before a trigger).\n */\nexport async function handleSegmentEntry(\n outDir: string,\n entry: ParsedEntry,\n durMs: number,\n classify: ClassifySubtree,\n onSegment: (startMs: number, durMs: number, bytes: number, subtree: Subtree) => void,\n logger: SegmentWatcherLogger,\n): Promise<'retained' | 'skipped' | 'pending'> {\n const startMs = parseEpochStartMs(path.isAbsolute(entry.segPath) ? entry.segPath : path.join(outDir, entry.segPath))\n if (startMs === null) return 'skipped'\n const subtree = classify(startMs, durMs)\n const resolved = resolveFinalizedSegment(outDir, entry.segPath, durMs, subtree)\n if (!resolved) return 'skipped'\n\n // Already relocated on a previous tick (restart replay) — re-index idempotently.\n const existing = await statSize(resolved.newAbs)\n if (existing !== null) {\n onSegment(resolved.startMs, resolved.durMs, existing, subtree)\n return 'retained'\n }\n\n // The flat source must be on disk to relocate. A finalized entry whose file\n // hasn't been flushed yet is PENDING, not lost — the caller retries.\n if ((await statSize(resolved.oldAbs)) === null) {\n logger.debug?.('segment flat file not on disk yet — will retry', { meta: { path: resolved.oldAbs } })\n return 'pending'\n }\n\n try {\n // ffmpeg writes segments FLAT; relocate into the subtree's UTC date bucket\n // (ffmpeg cannot mkdir on this build). mkdir -p is idempotent.\n await fs.mkdir(path.dirname(resolved.newAbs), { recursive: true })\n await fs.rename(resolved.oldAbs, resolved.newAbs)\n } catch {\n // A racing tick may have moved it; re-check the destination before retrying.\n logger.debug?.('segment relocate failed', { meta: { from: resolved.oldAbs, to: resolved.newAbs } })\n }\n const bytes = await statSize(resolved.newAbs)\n if (bytes === null) return 'pending'\n onSegment(resolved.startMs, resolved.durMs, bytes, subtree)\n return 'retained'\n}\n\nexport interface PlannedSegment {\n readonly entry: ParsedEntry\n /** Duration corrected against the successor's start (see `correctedDurMs`). */\n readonly durMs: number\n}\n\nexport interface FinalizationPlan {\n readonly toHandle: ReadonlyArray<PlannedSegment>\n /** First entry index covered by `toHandle` (the effective high-water mark\n * after any truncation reset). The caller advances from here. */\n readonly from: number\n /** New high-water mark if every entry in `toHandle` is handled. */\n readonly processed: number\n}\n\n/**\n * Decide which playlist entries are FINALIZABLE this tick and their corrected\n * durations, given the previously-processed high-water mark. Pure (no I/O) so\n * the tick loop is just: read file → plan → relocate each → advance mark.\n *\n * The live tail entry is held back while recording continues: a segment's true\n * duration needs its SUCCESSOR's start (ffmpeg's first-segment EXTINF is\n * inflated by the initial PTS offset). Once `#EXT-X-ENDLIST` appears the tail\n * is flushed too — by then it is never the unreliable first segment.\n */\nexport function planFinalizations(body: string, processed: number): FinalizationPlan {\n const entries = parseLivePlaylist(body)\n const endlist = body.includes('#EXT-X-ENDLIST')\n // A truncated/rotated playlist (entries shrank below the mark) means ffmpeg\n // restarted its list — replay from the top.\n const hw = entries.length < processed ? 0 : processed\n const finalizable = endlist ? entries.length : Math.max(0, entries.length - 1)\n const toHandle: PlannedSegment[] = []\n for (let i = hw; i < finalizable; i++) {\n const entry = entries[i]!\n const startMs = parseEpochStartMs(entry.segPath)\n const nextStartMs = i + 1 < entries.length ? parseEpochStartMs(entries[i + 1]!.segPath) : null\n const durMs = startMs === null\n ? Math.round(entry.durSeconds * 1000)\n : correctedDurMs(entry.durSeconds, startMs, nextStartMs)\n toHandle.push({ entry, durMs })\n }\n return { toHandle, from: hw, processed: Math.max(hw, finalizable) }\n}\n\nexport interface SegmentWatcherHandle {\n stop(): void\n}\n\n/**\n * Start polling `<outDir>/live.m3u8`. Returns a handle whose `stop()`\n * cancels the timer. Errors during a tick are swallowed (logged at debug)\n * — a transient read race self-corrects on the next interval.\n */\nexport function startSegmentWatcher(deps: SegmentWatcherDeps): SegmentWatcherHandle {\n const playlistPath = path.join(deps.outDir, 'live.m3u8')\n let processed = 0\n let stopped = false\n let ticking = false\n // A finalized entry whose flat file hasn't landed yet blocks the high-water\n // mark so it is retried, not orphaned. Bounded so a genuine phantom (an entry\n // whose file never lands) can't stall the whole stream forever.\n let pendingIndex = -1\n let pendingTicks = 0\n\n const tick = async (): Promise<void> => {\n if (stopped || ticking) return\n ticking = true\n try {\n let body: string\n try {\n body = await fs.readFile(playlistPath, 'utf8')\n } catch {\n // Playlist not created yet (ffmpeg still warming up) — skip.\n return\n }\n const plan = planFinalizations(body, processed)\n let index = plan.from\n for (const { entry, durMs } of plan.toHandle) {\n const status = await handleSegmentEntry(deps.outDir, entry, durMs, deps.classify, deps.onSegment, deps.logger)\n if (status === 'pending') {\n if (pendingIndex === index) {\n pendingTicks++\n if (pendingTicks >= MAX_PENDING_TICKS) {\n // Give up on this entry's file (never landed) and advance past it.\n deps.logger.warn('segment file never landed — skipping', { meta: { seg: entry.segPath } })\n pendingIndex = -1\n pendingTicks = 0\n index++\n continue\n }\n } else {\n pendingIndex = index\n pendingTicks = 1\n }\n break // retry this index on the next tick; hold the high-water mark here\n }\n index++\n }\n if (pendingIndex !== index) {\n pendingIndex = -1\n pendingTicks = 0\n }\n processed = index\n } finally {\n ticking = false\n }\n }\n\n const timer = setInterval(() => { void tick() }, deps.intervalMs)\n timer.unref?.()\n\n return {\n stop(): void {\n stopped = true\n clearInterval(timer)\n },\n }\n}\n","/**\n * Shallow disk scan of the recordings tree → `ScanEntry[]`, the input to\n * `RamIndex.fromScan`. Lets the recorder serve playback / availability for\n * PAST recordings — cameras that aren't currently attached, or recordings made\n * before the last restart — by rebuilding an in-RAM index from the filesystem\n * (spec §4: boot scan + periodic rescan).\n *\n * By default bytes are left 0: the scan is name-only (cheap, no per-file\n * stat), which is enough for availability + playlist building. Pass\n * `{ withSizes: true }` to stat every segment and populate real byte counts —\n * used by size-based retention enforcement (spec §5).\n */\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { parseSegmentRelPath } from './time-path.js'\nimport type { ScanEntry } from './ram-index.js'\n\nexport interface ScanRecordingsOpts {\n locationId?: string\n withSizes?: boolean\n}\n\n/**\n * Walk `rootDir` (one storage location's recordings root) and return one\n * {@link ScanEntry} per segment file\n * (`<deviceId>/<profile>/<subtree>/<Y>/<M>/<D>/<H>/<startMs>-<durMs>.m4s`).\n * Non-segment files (playlists, stray names) are ignored. A missing root →\n * [].\n *\n * `opts.locationId` stamps each entry with the location it was scanned from\n * so playback resolves the right root (absent for legacy single-root scans).\n *\n * `opts.withSizes` stat-s each segment file and sets real `bytes`; on stat\n * error the entry is skipped (continue). Defaults to name-only (`bytes: 0`).\n */\nexport async function scanRecordings(rootDir: string, opts?: ScanRecordingsOpts): Promise<ScanEntry[]> {\n const locationId = opts?.locationId\n const withSizes = opts?.withSizes ?? false\n\n let names: string[]\n try {\n names = await fs.readdir(rootDir, { recursive: true })\n } catch {\n return []\n }\n const entries: ScanEntry[] = []\n for (const name of names) {\n const rel = name.split(path.sep).join('/')\n if (!parseSegmentRelPath(rel)) continue\n if (withSizes) {\n let st: Awaited<ReturnType<typeof fs.stat>>\n try {\n st = await fs.stat(path.join(rootDir, name))\n } catch {\n continue\n }\n entries.push({ relPath: rel, bytes: st.size, locationId })\n } else {\n entries.push({ relPath: rel, bytes: 0, locationId })\n }\n }\n return entries\n}\n","/**\n * Stat-based sizing of a recordings tree — the accurate per-camera byte counts\n * the storage-usage report needs. Unlike `disk-scan.ts` (name-only, for the\n * playback index) this `fs.stat`s every segment file, so it is used ONLY\n * on-demand for `getStorageUsage`, never on the periodic rescan path.\n */\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { parseSegmentRelPath } from './time-path.js'\n\nexport interface RootUsage {\n /** deviceId (camera key) → total bytes of its segments under this root. */\n readonly perDevice: ReadonlyMap<string, number>\n /** Sum of every segment's bytes under this root. */\n readonly totalBytes: number\n}\n\n/**\n * Walk `rootDir` (one storage location's recordings root) and sum each segment\n * file's size, grouped by camera (deviceId). Non-segment files are ignored; a\n * missing root → empty. Best-effort: a file that vanishes mid-walk is skipped.\n */\nexport async function sizeRecordingsTree(rootDir: string): Promise<RootUsage> {\n let names: string[]\n try {\n names = await fs.readdir(rootDir, { recursive: true })\n } catch {\n return { perDevice: new Map(), totalBytes: 0 }\n }\n const perDevice = new Map<string, number>()\n let totalBytes = 0\n for (const name of names) {\n const rel = name.split(path.sep).join('/')\n const parsed = parseSegmentRelPath(rel)\n if (!parsed) continue\n let size: number\n try {\n size = (await fs.stat(path.join(rootDir, name))).size\n } catch {\n continue\n }\n perDevice.set(parsed.camera, (perDevice.get(parsed.camera) ?? 0) + size)\n totalBytes += size\n }\n return { perDevice, totalBytes }\n}\n\n/** Merge several {@link RootUsage} into one combined per-device map + total. */\nexport function mergeRootUsage(usages: readonly RootUsage[]): RootUsage {\n const perDevice = new Map<string, number>()\n let totalBytes = 0\n for (const u of usages) {\n for (const [device, bytes] of u.perDevice) {\n perDevice.set(device, (perDevice.get(device) ?? 0) + bytes)\n }\n totalBytes += u.totalBytes\n }\n return { perDevice, totalBytes }\n}\n","/**\n * In-process video codec probe — walks an fMP4 segment's box tree to the video\n * sample entry and returns its RFC 6381 codec string + resolution, for the HLS\n * master playlist's `CODECS` / `RESOLUTION` attributes. No ffprobe spawn.\n *\n * Why it matters: without `CODECS`, hls.js can't tell that a Reolink `high`\n * variant is HEVC (which Chrome's MSE cannot decode) and may select it →\n * \"freeze on first frame\". `MediaSource.isTypeSupported(codecs)` lets hls.js\n * skip the HEVC variant in Chrome and pick the H.264 mid/low one (Safari, which\n * decodes HEVC natively, keeps all variants). hls.js builds the actual\n * SourceBuffer from the parsed fragment, so the playlist string only needs to\n * be honest about H.264-vs-HEVC — exact for H.264 (from `avcC`), coarse but\n * Chrome-rejected for HEVC.\n */\n\n/** Boxes that contain child boxes we descend into on the way to `stsd`. */\nconst CONTAINER_BOXES = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl'])\n/** Sample-entry coding names we treat as video. */\nconst H264_CODINGS = new Set(['avc1', 'avc3'])\nconst HEVC_CODINGS = new Set(['hvc1', 'hev1'])\n\nexport interface VideoCodecInfo {\n /** RFC 6381 codec string, e.g. `avc1.640029` / `hvc1.1.6.L120.B0`. */\n readonly codec: string\n readonly width: number\n readonly height: number\n}\n\n/** Read a 32-bit big-endian length-prefixed box header at `o`: `{ size, type }`. */\nfunction readBoxHeader(buf: Buffer, o: number): { size: number; type: string; headerSize: number } | null {\n if (o + 8 > buf.length) return null\n let size = buf.readUInt32BE(o)\n let headerSize = 8\n if (size === 1) {\n if (o + 16 > buf.length) return null\n size = Number(buf.readBigUInt64BE(o + 8))\n headerSize = 16\n }\n const type = buf.toString('latin1', o + 4, o + 8)\n if (size < headerSize) return null\n return { size, type, headerSize }\n}\n\n/** Find a direct child box by type within `[start,end)`; returns its content range. */\nfunction findChild(buf: Buffer, start: number, end: number, wanted: string): { contentStart: number; end: number } | null {\n let o = start\n while (o + 8 <= end) {\n const h = readBoxHeader(buf, o)\n if (!h) return null\n if (h.type === wanted) return { contentStart: o + h.headerSize, end: Math.min(o + h.size, end) }\n o += h.size\n }\n return null\n}\n\n/** Build the `avc1.PPCCLL` string from an `avcC` box's first config bytes. */\nfunction avcCodecString(buf: Buffer, avcCContentStart: number): string {\n // avcC: configurationVersion[1], AVCProfileIndication[1], profile_compat[1], AVCLevelIndication[1], …\n const profile = buf.readUInt8(avcCContentStart + 1)\n const compat = buf.readUInt8(avcCContentStart + 2)\n const level = buf.readUInt8(avcCContentStart + 3)\n const hex = (n: number): string => n.toString(16).padStart(2, '0')\n return `avc1.${hex(profile)}${hex(compat)}${hex(level)}`\n}\n\n/**\n * Extract the video codec + resolution from a complete fMP4 segment (its `moov`\n * carries the sample description). Returns null when no video sample entry is\n * found (audio-only / malformed). Best-effort and defensive against truncation.\n */\nexport function extractVideoCodecInfo(buf: Buffer): VideoCodecInfo | null {\n let result: VideoCodecInfo | null = null\n\n const walk = (start: number, end: number): void => {\n let o = start\n while (o + 8 <= end && !result) {\n const h = readBoxHeader(buf, o)\n if (!h) return\n const boxEnd = Math.min(o + h.size, end)\n if (CONTAINER_BOXES.has(h.type)) {\n walk(o + h.headerSize, boxEnd)\n } else if (h.type === 'stsd') {\n // stsd content: 4 (version+flags) + 4 (entry_count) then sample entries.\n let p = o + h.headerSize + 8\n while (p + 8 <= boxEnd && !result) {\n const e = readBoxHeader(buf, p)\n if (!e) break\n const isH264 = H264_CODINGS.has(e.type)\n const isHevc = HEVC_CODINGS.has(e.type)\n if (isH264 || isHevc) {\n // VisualSampleEntry: 8 box header + 78 header before child boxes;\n // width@+32, height@+34 from the sample-entry box start.\n const width = p + 34 <= boxEnd ? buf.readUInt16BE(p + 32) : 0\n const height = p + 36 <= boxEnd ? buf.readUInt16BE(p + 34) : 0\n let codec: string\n if (isH264) {\n const avcC = findChild(buf, p + 8 + 78, Math.min(p + e.size, boxEnd), 'avcC')\n codec = avcC ? avcCodecString(buf, avcC.contentStart) : 'avc1.640029'\n } else {\n // HEVC: a coarse, valid-shaped string — Chrome rejects all HEVC\n // here, Safari accepts; the exact level is immaterial for that.\n codec = `${e.type}.1.6.L120.B0`\n }\n result = { codec, width, height }\n }\n p += e.size\n }\n }\n o += h.size\n }\n }\n\n walk(0, buf.length)\n return result\n}\n","/**\n * Custom actions exposed by the recorder addon.\n *\n * P1 has no orchestrator, so `attachCamera`/`detachCamera` are plain\n * admin-callable actions (driven by the Task-8 live verification tRPC\n * call). A later phase wires the orchestrator to call them on camera\n * assignment, mirroring how `pipeline-runner` exposes `attachCamera`.\n */\nimport { z } from 'zod'\nimport { customAction, defineCustomActions, CamProfileSchema, RecordingRetentionSchema } from '@camstack/types'\nimport type { CamProfile, RecordingRetention } from '@camstack/types'\nimport { RecordingRuleSchema, type RecordingRule } from './recording-policy.js'\n\ninterface AttachCameraInputShape {\n readonly deviceId: number\n /** Broker profile slots to record (e.g. `['high']`). Omit to record every\n * assigned source. */\n readonly profiles?: readonly CamProfile[]\n readonly segmentSeconds?: number\n /** Policy rules; omit/empty = continuous (always record). */\n readonly rules?: readonly RecordingRule[]\n readonly retention?: RecordingRetention\n}\ninterface AttachCameraResultShape { readonly success: true }\ninterface DetachCameraInputShape { readonly deviceId: number }\ninterface DetachCameraResultShape { readonly success: true }\n\nexport const AttachCameraInputSchema: z.ZodType<AttachCameraInputShape> = z.object({\n deviceId: z.number().int().nonnegative(),\n profiles: z.array(CamProfileSchema).optional(),\n segmentSeconds: z.number().int().positive().optional(),\n rules: z.array(RecordingRuleSchema).optional(),\n retention: RecordingRetentionSchema.optional(),\n})\nexport const AttachCameraResultSchema: z.ZodType<AttachCameraResultShape> = z.object({\n success: z.literal(true),\n})\nexport const DetachCameraInputSchema: z.ZodType<DetachCameraInputShape> = z.object({\n deviceId: z.number().int().nonnegative(),\n})\nexport const DetachCameraResultSchema: z.ZodType<DetachCameraResultShape> = z.object({\n success: z.literal(true),\n})\n\ninterface SetPlacementInputShape {\n /** profile/track key → ordered storage-location ids. Unassigned = no\n * recording for that profile (no fallback). */\n readonly assignments: Readonly<Record<string, readonly string[]>>\n}\ninterface PlacementResultShape { readonly success: true }\n\nexport const SetPlacementInputSchema: z.ZodType<SetPlacementInputShape> = z.object({\n assignments: z.record(z.string(), z.array(z.string())),\n})\nexport const SetPlacementResultSchema: z.ZodType<PlacementResultShape> = z.object({\n success: z.literal(true),\n})\n\nexport const recorderActions = defineCustomActions({\n attachCamera: customAction(AttachCameraInputSchema, AttachCameraResultSchema, { kind: 'mutation' }),\n detachCamera: customAction(DetachCameraInputSchema, DetachCameraResultSchema, { kind: 'mutation' }),\n setPlacement: customAction(SetPlacementInputSchema, SetPlacementResultSchema, { kind: 'mutation' }),\n})\n\nexport type RecorderActions = typeof recorderActions\n\nexport interface AttachCameraInput {\n readonly deviceId: number\n readonly profiles?: readonly CamProfile[]\n readonly segmentSeconds?: number\n readonly rules?: readonly RecordingRule[]\n readonly retention?: RecordingRetention\n}\nexport interface DetachCameraInput { readonly deviceId: number }\nexport interface SetPlacementInput {\n readonly assignments: Readonly<Record<string, readonly string[]>>\n}\n","import type { RecordingConfig } from '@camstack/types'\nimport type { RecordingTarget } from './recording-targets.js'\n\n/** A persisted target → the wire config (disabled default when absent). */\nexport function configFromTarget(target: RecordingTarget | undefined): RecordingConfig {\n if (!target) return { enabled: false, rules: [] }\n return {\n enabled: target.enabled,\n profiles: target.profiles,\n segmentSeconds: target.segmentSeconds,\n rules: target.rules ?? [],\n retention: target.retention,\n }\n}\n\n/** A wire config → a persisted target (identity today; isolates the boundary). */\nexport function targetFromConfig(config: RecordingConfig): RecordingTarget {\n return {\n enabled: config.enabled,\n profiles: config.profiles,\n segmentSeconds: config.segmentSeconds,\n rules: config.rules,\n retention: config.retention,\n }\n}\n","/**\n * Pure helper for the `getStatus` provider's stat-based storage fallback.\n *\n * When the RAM index reports `storageBytes === 0` (e.g. fresh restart before\n * the seeding scan completes, or a detached-then-reattached camera whose index\n * was cleared) but on-disk footage exists, returning 0 would be misleading.\n * This helper implements the resolution rule:\n *\n * • index has bytes → trust the index (fast path, no stat needed)\n * • index is 0 AND ranges exist (footage confirmed) → use stat-based size\n * • index is 0 AND no ranges → return 0 (truly empty)\n */\n\n/** Input for {@link resolveStorageBytes}. */\nexport interface ResolveStorageBytesInput {\n /** Bytes reported by the live RAM index (may be 0 right after restart). */\n readonly indexBytes: number\n /** Whether the availability index shows at least one range for the device. */\n readonly hasRanges: boolean\n /** Stat-based byte total for the device's on-disk tree (lazy — only called\n * when `indexBytes === 0 && hasRanges`). */\n readonly statSize: number\n}\n\n/**\n * Return the best storage byte estimate for a device's `getStatus` response.\n *\n * - `indexBytes > 0` → return `indexBytes` (RAM index is the authoritative,\n * O(1) answer; no disk I/O needed).\n * - `indexBytes === 0 && hasRanges` → return `statSize` (footage exists but\n * the index hasn't been seeded yet; fall back to the disk stat).\n * - `indexBytes === 0 && !hasRanges` → return `0` (device genuinely has no\n * footage recorded yet).\n */\nexport function resolveStorageBytes(input: ResolveStorageBytesInput): number {\n const { indexBytes, hasRanges, statSize } = input\n if (indexBytes > 0) return indexBytes\n if (hasRanges) return statSize\n return 0\n}\n","/**\n * recorder — continuous, local, filesystem-native camera recording (P1).\n *\n * On `attachCamera({ deviceId, profiles })` the addon resolves the broker\n * RTSP restream URL per profile and spawns one passthrough ffmpeg\n * (`-c copy -f segment`) per profile, writing fMP4 fragments into a\n * time-bucketed tree under `${ctx.dataDir}/recordings/<deviceId>/<profile>/\n * <Y>/<M>/<D>/<H>/<startMs>-<durMs>.m4s`. A poll-based watcher tails each\n * profile's `live.m3u8`, renames each finalized strftime segment to the\n * `<startMs>-<durMs>` convention, and feeds it into the per-camera RAM\n * index. The `recording` cap (system-scoped) answers\n * `getStatus`/`getAvailability`/`getPlaybackManifest` from that index.\n *\n * Pure logic (time paths, playlists, the RAM index, the ffmpeg-arg\n * builder, the segment-writer supervisor, and the transport-free\n * `RecorderCore`) lives in sibling modules and is unit-tested in\n * isolation. This file is the thin integration glue that binds `ctx`.\n *\n * Out of P1 scope (later phases): scrub track, policy/modes/pre-buffer/\n * retention, JWT + node endpoints, orchestrator-driven assignment, admin\n * UI. P1 runs on the hub, so `127.0.0.1` restream URLs are correct.\n */\nimport { spawn as childProcessSpawn } from 'node:child_process'\nimport path from 'node:path'\nimport { promises as fs, mkdirSync } from 'node:fs'\nimport {\n BaseAddon,\n recordingCapability,\n storageEvictableCapability,\n type IStorageEvictableProvider,\n errMsg,\n selectAssignedProfileSlots,\n makeProfileBrokerId,\n EventCategory,\n hydrateSchema,\n type ConfigUISchemaWithValues,\n type AddonInitResult,\n type RecordingStatus,\n type RecordingAvailability,\n type RecordingManifest,\n type RecordingStorageUsage,\n type InferProvider,\n type CamProfile,\n type DurableState,\n} from '@camstack/types'\nimport { RecorderCore, type SeedSegment } from './recorder-core.js'\nimport { pruneDeviceWithEvents } from './retention-orchestration.js'\nimport type { RetentionPolicy } from './retention.js'\nimport { resolveDeviceRetention } from './retention-resolve.js'\nimport type { ScheduleClock } from './recording-policy.js'\nimport {\n readRecordingTargets,\n upsertRecordingTarget,\n removeRecordingTarget,\n type RecordingTarget,\n} from './recording-targets.js'\nimport { buildRecordingDeviceSchema } from './recording-device-settings.js'\nimport {\n resolveAssignedLocations,\n chooseWriteLocation,\n DEFAULT_RECORDING_PLACEMENT,\n RecordingPlacementBlobSchema,\n type RecordingPlacement,\n type RecordingPlacementBlob,\n} from './recording-placement.js'\nimport { SegmentWriter } from './segment-writer.js'\nimport { segmentRelPath, parseSegmentRelPath, type Subtree } from './time-path.js'\nimport {\n buildVariantPlaylist,\n buildMasterPlaylist,\n type VariantSegment,\n type MasterVariant,\n} from './playlist.js'\nimport { startSegmentWatcher, type SegmentWatcherHandle } from './segment-watcher.js'\nimport { createFileDataPlaneHandler } from '@camstack/core'\nimport type { AddonDataPlaneHandle } from '@camstack/types'\nimport { scanRecordings } from './disk-scan.js'\nimport { sizeRecordingsTree, mergeRootUsage, type RootUsage } from './disk-usage.js'\nimport { extractVideoCodecInfo } from './probe-codec.js'\nimport { recorderActions, type RecorderActions, type AttachCameraInput, type DetachCameraInput, type SetPlacementInput } from './recorder-actions.js'\nimport { configFromTarget, targetFromConfig } from './recording-device-config.js'\nimport { resolveStorageBytes } from './resolve-storage-bytes.js'\nimport type { RecordingConfig } from '@camstack/types'\n\n/** How long (ms) a stat-based storage size for a device is considered fresh.\n * The stat is only re-run when the RAM index reports 0 AND footage exists, so\n * this TTL only matters for that corner case. 30 s keeps the value reasonable\n * without re-stat-ing on every poll (which could be <1 s). */\nconst STAT_CACHE_TTL_MS = 30_000\n\ninterface RecorderAddonConfig {\n /** Segment length in seconds. */\n readonly segmentSeconds: number\n /** live.m3u8 tail interval in ms. */\n readonly watchIntervalMs: number\n /** Prune segments older than this many days. `0` = age pruning disabled.\n * Default for both subtrees unless a per-subtree override below is set. */\n readonly retentionMaxAgeDays: number\n /** Prune oldest segments once a camera exceeds this many GB. `0` = size pruning\n * disabled. Default for both subtrees unless a per-subtree override is set. */\n readonly retentionMaxSizeGb: number\n /**\n * Per-subtree retention overrides (continuous vs event footage retained\n * independently — spec §6). `0` = inherit the global default above; a value\n * `> 0` overrides it for that subtree. Event footage is usually kept longer.\n */\n readonly continuousRetentionMaxAgeDays: number\n readonly continuousRetentionMaxSizeGb: number\n readonly eventRetentionMaxAgeDays: number\n readonly eventRetentionMaxSizeGb: number\n /** How often the retention sweep runs, in ms. */\n readonly retentionSweepIntervalMs: number\n /** How often the modes-engine policy reaper runs, in ms (≪ pre-buffer). */\n readonly policySweepIntervalMs: number\n /** How often the playback index is rebuilt from a disk scan, in ms. */\n readonly playbackRescanIntervalMs: number\n /**\n * Operator placement: profile/track key → ordered storage-location ids.\n * Multiple ids → stripe by free space (least-full wins). NO fallback — an\n * unassigned profile fails recording explicitly (resolveProfilePlacements\n * throws). Empty by default → a fresh install records nothing until the\n * operator assigns locations.\n */\n readonly recordingPlacement?: {\n readonly assignments?: Readonly<Record<string, readonly string[]>>\n }\n}\n\nconst DEFAULT_CONFIG: RecorderAddonConfig = {\n segmentSeconds: 4,\n watchIntervalMs: 2_000,\n // Sane default so an attached camera can't fill the disk unbounded; the\n // operator raises/lowers it via global addon settings (and, in P6, per\n // device). Age-only by default — size cap is opt-in.\n retentionMaxAgeDays: 14,\n retentionMaxSizeGb: 0,\n // 0 = inherit the global default; operator raises the event window for longer\n // motion/audio-clip retention. Present in defaults so resolveConfig reads them.\n continuousRetentionMaxAgeDays: 0,\n continuousRetentionMaxSizeGb: 0,\n eventRetentionMaxAgeDays: 0,\n eventRetentionMaxSizeGb: 0,\n retentionSweepIntervalMs: 15 * 60_000,\n policySweepIntervalMs: 5_000,\n playbackRescanIntervalMs: 5 * 60_000,\n // Present in defaults so `BaseAddon.resolveConfig` reads it from the store\n // (it only merges keys that exist in defaults). Seeded to the standard\n // placement so a fresh install routes high/mid → recordings, low/scrub →\n // recordingsLow automatically, without operator intervention.\n recordingPlacement: { assignments: DEFAULT_RECORDING_PLACEMENT },\n}\n\n\n/** Bandwidth hint per profile for the master playlist (real bitrate is a P2 refinement). */\nconst PROFILE_BANDWIDTH: Record<string, number> = {\n high: 4_000_000,\n mid: 1_500_000,\n low: 500_000,\n}\nconst DEFAULT_BANDWIDTH = 2_000_000\n\ntype RecordingProvider = InferProvider<typeof recordingCapability>\n\nexport default class RecorderAddon extends BaseAddon<RecorderAddonConfig> {\n private core: RecorderCore | null = null\n private nodeId = 'hub'\n private dataDir = ''\n /** Storage location recordings are written to (the managed `recordings`\n * location). `undefined` = legacy addon-local fallback (storage cap down). */\n private recordingLocationId: string | undefined = undefined\n /** Resolved placement: profile/track → location ids. NO fallback — an\n * unassigned profile fails to record, explicitly. */\n private placement: RecordingPlacement = { assignments: {} }\n /** Durable handle over the `recordingPlacement` addon-store key. Both the\n * boot load and `setPlacement` route through it so the WHOLE blob round-trips\n * (no hand-listed field can be dropped on save). Built in `onInitialize`. */\n private placementState: DurableState<RecordingPlacementBlob> | null = null\n /** Cache of locationId → absolute root path (via `storage.resolve`). */\n private readonly locationRootCache = new Map<string, string>()\n /** Cache of `${deviceId}/${profile}` → master-playlist codec/resolution,\n * probed once from a segment's moov (codec is stable per camera profile). */\n private readonly profileCodecCache = new Map<string, { codecs: string; resolution: { width: number; height: number } }>()\n /** GLOBAL write location chosen per profile/track. Cached so EVERY camera\n * records a given profile to the SAME location (assignment is global, not\n * per-camera). Reset on addon restart (re-config). */\n private readonly profileWriteLocation = new Map<string, { locationId: string; root: string }>()\n /** One watcher per (deviceId, profile). Keyed `${deviceId}/${profile}`. */\n private readonly watchers = new Map<string, SegmentWatcherHandle>()\n /** Periodic retention sweep; cleared on shutdown. */\n private retentionTimer: ReturnType<typeof setInterval> | null = null\n /** Periodic modes-engine policy reaper (pre/post-buffer enforcement). */\n private policyTimer: ReturnType<typeof setInterval> | null = null\n /** Playback HTTP data-plane (hub reverse-proxies to it); null until served. */\n private playbackDataPlane: AddonDataPlaneHandle | null = null\n /** Periodic disk rescan rebuilding the playback index (past recordings). */\n private playbackRescanTimer: ReturnType<typeof setInterval> | null = null\n /** In-memory mirror of persisted targets so the SYNC retention resolver can\n * read per-device overrides without I/O. Refreshed on attach/detach/setConfig\n * and boot-restore. */\n private deviceTargets = new Map<number, RecordingTarget>()\n /** Short-lived stat-based byte counts keyed by deviceId — used ONLY in the\n * `getStatus` fallback path (index === 0 && footage exists). Entries expire\n * after {@link STAT_CACHE_TTL_MS} so repeated polls don't re-stat on every\n * call, yet the value stays reasonably fresh. */\n private readonly statSizeCache = new Map<number, { bytes: number; expiresAt: number }>()\n\n constructor() { super({ ...DEFAULT_CONFIG }) }\n\n protected async onInitialize(): Promise<AddonInitResult<RecorderActions>> {\n const raw = this.ctx.kernel.localNodeId ?? this.ctx.id\n this.nodeId = raw.includes('/') ? raw.split('/')[0]! : raw\n // Recordings live in the managed `recordings` storage location (operator-\n // configurable, multi-disk-ready), not the addon's data dir. Fall back to\n // the addon dir if the storage cap is unreachable at boot.\n const loc = await this.resolveRecordingsLocation()\n this.dataDir = loc.root\n this.recordingLocationId = loc.locationId\n if (loc.locationId) this.locationRootCache.set(loc.locationId, loc.root)\n // Build placement from the operator's assignments via the durable handle —\n // the WHOLE `recordingPlacement` blob round-trips through one schema so no\n // sibling field can be dropped on save. The fallback mirrors the config\n // default (seeded to DEFAULT_RECORDING_PLACEMENT) so a fresh install still\n // records out-of-the-box. No fallback at the profile level: an unassigned\n // profile fails recording explicitly (resolveProfilePlacements).\n const fallbackAssignments: Record<string, string[]> = {}\n for (const [key, ids] of Object.entries(this.config.recordingPlacement?.assignments ?? {})) {\n fallbackAssignments[key] = [...ids]\n }\n this.placementState = this.state<RecordingPlacementBlob>(\n 'recordingPlacement',\n RecordingPlacementBlobSchema,\n { assignments: fallbackAssignments },\n )\n const loadedPlacement = await this.placementState.get()\n this.placement = { assignments: loadedPlacement.assignments ?? {} }\n\n this.core = new RecorderCore({\n dataDir: this.dataDir,\n nodeId: this.nodeId,\n acquireSource: (deviceId, profile) => this.acquireSource(deviceId, profile),\n releaseSource: (pipelineKey) => this.releaseSource(pipelineKey),\n makeWriter: (cfg) => {\n // Pre-create the profile dir so ffmpeg can write `live.m3u8` from the\n // first frame (the date-bucket subdirs are created by ffmpeg via\n // `-strftime_mkdir 1`). Sync so it completes before `start()` spawns.\n mkdirSync(cfg.outDir, { recursive: true })\n return new SegmentWriter(cfg, { spawn: childProcessSpawn, logger: this.ctx.logger })\n },\n logger: this.ctx.logger,\n })\n\n this.startRetentionTimer()\n this.startPolicyTimer()\n this.subscribeTriggers()\n // Playback data plane: the addon streams its own segments through the hub's\n // port. `ctx.dataPlane` binds a localhost listener here; the hub authenticates\n // (admin) and reverse-proxies `/addon/recorder/playback/*` to it — same\n // cert/origin as the admin-ui, no token-in-path, no separate port.\n try {\n const handler = createFileDataPlaneHandler({ getRoots: () => this.playbackRoots() })\n this.playbackDataPlane = (await this.ctx.dataPlane?.serve({ prefix: 'playback', access: 'admin', handler })) ?? null\n this.ctx.logger.info('recorder playback data-plane served', {\n meta: { baseUrl: this.playbackDataPlane?.baseUrl ?? '(no dataPlane facility)' },\n })\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder playback data-plane failed to serve', { meta: { error: errMsg(err) } })\n }\n // Build the playback index from disk so PAST recordings (detached cameras,\n // pre-restart) are immediately queryable + playable, then rescan on a timer.\n await this.rescanPlayback()\n this.startPlaybackRescanTimer()\n\n this.ctx.logger.info('recorder addon started', {\n tags: { nodeId: this.nodeId },\n meta: {\n dataDir: this.dataDir,\n segmentSeconds: this.config.segmentSeconds,\n retentionMaxAgeDays: this.config.retentionMaxAgeDays,\n retentionMaxSizeGb: this.config.retentionMaxSizeGb,\n placement: this.placement,\n },\n })\n\n const provider: RecordingProvider = {\n getStatus: async ({ deviceId }): Promise<RecordingStatus | null> => {\n if (!this.core) return null\n const status = this.core.getStatus(deviceId)\n const indexBytes = status.storageBytes\n // Fast path: the RAM index already has bytes — no stat needed.\n if (indexBytes > 0) return status\n // Fallback path: index is 0 — check whether footage exists so we can\n // stat the on-disk tree instead of misleadingly returning 0.\n const availability = this.core.getAvailability(deviceId, 0, Date.now())\n const hasRanges = availability.ranges.length > 0\n const statSize = await this.deviceStatSize(deviceId, hasRanges)\n return { ...status, storageBytes: resolveStorageBytes({ indexBytes, hasRanges, statSize }) }\n },\n getAvailability: async ({ deviceId, fromMs, toMs }): Promise<RecordingAvailability> => {\n if (!this.core) return { deviceId, ranges: [] }\n return this.core.getAvailability(deviceId, fromMs, toMs)\n },\n getPlaybackManifest: ({ deviceId, fromMs, toMs }): Promise<RecordingManifest> =>\n this.buildPlaybackManifest(deviceId, fromMs, toMs),\n getStorageUsage: (): Promise<RecordingStorageUsage> => this.buildStorageUsage(),\n getDeviceConfig: async ({ deviceId }): Promise<RecordingConfig> => {\n const settings = this.ctxIfReady?.settings\n const target = settings ? (await readRecordingTargets(settings)).get(deviceId) : undefined\n return configFromTarget(target)\n },\n rescanStorage: async ({ deviceId }): Promise<RecordingStatus> => {\n const core = this.core\n if (!core) return {\n deviceId,\n enabled: false,\n activeMode: 'off',\n nodeId: this.nodeId,\n storageBytes: 0,\n }\n // Clear the live index for this device, then re-seed it from a fresh\n // sized disk scan of every recordings location. A clear + reseed is\n // always a complete and correct full re-count: the scan walks ALL on-disk\n // footage (pre-restart, detached, newly-written) and seeds every found\n // segment into a fresh index — nothing is missed and nothing is\n // double-counted.\n core.rescanDevice(deviceId)\n await this.seedIndexForDevice(core, deviceId)\n // Also evict any stale stat-based size cache for this device so the\n // fallback path in getStatus sees the freshly-seeded index bytes.\n this.statSizeCache.delete(deviceId)\n // Return the fresh status using the same resolution logic as getStatus\n // (index bytes → stat fallback if still 0 with footage present).\n const status = core.getStatus(deviceId)\n const indexBytes = status.storageBytes\n if (indexBytes > 0) return status\n const availability = core.getAvailability(deviceId, 0, Date.now())\n const hasRanges = availability.ranges.length > 0\n const statSize = await this.deviceStatSize(deviceId, hasRanges)\n return { ...status, storageBytes: resolveStorageBytes({ indexBytes, hasRanges, statSize }) }\n },\n setDeviceConfig: async ({ deviceId, config }): Promise<RecordingConfig> => {\n const next = targetFromConfig(config)\n if (next.enabled) {\n await this.attachCamera({ deviceId, profiles: next.profiles, segmentSeconds: next.segmentSeconds, rules: next.rules, retention: next.retention })\n } else {\n await this.detachCamera({ deviceId })\n // Persist the disabled config so rules/retention survive a re-enable\n // and getDeviceConfig still returns them. (attachCamera persists on\n // the enabled path; detach removes the row — re-upsert here.)\n const disabledSettings = this.ctx.settings\n if (disabledSettings) {\n await upsertRecordingTarget(disabledSettings, deviceId, next)\n }\n }\n this.deviceTargets.set(deviceId, next)\n return configFromTarget(next)\n },\n // Per-device recording settings surfaced in device-details (the system\n // contributions pass consults this cap even though it isn't device-bound).\n getDeviceSettingsContribution: (_input: { readonly deviceId: number }): Promise<ConfigUISchemaWithValues | null> =>\n this.buildDeviceSettingsContribution(),\n getDeviceLiveContribution: async () => null,\n applyDeviceSettingsPatch: async ({ patch }: { readonly deviceId: number; readonly patch: Record<string, unknown> }) => {\n const keys = Object.keys(patch ?? {})\n if (keys.length > 0) {\n this.ctx.logger.debug('recorder: ignoring legacy settings patch (widget owns settings)', { meta: { keys } })\n }\n return { success: true as const }\n },\n pruneFootage: async ({ deviceId }): Promise<{ floorMs: number | null; deletedBuckets: number; reclaimedBytes: number }> => {\n const core = this.core\n if (!core) return { floorMs: null, deletedBuckets: 0, reclaimedBytes: 0 }\n return core.pruneFootageForDevice(\n deviceId,\n Date.now(),\n (id, subtree) => this.resolveRetentionPolicy(id, subtree),\n (relPath, locationId) => this.removeBucketDir(relPath, locationId),\n )\n },\n }\n\n // The recorder is a `storage-evictable` participant: the core\n // StoragePressureManager owns the disk-pressure TRIGGER and asks us to free\n // a byte target on a location; we delete our own oldest footage there. The\n // recorder no longer runs its own free-space timer.\n const evictableProvider: IStorageEvictableProvider = {\n getEvictableUsage: async ({ locationId }) => {\n const core = this.core\n return { bytes: core ? core.evictableBytesOnLocation(locationId) : 0 }\n },\n evict: async ({ locationId, targetBytes }) => {\n const core = this.core\n if (!core) return { reclaimedBytes: 0, exhausted: true }\n return core.evictBytesOnLocation(\n locationId,\n targetBytes,\n (relPath, locId) => this.removeBucketDir(relPath, locId),\n )\n },\n }\n\n return {\n providers: [\n { capability: recordingCapability, provider },\n { capability: storageEvictableCapability, provider: evictableProvider },\n ],\n customActions: recorderActions,\n actionHandlers: {\n attachCamera: async (input) => this.attachCamera(input),\n detachCamera: async (input) => this.detachCamera(input),\n setPlacement: async (input) => this.setPlacement(input),\n },\n }\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.retentionTimer) { clearInterval(this.retentionTimer); this.retentionTimer = null }\n if (this.policyTimer) { clearInterval(this.policyTimer); this.policyTimer = null }\n if (this.playbackRescanTimer) { clearInterval(this.playbackRescanTimer); this.playbackRescanTimer = null }\n for (const handle of this.watchers.values()) handle.stop()\n this.watchers.clear()\n await this.playbackDataPlane?.dispose()\n this.playbackDataPlane = null\n await this.core?.detachAll()\n this.core = null\n }\n\n // ── Modes engine triggers ──────────────────────────────────────────────\n\n /**\n * Feed motion events into the policy engine. A `detected:true` transition for\n * a camera records a motion trigger; `onMotion` rules then retain segments\n * for their `postBufferSec`. Recording continuously-connected cameras get the\n * event even though the recorder is a separate process (UDS event bridge).\n * `onAudioThreshold` ingestion is deferred (audio analysis is stream-demand\n * gated) — the policy schema already supports it.\n */\n private subscribeTriggers(): void {\n this.ctx.eventBus.subscribe({ category: EventCategory.MotionOnMotionChanged }, (event) => {\n if (!event.data.detected) return\n this.core?.recordTrigger(event.data.deviceId, 'motion', event.data.timestamp)\n })\n // Audio level → onAudioThreshold rules. The core compares dBFS to each\n // rule's threshold. Audio analysis is stream-demand gated, so these only\n // arrive while the camera's stream is pulled — a known coverage limit.\n this.ctx.eventBus.subscribe({ category: EventCategory.PipelineAudioInferenceResult }, (event) => {\n const dbfs = event.data.frame.level?.dbfs\n if (dbfs == null) return\n this.core?.onAudioLevel(event.data.deviceId, dbfs, event.data.frame.timestamp)\n })\n }\n\n // ── Modes engine policy reaper (pre/post-buffer enforcement) ────────────\n\n private startPolicyTimer(): void {\n const timer = setInterval(() => void this.sweepPolicy(), this.config.policySweepIntervalMs)\n timer.unref?.()\n this.policyTimer = timer\n }\n\n /** Run one policy sweep: retain triggered segments, reap expired-untriggered. */\n private async sweepPolicy(): Promise<void> {\n const core = this.core\n if (!core) return\n try {\n await core.sweepPolicy(localClock, Date.now(), (deviceId, profile, startMs, durMs, locationId, subtree) =>\n this.deleteSegmentFile(deviceId, profile, startMs, durMs, locationId, subtree),\n )\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder policy sweep failed', { meta: { error: errMsg(err) } })\n }\n }\n\n /** Delete one relocated segment file under its storage location's root, in the\n * subtree it was written to. */\n private async deleteSegmentFile(deviceId: number, profile: string, startMs: number, durMs: number, locationId: string | undefined, subtree: Subtree): Promise<void> {\n const root = await this.locationRootOrDefault(locationId)\n const abs = path.resolve(root, segmentRelPath(String(deviceId), profile, subtree, startMs, durMs))\n await fs.rm(abs, { force: true })\n }\n\n // ── Retention ──────────────────────────────────────────────────────────\n\n private startRetentionTimer(): void {\n const intervalMs = this.config.retentionSweepIntervalMs\n const timer = setInterval(() => void this.sweepRetention(), intervalMs)\n // Don't let the sweep timer keep the process alive on its own.\n timer.unref?.()\n this.retentionTimer = timer\n }\n\n /** Run one retention sweep across every attached camera, then enforce the\n * disk-pressure free-space guard across every device. */\n private async sweepRetention(): Promise<void> {\n const core = this.core\n if (!core) return\n // Per-device retention: prune footage and coordinate event pruning to the\n // same floor (B3). `pruneDeviceWithEvents` calls `pruneFootageForDevice`\n // per device (equivalent to the old `core.sweepRetention` loop) and then\n // best-effort calls `pipelineAnalytics.pruneEventsBefore` so events +\n // thumbnails never outlive footage. Per-device failures are isolated.\n const nowMs = Date.now()\n for (const deviceId of core.attachedDevices()) {\n try {\n await pruneDeviceWithEvents(\n core,\n deviceId,\n nowMs,\n (id, subtree) => this.resolveRetentionPolicy(id, subtree),\n (relPath, locationId) => this.removeBucketDir(relPath, locationId),\n this.ctx.api,\n this.ctx.logger,\n )\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder retention sweep failed for device', { tags: { deviceId }, meta: { error: errMsg(err) } })\n }\n }\n // Whole-volume disk pressure is no longer the recorder's concern: the core\n // StoragePressureManager owns the trigger and drives our `storage-evictable`\n // provider (see onInitialize). Here we only run per-device age/size retention.\n }\n\n /** Available + total bytes on a volume (`statfs`); null when unstattable. */\n private async rootCapacity(root: string): Promise<{ availableBytes: number; totalBytes: number } | null> {\n try {\n const st = await fs.statfs(root)\n return { availableBytes: st.bavail * st.bsize, totalBytes: st.blocks * st.bsize }\n } catch {\n return null\n }\n }\n\n // ── Storage-usage report (stat-based, on-demand) ───────────────────────\n\n /**\n * Accurate recordings storage usage for this node: global total + per-camera\n * (summed over every profile/subtree/location) + per-location used/available.\n * Stat-based so detached cameras count correctly (the playback index is\n * name-only). Includes the read-only legacy root (locationId null) in the\n * totals + per-camera, but never as a guarded managed location.\n */\n private async buildStorageUsage(): Promise<RecordingStorageUsage> {\n const managed = await this.listRecordingsLocations()\n const roots: { id: string | null; root: string }[] = managed.map((l) => ({ id: l.id ?? null, root: l.root }))\n\n const sized: { id: string | null; root: string; usage: RootUsage }[] = await Promise.all(\n roots.map(async (r) => ({ ...r, usage: await sizeRecordingsTree(r.root) })),\n )\n const merged = mergeRootUsage(sized.map((s) => s.usage))\n\n const devices = [...merged.perDevice]\n .map(([camera, usedBytes]) => ({ deviceId: Number(camera), usedBytes }))\n .filter((d) => Number.isFinite(d.deviceId))\n .sort((a, b) => b.usedBytes - a.usedBytes)\n\n const locations = await Promise.all(\n sized.map(async (s) => {\n const cap = await this.rootCapacity(s.root)\n return {\n locationId: s.id,\n usedBytes: s.usage.totalBytes,\n availableBytes: cap?.availableBytes ?? null,\n totalBytes: cap?.totalBytes ?? null,\n }\n }),\n )\n\n return { nodeId: this.nodeId, totalUsedBytes: merged.totalBytes, devices, locations }\n }\n\n /**\n * Stat the on-disk tree for a single device and return its total bytes.\n * Uses a short-lived cache ({@link STAT_CACHE_TTL_MS}) so repeated `getStatus`\n * polls on the zero-index fallback path don't re-stat on every call.\n *\n * Called ONLY when `indexBytes === 0 && hasRanges` — not on the common\n * non-zero path, so disk I/O cost is limited to the edge case.\n *\n * @param deviceId The device to size.\n * @param hasRanges If false the call is skipped and 0 is returned immediately.\n */\n private async deviceStatSize(deviceId: number, hasRanges: boolean): Promise<number> {\n if (!hasRanges) return 0\n const now = Date.now()\n const cached = this.statSizeCache.get(deviceId)\n if (cached && now < cached.expiresAt) return cached.bytes\n // Sum across every known root (default + all managed locations).\n const roots = this.playbackRoots()\n const key = String(deviceId)\n let total = 0\n for (const root of roots) {\n const usage = await sizeRecordingsTree(root)\n total += usage.perDevice.get(key) ?? 0\n }\n this.statSizeCache.set(deviceId, { bytes: total, expiresAt: now + STAT_CACHE_TTL_MS })\n return total\n }\n\n /**\n * Resolve the retention policy for a device's `continuous` vs `events`\n * subtree: the per-subtree override (`> 0`) wins, else the global default.\n * P2: global addon config only (per-device override arrives with the\n * per-device settings UI in P6 — `deviceId` is already threaded for that swap).\n */\n private resolveRetentionPolicy(deviceId: number, subtree: Subtree): RetentionPolicy {\n const c = this.config\n const pick = (ovr: number, base: number): number => (ovr > 0 ? ovr : base)\n const defaultAgeDays = subtree === 'events'\n ? pick(c.eventRetentionMaxAgeDays, c.retentionMaxAgeDays)\n : pick(c.continuousRetentionMaxAgeDays, c.retentionMaxAgeDays)\n const defaultSizeGb = subtree === 'events'\n ? pick(c.eventRetentionMaxSizeGb, c.retentionMaxSizeGb)\n : pick(c.continuousRetentionMaxSizeGb, c.retentionMaxSizeGb)\n const override = this.deviceTargets.get(deviceId)?.retention\n return resolveDeviceRetention(override, defaultAgeDays, defaultSizeGb)\n }\n\n /**\n * `rm -rf` one hour-bucket directory under its storage location's root.\n * `relPath` originates from our own RAM index\n * (`<deviceId>/<profile>/<Y>/<M>/<D>/<H>`) and `locationId` is the location it\n * lives on; resolve against that location's root and refuse anything that\n * escapes it as a defense-in-depth guard.\n */\n protected async removeBucketDir(relPath: string, locationId?: string): Promise<void> {\n const root = path.resolve(await this.locationRootOrDefault(locationId))\n const abs = path.resolve(root, relPath)\n if (abs !== root && !abs.startsWith(root + path.sep)) {\n throw new Error(`recorder retention: refusing to rm outside location root: ${relPath}`)\n }\n await fs.rm(abs, { recursive: true, force: true })\n }\n\n // ── Custom actions ────────────────────────────────────────────────────\n\n private async attachCamera(input: AttachCameraInput): Promise<{ success: true }> {\n const core = this.core\n if (!core) throw new Error('RecorderAddon: attachCamera called before initialize completed')\n\n const profiles = await this.resolveRecordingProfiles(input.deviceId, input.profiles)\n if (profiles.length === 0) {\n throw new Error(\n `RecorderAddon.attachCamera: no assigned broker sources for device ${input.deviceId}`,\n )\n }\n const segmentSeconds = input.segmentSeconds ?? this.config.segmentSeconds\n const rules = input.rules ?? []\n\n // Tear down any existing watchers for this device before re-attaching —\n // `core.attach` already stops the old writers.\n this.stopWatchers(input.deviceId)\n\n // Resolve where each profile writes (placement + least-full striping). The\n // SAME chosen outDir drives both the ffmpeg writer (via core) and the\n // watcher; the locationId stamps the index for per-location playback/guard.\n const placements = await this.resolveProfilePlacements(input.deviceId, profiles)\n const outDirs: Record<string, string> = {}\n for (const [profile, pl] of placements) outDirs[profile] = pl.outDir\n\n await core.attach({ deviceId: input.deviceId, profiles, segmentSeconds, rules, outDirs })\n\n // Start one live.m3u8 watcher per profile. Every finalized segment is\n // indexed; if it's already demanded at finalize (continuous, or inside a\n // live trigger window) we mark it retained immediately so the policy reaper\n // skips it. Segments NOT demanded yet stay candidates — the reaper later\n // retains them (if a trigger arrives within preBufferSec) or discards them.\n for (const profile of profiles) {\n const pl = placements.get(profile)!\n const key = makeProfileBrokerId(input.deviceId, profile)\n const handle = startSegmentWatcher({\n outDir: pl.outDir,\n intervalMs: this.config.watchIntervalMs,\n // Classify each finalized segment into its continuous/events subtree\n // (node-local clock) so the watcher relocates it into the right tree.\n classify: (startMs, durMs) =>\n this.core?.classifySubtree(input.deviceId, startMs + durMs, localClock(startMs + durMs)) ?? 'continuous',\n onSegment: (startMs, durMs, bytes, subtree) => {\n const core = this.core\n if (!core) return\n core.onSegmentFinalized(input.deviceId, profile, startMs, durMs, bytes, pl.locationId, subtree)\n if (core.shouldRetainSegment(input.deviceId, startMs, startMs + durMs, localClock(startMs + durMs))) {\n core.markSegmentRetained(input.deviceId, profile, startMs)\n }\n },\n logger: this.ctx.logger,\n })\n this.watchers.set(key, handle)\n }\n\n // Persist the recording intent only AFTER a successful attach, so a\n // camera that can never resolve a source isn't left enabled to retry-fail\n // every boot. Stores the operator's ORIGINAL choices (override profiles /\n // segmentSeconds may be undefined → restore recomputes assigned sources).\n await this.persistTarget(input)\n\n // Mirror into the in-memory map so the SYNC retention resolver can read\n // per-device config without I/O.\n this.deviceTargets.set(input.deviceId, {\n enabled: true,\n profiles: input.profiles ? [...input.profiles] : undefined,\n segmentSeconds: input.segmentSeconds,\n rules: input.rules ? [...input.rules] : undefined,\n retention: input.retention,\n })\n\n // Seed the live index with pre-existing on-disk footage so storageBytes and\n // the size-retention cap see ALL segments, not just those finalized since\n // this attach. Scoped to the device sub-tree of each location root so the\n // scan is cheap even on a large volume. Best-effort: a failed scan is logged\n // but never aborts the attach.\n await this.seedIndexForDevice(core, input.deviceId)\n\n this.ctx.logger.info('recorder attachCamera', {\n tags: { deviceId: input.deviceId },\n meta: { profiles, segmentSeconds },\n })\n return { success: true }\n }\n\n /**\n * Set the GLOBAL recording placement (profile/track → location ids). Persists\n * to the addon store (so it survives restarts) and rebuilds `this.placement`\n * + clears the per-profile location cache so the next attach re-resolves. The\n * admin UI (and the live-verification path) call this.\n */\n private async setPlacement(input: SetPlacementInput): Promise<{ success: true }> {\n // Persist through the durable handle: the whole `recordingPlacement` blob\n // is Zod-validated and written as one value, so no field can be dropped.\n // Copy the readonly input into the blob's mutable-array shape.\n const assignments: Record<string, string[]> = {}\n for (const [key, ids] of Object.entries(input.assignments ?? {})) assignments[key] = [...ids]\n await this.placementState?.set({ assignments })\n this.placement = { assignments: input.assignments ?? {} }\n this.profileWriteLocation.clear()\n this.ctx.logger.info('recorder placement updated', { meta: { assignments: input.assignments } })\n return { success: true }\n }\n\n private async detachCamera(input: DetachCameraInput): Promise<{ success: true }> {\n await this.clearTarget(input.deviceId)\n this.stopWatchers(input.deviceId)\n await this.core?.detach(input.deviceId)\n // Refresh the playback index so this camera's just-finished recording stays\n // queryable/playable now that its live index is gone.\n await this.rescanPlayback()\n this.ctx.logger.info('recorder detachCamera', { tags: { deviceId: input.deviceId } })\n return { success: true }\n }\n\n // ── Boot-time index seeding (sized scan per device) ──────────────────────\n\n /**\n * Scan every recordings location for this device's sub-tree (with sizes) and\n * seed the live RAM index so `storageBytes` and size-retention reflect ALL\n * on-disk footage, not just segments finalized since the last attach. Scoping\n * the scan to `<root>/<deviceId>/` keeps it cheap on large volumes.\n *\n * Must be called after `core.attach(deviceId, …)` so the seed lands in the\n * camera's live index (not a stale pre-attach sentinel). Best-effort: any\n * per-location failure is warned and skipped.\n */\n private async seedIndexForDevice(core: RecorderCore, deviceId: number): Promise<void> {\n let locations: { id: string | undefined; root: string }[]\n try {\n locations = await this.listRecordingsLocations()\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder seedIndex: could not list locations', {\n tags: { deviceId }, meta: { error: errMsg(err) },\n })\n return\n }\n\n const roots: { id: string | undefined; root: string }[] = [...locations]\n\n const segments: SeedSegment[] = []\n for (const loc of roots) {\n // Scope to `<root>/<deviceId>` to avoid scanning every other camera's footage.\n const deviceRoot = path.join(loc.root, String(deviceId))\n try {\n const entries = await scanRecordings(deviceRoot, { locationId: loc.id, withSizes: true })\n for (const e of entries) {\n // scanRecordings is called with the DEVICE sub-dir as root, so e.relPath omits\n // the leading \"<deviceId>/\" — re-add it so parseSegmentRelPath sees <cam>/<profile>/...\n const fullRelPath = `${deviceId}/${e.relPath}`\n const parsed = parseSegmentRelPath(fullRelPath)\n if (!parsed) continue\n segments.push({\n profile: parsed.profile,\n startMs: parsed.startMs,\n durMs: parsed.durMs,\n bytes: e.bytes,\n locationId: e.locationId,\n subtree: parsed.subtree,\n })\n }\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder seedIndex: scan failed for location', {\n tags: { deviceId }, meta: { locationId: loc.id, error: errMsg(err) },\n })\n }\n }\n\n if (segments.length === 0) return\n core.seedIndexFromScan(deviceId, segments)\n this.ctx.logger.info('recorder seedIndex: seeded from disk scan', {\n tags: { deviceId }, meta: { segmentCount: segments.length },\n })\n }\n\n // ── Playback index (past recordings via disk scan) ──────────────────────\n\n private startPlaybackRescanTimer(): void {\n const timer = setInterval(() => void this.rescanPlayback(), this.config.playbackRescanIntervalMs)\n timer.unref?.()\n this.playbackRescanTimer = timer\n }\n\n /** Rebuild the playback index from a shallow disk scan of EVERY recordings\n * storage location plus the read-only legacy root, stamping each segment with\n * the location it came from. */\n private async rescanPlayback(): Promise<void> {\n const core = this.core\n if (!core) return\n try {\n const locations = await this.listRecordingsLocations()\n const scanRoots: { id: string | undefined; root: string }[] = [...locations]\n const scanned = await Promise.all(scanRoots.map((l) => scanRecordings(l.root, { locationId: l.id })))\n core.setPlaybackIndex(scanned.flat())\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder playback rescan failed', { meta: { error: errMsg(err) } })\n }\n }\n\n\n /** All recordings storage locations (every `recordings*` type) as\n * `{ id, root }`, resolving (and caching) each root. Falls back to the\n * single default root when the storage cap is unreachable. */\n private async listRecordingsLocations(): Promise<{ id: string | undefined; root: string }[]> {\n try {\n const locs = await this.ctx.api.storage.listLocations.query({})\n const recordings = locs.filter((l) => l.type === 'recordings' || l.type === 'recordingsLow')\n if (recordings.length > 0) {\n return Promise.all(recordings.map(async (l) => ({ id: l.id, root: await this.resolveLocationRoot(l.id) })))\n }\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder: listLocations failed — scanning default root only', { meta: { error: errMsg(err) } })\n }\n return [{ id: this.recordingLocationId, root: this.dataDir }]\n }\n\n /**\n * Resolve the recordings write root via the `storage` cap (the default\n * `recordings` location). Falls back to the addon-local data dir with no\n * locationId when the storage cap is unreachable at boot, so recording still\n * works degraded rather than not at all.\n */\n private async resolveRecordingsLocation(): Promise<{ root: string; locationId: string | undefined }> {\n try {\n const def = await this.ctx.api.storage.getDefaultLocation.query({ type: 'recordings' })\n if (def) {\n const root = await this.ctx.api.storage.resolve.query({ location: def.id, relativePath: '' })\n return { root, locationId: def.id }\n }\n this.ctx.logger.warn('recorder: no default recordings storage location — using addon data dir')\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder: storage cap unreachable — using addon data dir', { meta: { error: errMsg(err) } })\n }\n return { root: path.join(this.ctx.dataDir, 'recordings'), locationId: undefined }\n }\n\n /** Distinct recordings roots the playback server searches: the default\n * location root plus every location root we've resolved (write or scan). */\n private playbackRoots(): string[] {\n return [...new Set([this.dataDir, ...this.locationRootCache.values()])]\n }\n\n /** Absolute root of a storage location (cached `storage.resolve(id, '')`). */\n protected async resolveLocationRoot(locationId: string): Promise<string> {\n const cached = this.locationRootCache.get(locationId)\n if (cached) return cached\n const root = await this.ctx.api.storage.resolve.query({ location: locationId, relativePath: '' })\n this.locationRootCache.set(locationId, root)\n return root\n }\n\n /** A location's absolute root, or the default recordings root when the id is\n * absent (legacy) or unresolvable. */\n private async locationRootOrDefault(locationId: string | undefined): Promise<string> {\n if (!locationId) return this.dataDir\n try {\n return await this.resolveLocationRoot(locationId)\n } catch {\n return this.dataDir\n }\n }\n\n /** Free bytes available on a location's volume (`statfs`); null on failure. */\n private async locationFreeBytes(locationId: string): Promise<number | null> {\n try {\n const st = await fs.statfs(await this.resolveLocationRoot(locationId))\n return st.bavail * st.bsize\n } catch {\n return null\n }\n }\n\n /**\n * The GLOBAL write location for a profile/track — camera-independent, so all\n * cameras record that profile to the SAME location. The placement-assigned\n * location(s) are chosen by free space (least-full wins) ONCE and cached.\n * Returns `null` when the profile is UNASSIGNED — there is no fallback; the\n * caller must fail the recording explicitly. (Free-space-driven switching\n * when a disk fills is part of the deferred multi-location structure rework.)\n */\n private async resolveGlobalProfileLocation(\n profile: string,\n ): Promise<{ locationId: string; root: string } | null> {\n const cached = this.profileWriteLocation.get(profile)\n if (cached) return cached\n const ids = resolveAssignedLocations(this.placement, profile)\n if (ids.length === 0) return null\n const free = await Promise.all(\n ids.map(async (id) => ({ locationId: id, availableBytes: await this.locationFreeBytes(id) })),\n )\n const chosen = chooseWriteLocation(ids, free)\n if (chosen === null) return null\n const choice = { locationId: chosen, root: await this.resolveLocationRoot(chosen) }\n this.profileWriteLocation.set(profile, choice)\n return choice\n }\n\n /**\n * Resolve where each profile's segments are written for this camera: the\n * global per-profile location's root + `<deviceId>/<profile>`. THROWS a clear\n * error when any profile has no assigned storage location — recording must\n * fail explicitly rather than write somewhere unintended.\n */\n private async resolveProfilePlacements(\n deviceId: number,\n profiles: readonly CamProfile[],\n ): Promise<Map<string, { outDir: string; locationId: string }>> {\n const out = new Map<string, { outDir: string; locationId: string }>()\n const unassigned: string[] = []\n for (const profile of profiles) {\n const choice = await this.resolveGlobalProfileLocation(profile)\n if (choice === null) {\n unassigned.push(profile)\n continue\n }\n out.set(profile, { outDir: path.join(choice.root, String(deviceId), profile), locationId: choice.locationId })\n }\n if (unassigned.length > 0) {\n throw new Error(\n `recorder: no storage location assigned for profile(s) [${unassigned.join(', ')}] — assign one in recording settings before enabling recording`,\n )\n }\n return out\n }\n\n // ── Per-device settings (device-details recording widget) ─────────────\n\n /** Build the device-details recording contribution: the widget section only. */\n private async buildDeviceSettingsContribution(): Promise<ConfigUISchemaWithValues | null> {\n if (!this.ctxIfReady?.settings) return null\n return hydrateSchema(buildRecordingDeviceSchema(), {})\n }\n\n // ── Durable enable (persisted recording targets) ───────────────────────\n\n /** Best-effort persist of one camera's recording intent. */\n private async persistTarget(input: AttachCameraInput): Promise<void> {\n const settings = this.ctx.settings\n if (!settings) return\n try {\n await upsertRecordingTarget(settings, input.deviceId, {\n enabled: true,\n profiles: input.profiles ? [...input.profiles] : undefined,\n segmentSeconds: input.segmentSeconds,\n rules: input.rules ? [...input.rules] : undefined,\n retention: input.retention,\n })\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder persist target failed', {\n meta: { deviceId: input.deviceId, error: errMsg(err) },\n })\n }\n }\n\n /** Best-effort removal of one camera's persisted recording intent. */\n private async clearTarget(deviceId: number): Promise<void> {\n const settings = this.ctx.settings\n if (!settings) return\n try {\n await removeRecordingTarget(settings, deviceId)\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder clear target failed', { meta: { deviceId, error: errMsg(err) } })\n }\n }\n\n /**\n * Re-attach every persisted enabled camera once the hub is reachable\n * (`ctx.api.*` is safe here, not in `onInitialize`). Per-device failures —\n * a camera that's offline at boot — are logged and kept (they resume on the\n * next attach), never aborting the rest of the restore.\n */\n override async onHubReachable(): Promise<void> {\n const settings = this.ctx.settings\n if (!settings) return\n const targets = await readRecordingTargets(settings)\n const enabled = [...targets].filter(([, t]) => t.enabled)\n if (enabled.length === 0) return\n\n // Gate on the broker cap. `onHubReachable` can fire before this freshly\n // (re)spawned child's route to the sibling `stream-broker` is wired, so a\n // restore `attachCamera` would race the broker's readiness and fail with\n // \"Service not found\". `acquireCapability` blocks until the broker's\n // readiness signal reaches this node — same gate the orchestrator uses for\n // `pipeline-executor`. On timeout we attempt anyway (per-device failures\n // are caught and the camera resumes on the next attach).\n try {\n await this.ctx.acquireCapability('stream-broker', { type: 'node', nodeId: this.nodeId }, { timeoutMs: 60_000 })\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder restore: stream-broker not ready in time, attempting anyway', {\n meta: { error: errMsg(err) },\n })\n }\n\n this.ctx.logger.info('recorder restoring recording targets', { meta: { count: enabled.length } })\n for (const [deviceId, target] of enabled) {\n // Seed the in-memory map with only ENABLED targets (the loop above filters\n // to enabled only). Disabled cameras correctly fall back to the global\n // free-space threshold in the retention sweep — no seeding needed for them.\n this.deviceTargets.set(deviceId, target)\n try {\n await this.attachCamera({ deviceId, profiles: target.profiles, segmentSeconds: target.segmentSeconds, rules: target.rules, retention: target.retention })\n } catch (err: unknown) {\n this.ctx.logger.warn('recorder restore attach failed', {\n tags: { deviceId },\n meta: { error: errMsg(err) },\n })\n }\n }\n }\n\n private stopWatchers(deviceId: number): void {\n const prefix = `${deviceId}/`\n for (const [key, handle] of [...this.watchers]) {\n if (!key.startsWith(prefix)) continue\n handle.stop()\n this.watchers.delete(key)\n }\n }\n\n // ── Source resolution (broker single-dial) ─────────────────────────────\n\n /**\n * Pick which profiles to record for a device. The recorder enumerates the\n * broker's ASSIGNED profile slots, deduped by physical source\n * (`selectAssignedProfileSlots`) so the same camera encoder is never recorded\n * twice. An operator `override` (the per-device \"Profiles\" multiselect)\n * DISABLES the unselected profiles: the result is the assigned set INTERSECTED\n * with the selection. Per the spec, recording keeps a MINIMUM OF 1 — if the\n * selection matches none of the assigned sources (stale pick / wrong camera),\n * it's ignored and every assigned profile is recorded. Empty/absent override =\n * record every assigned source.\n */\n private async resolveRecordingProfiles(\n deviceId: number,\n override: readonly CamProfile[] | undefined,\n ): Promise<readonly CamProfile[]> {\n const api = this.ctx.api\n if (!api) throw new Error(`RecorderAddon.resolveRecordingProfiles: ctx.api unavailable (device ${deviceId})`)\n const slots = await api.streamBroker.listAllProfileSlots.query()\n const assigned = selectAssignedProfileSlots(slots, deviceId).map((slot) => slot.profile)\n if (!override || override.length === 0) return assigned\n const selected = assigned.filter((p) => override.includes(p))\n return selected.length > 0 ? selected : assigned\n }\n\n /**\n * Acquire a demand-counted broker stream for one (deviceId, profile) via\n * `getStreamWithCodec` (passthrough — `video:'copy'`). The returned\n * `pipelineKey` is released on detach, so the recorder rides the broker's\n * single source dial instead of opening a second camera connection.\n */\n private async acquireSource(deviceId: number, profile: CamProfile): Promise<{ url: string; pipelineKey: string }> {\n const api = this.ctx.api\n if (!api) throw new Error(`RecorderAddon.acquireSource: ctx.api unavailable (device ${deviceId})`)\n const source = await api.streamBroker.getStreamWithCodec.mutate({\n deviceId,\n video: 'copy',\n audio: 'aac',\n profile,\n tag: `recorder:${deviceId}/${profile}`,\n })\n return { url: source.url, pipelineKey: source.pipelineKey }\n }\n\n private async releaseSource(pipelineKey: string): Promise<void> {\n const api = this.ctx.api\n if (!api) return\n try {\n await api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey })\n } catch (err: unknown) {\n this.ctx.logger.warn('releaseStreamWithCodec failed', {\n meta: { pipelineKey, error: errMsg(err) },\n })\n }\n }\n\n // ── Playback manifest ─────────────────────────────────────────────────\n\n /**\n * Build a master + per-profile variant playlist on disk for the\n * requested window and return the local master path. Returns\n * `localMasterPath: null` when the camera has no recorded segments in\n * the range.\n */\n /**\n * Probe a profile's video codec + resolution for the master playlist, reading\n * only the head of one segment (the `moov` sits at the start) and caching per\n * (device, profile) — the camera's encoding is stable. Returns null on any\n * read/parse failure (the variant then ships without CODECS, as before).\n */\n private async probeProfileCodec(\n deviceId: number,\n profile: string,\n segAbsPath: string,\n ): Promise<{ codecs: string; resolution: { width: number; height: number } } | null> {\n const key = `${deviceId}/${profile}`\n const cached = this.profileCodecCache.get(key)\n if (cached) return cached\n try {\n const fh = await fs.open(segAbsPath, 'r')\n try {\n const buf = Buffer.alloc(65536) // moov fits comfortably for 1 video + 1 audio track\n const { bytesRead } = await fh.read(buf, 0, buf.length, 0)\n const info = extractVideoCodecInfo(buf.subarray(0, bytesRead))\n if (!info) return null\n const result = { codecs: info.codec, resolution: { width: info.width, height: info.height } }\n this.profileCodecCache.set(key, result)\n return result\n } finally {\n await fh.close()\n }\n } catch {\n return null\n }\n }\n\n private async buildPlaybackManifest(deviceId: number, fromMs: number, toMs: number): Promise<RecordingManifest> {\n const core = this.core\n if (!core) return { deviceId, localMasterPath: null, playbackUrl: null, playbackEndpoints: [] }\n\n const profiles = core.getProfiles(deviceId)\n const deviceDir = path.join(this.dataDir, String(deviceId))\n const variants: MasterVariant[] = []\n\n for (const profile of profiles) {\n const segments = core.listSegments(deviceId, profile, fromMs, toMs)\n if (segments.length === 0) continue\n // Only reference segments whose file is actually on disk — a missing file\n // would 404 mid-playlist and break the player (defense-in-depth over the\n // watcher's index-only-if-present guard).\n const variantSegments: VariantSegment[] = []\n let firstSegAbs: string | null = null\n for (const s of segments) {\n const rel = segmentRelPath(String(deviceId), profile, s.subtree, s.startMs, s.durMs)\n // Stat the segment in ITS storage location's root (segments may span\n // locations); the playback server then searches roots to serve them.\n const segRoot = await this.locationRootOrDefault(s.locationId)\n const segAbs = path.join(segRoot, rel)\n try {\n await fs.stat(segAbs)\n } catch {\n continue\n }\n if (firstSegAbs === null) firstSegAbs = segAbs\n // Variant uris are relative to the device dir so the master + variants\n // form a self-contained playlist tree.\n variantSegments.push({ uri: rel.split('/').slice(1).join('/'), startMs: s.startMs, durMs: s.durMs })\n }\n if (variantSegments.length === 0) continue\n const variantBody = buildVariantPlaylist(variantSegments)\n const variantPath = path.join(deviceDir, `${profile}.m3u8`)\n await fs.mkdir(deviceDir, { recursive: true })\n await fs.writeFile(variantPath, variantBody, 'utf8')\n // Probe the codec so the master playlist carries CODECS/RESOLUTION — hls.js\n // then skips the HEVC variant on browsers (Chrome) that can't decode it.\n const codec = firstSegAbs ? await this.probeProfileCodec(deviceId, profile, firstSegAbs) : null\n variants.push({\n profile,\n bandwidth: PROFILE_BANDWIDTH[profile] ?? DEFAULT_BANDWIDTH,\n uri: `${profile}.m3u8`,\n ...(codec ? { codecs: codec.codecs, resolution: codec.resolution } : {}),\n })\n }\n\n if (variants.length === 0) return { deviceId, localMasterPath: null, playbackUrl: null, playbackEndpoints: [] }\n\n const masterPath = path.join(deviceDir, 'master.m3u8')\n await fs.mkdir(deviceDir, { recursive: true })\n await fs.writeFile(masterPath, buildMasterPlaylist(variants), 'utf8')\n // RELATIVE URL through the hub's data-plane reverse-proxy. No host, no port,\n // no token-in-path: the hub serves it on its own origin and authenticates the\n // caller (admin). The variant/segment URIs inside the playlists are relative,\n // so the player resolves them against this master URL automatically.\n //\n // The `?from=&to=` query encodes the WINDOW this manifest represents. The\n // master.m3u8 file is regenerated per request at a fixed per-device path, so\n // without it two different windows would share an identical URL and a client\n // player wouldn't reload when switching clips. The data-plane handler strips\n // the query (serves master.m3u8); it exists purely to make the URL distinct\n // per window. Relative variant/segment URIs resolve against the path, not the\n // query, so they stay un-suffixed.\n const playbackUrl = `/addon/recorder/playback/${deviceId}/master.m3u8?from=${fromMs}&to=${toMs}`\n return { deviceId, localMasterPath: masterPath, playbackUrl, playbackEndpoints: [playbackUrl] }\n }\n}\n\n/**\n * Convert a wall-clock epoch (ms) into a {@link ScheduleClock} in the recording\n * node's LOCAL time — operators author schedules (\"day\", \"night\") in local\n * time, while the segment tree stays UTC.\n */\nfunction localClock(ms: number): ScheduleClock {\n const d = new Date(ms)\n return { weekday: d.getDay(), minutesOfDay: d.getHours() * 60 + d.getMinutes() }\n}\n\n// Static `customActions` catalog — read by the addon registry at boot time\n// (before instantiating the addon class) to wire cross-process dispatch.\nexport { recorderActions as customActions } from './recorder-actions.js'\n\n// Re-export the core for tests / consumers that need direct access.\nexport { RecorderCore } from './recorder-core.js'\n"],"names":["HOUR_MS","z.record","z.string","z.object","z.array","path","fs","z.number","z.literal","statSize","core"],"mappings":"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"}
@@ -1,41 +0,0 @@
1
- import * as React from 'react';
2
- import type { EncodeProfile } from '@camstack/types';
3
- import type { WidgetProps } from '@camstack/ui-library';
4
- export interface FfmpegParamsWidgetConfig {
5
- /** Patch key written through `deviceManager.updateDeviceField`. */
6
- fieldKey: string;
7
- label?: string;
8
- /**
9
- * Read-only "Source (probed)" snapshot — codec / resolution / fps /
10
- * bitrate (and audio shape) the publisher actually advertised when it
11
- * registered the camStream. Rendered as a chips card at the top of the
12
- * widget so the operator sees the REAL per-stream numbers next to the
13
- * (separate) override editor. Each consumer (broker source-stream,
14
- * broker derived, HAP, Alexa) populates it from its own source-side
15
- * data — the widget never invents probed values.
16
- */
17
- sourceParams?: EncodeProfile;
18
- /** Initial value sent by the contribution builder — read once on
19
- * mount. The widget tracks its own state from then on. */
20
- initialValue?: EncodeProfile;
21
- /** Routing for `deviceManager.updateDeviceField`. Required when the
22
- * widget is mounted via the form-builder (WidgetProps wrapper). */
23
- writerCapName?: string;
24
- writerAddonId?: string;
25
- }
26
- export interface FfmpegParamsCoreProps {
27
- readonly config: FfmpegParamsWidgetConfig;
28
- readonly value: EncodeProfile | undefined;
29
- readonly onSave: (next: EncodeProfile | undefined) => void;
30
- }
31
- /**
32
- * Pure presentational component. Receives `value` + `onSave` directly
33
- * from its parent — used by the WidgetProps wrapper below AND by the
34
- * unit tests (so we don't need to stub a tRPC client).
35
- *
36
- * This is the derived-stream transform editor: an always-editable codec
37
- * + video / audio + raw ffmpeg-args form. There is no preset layer — the
38
- * operator authors the transform fields directly.
39
- */
40
- export declare function FfmpegParamsCore({ config, value, onSave }: FfmpegParamsCoreProps): React.ReactElement;
41
- export declare function FfmpegParamsField(props: WidgetProps): React.ReactElement | null;