@camstack/addon-pipeline 1.2.171 → 1.2.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +5 -5
  4. package/dist/detection-pipeline/index.mjs +4 -4
  5. package/dist/{dist-D0liXHO7.mjs → dist-D35w3ngN.mjs} +154 -3
  6. package/dist/{dist-M39Js6iu.js → dist-DSJDq5hY.js} +154 -3
  7. package/dist/{lazy-sharp-CO-c2L9_.js → lazy-sharp-Fre6HHg6.js} +1 -1
  8. package/dist/{local-frame-registry--_WjAPa2.mjs → local-frame-registry-AtRvlifj.mjs} +1 -1
  9. package/dist/{local-frame-registry-CHUbcz6r.js → local-frame-registry-Y4TreSiT.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-CQ6VBSrE.mjs → node-B2NS7cyi.mjs} +1 -1
  13. package/dist/{node-Byy6tlFm.js → node-DVcGI_Kr.js} +1 -1
  14. package/dist/pipeline-runner/index.js +63 -6
  15. package/dist/pipeline-runner/index.mjs +62 -5
  16. package/dist/{process-memory-DjLHImhM.mjs → process-memory-BFuXzmyZ.mjs} +1 -1
  17. package/dist/{process-memory-CH3OIHHe.js → process-memory-DR2vCoO6.js} +1 -1
  18. package/dist/recorder/index.js +97 -13
  19. package/dist/recorder/index.mjs +97 -13
  20. package/dist/{segment-demux-js-B0KzROD9.js → segment-demux-js-CeJfEP5v.js} +1 -1
  21. package/dist/{segment-demux-js-Bmy3u2VM.mjs → segment-demux-js-wtPM5v2H.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +2 -2
  23. package/dist/session-decode/decode-worker-child.mjs +1 -1
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CN6ybZtQ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-eyqVqXTI.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_HZNoVq.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC-nahrU.mjs +26 -0
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-Dq40Ieaf.mjs → hostInit-eKLgfpTH.mjs} +3 -3
  31. package/dist/stream-broker/index.js +87 -4
  32. package/dist/stream-broker/index.mjs +87 -4
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-CAWMqnxs.mjs → worker-protocol-B7PzysnJ.mjs} +1 -1
  35. package/dist/{worker-protocol-NB-CjUQf.js → worker-protocol-wr4XEhUh.js} +1 -1
  36. package/package.json +8 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BjYSKqCM.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-E0nHxt6U.mjs +0 -26
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-M39Js6iu.js");
6
- const require_node = require("../node-Byy6tlFm.js");
7
- const require_local_frame_registry = require("../local-frame-registry-CHUbcz6r.js");
8
- const require_worker_protocol = require("../worker-protocol-NB-CjUQf.js");
5
+ const require_dist = require("../dist-DSJDq5hY.js");
6
+ const require_node = require("../node-DVcGI_Kr.js");
7
+ const require_local_frame_registry = require("../local-frame-registry-Y4TreSiT.js");
8
+ const require_worker_protocol = require("../worker-protocol-wr4XEhUh.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-CO-c2L9_.js");
10
+ const require_lazy_sharp = require("../lazy-sharp-Fre6HHg6.js");
11
11
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
12
12
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
13
13
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
@@ -18,6 +18,51 @@ let node_url = require("node:url");
18
18
  let node_perf_hooks = require("node:perf_hooks");
19
19
  let node_os = require("node:os");
20
20
  node_os = require_dist.__toESM(node_os);
21
+ //#region src/pipeline-runner/first-detection-frame.ts
22
+ /**
23
+ * How long a camera waits, after `attachCamera`, before a decoded frame
24
+ * actually reaches the detection queue.
25
+ *
26
+ * The orchestrator already measures `session motion → attach latency` and it
27
+ * is small — 11, 13 and 18 ms across three consecutive sessions on the same
28
+ * camera. What nothing measured is the NEXT leg: from the attach to the first
29
+ * frame the detector is given. An operator reporting "detection starts late"
30
+ * is describing that leg, and answering them required reading the timestamps
31
+ * of unrelated INFO lines and inferring — which produced a wrong first answer
32
+ * (the absence of a `frame processed` line means the frame produced nothing
33
+ * notable, not that no frame arrived).
34
+ *
35
+ * One number, once per attach, so a slow start is a value rather than an
36
+ * inference.
37
+ */
38
+ /** Pending attach marks, keyed by device. */
39
+ var FirstDetectionFrameTimer = class {
40
+ attachedAt = /* @__PURE__ */ new Map();
41
+ /** A camera attached. Replaces any pending mark: the newest attach is the
42
+ * one the next frame belongs to. */
43
+ markAttached(deviceId, nowMs) {
44
+ this.attachedAt.set(deviceId, nowMs);
45
+ }
46
+ /**
47
+ * A decoded frame was admitted to the detection queue. Returns the elapsed
48
+ * ms since the attach the FIRST time it is called after one, and `null`
49
+ * every time after — so the caller logs once per attach, not once per frame.
50
+ */
51
+ takeLatency(deviceId, nowMs) {
52
+ const at = this.attachedAt.get(deviceId);
53
+ if (at === void 0) return null;
54
+ this.attachedAt.delete(deviceId);
55
+ return Math.max(0, nowMs - at);
56
+ }
57
+ /** A camera detached with no frame ever admitted — drop the mark so the
58
+ * NEXT attach is not measured from this one. Returns whether one was
59
+ * pending, which is itself the interesting fact: a session that ran and
60
+ * fed the detector nothing. */
61
+ forget(deviceId) {
62
+ return this.attachedAt.delete(deviceId);
63
+ }
64
+ };
65
+ //#endregion
21
66
  //#region src/session-decode/fork-decode-worker.ts
22
67
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
23
68
  var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
@@ -6434,6 +6479,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6434
6479
  if (config.motionSources.includes("analyzer")) attachment.motionUnsubscribe = await this.subscribeMotionFrames(config);
6435
6480
  const stepsCount = config.steps?.length ?? 0;
6436
6481
  const dispatch = stepsCount > 0 ? `runPipeline(${stepsCount}step${stepsCount === 1 ? "" : "s"})` : config.steps !== void 0 ? "skip(0steps)" : "runFrame(legacy)";
6482
+ const engineLabel = "node-local";
6483
+ this.firstDetectionFrame.markAttached(config.deviceId, Date.now());
6437
6484
  this.ctx.logger.info("attachCamera", {
6438
6485
  tags: { deviceId: config.deviceId },
6439
6486
  meta: {
@@ -6443,7 +6490,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6443
6490
  detectionFps: config.detectionFps,
6444
6491
  motionSources: config.motionSources,
6445
6492
  dispatch,
6446
- engine: "node-local"
6493
+ engine: engineLabel
6447
6494
  }
6448
6495
  });
6449
6496
  return { success: true };
@@ -7292,10 +7339,20 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7292
7339
  * no second dial, no second decode, no GPU). Mirrors the shm shared-motion
7293
7340
  * seam (`subscribeMotionFrames` `shared` branch).
7294
7341
  */
7342
+ /** Attach → first decoded detection frame, one line per attach. See
7343
+ * {@link FirstDetectionFrameTimer}. */
7344
+ firstDetectionFrame = new FirstDetectionFrameTimer();
7295
7345
  enqueueSharedDetectionFrame(deviceId, frame, handle, reservation) {
7296
7346
  const runner = this.runner;
7297
7347
  if (!runner) return false;
7298
7348
  const admitted = reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
7349
+ if (admitted) {
7350
+ const latencyMs = this.firstDetectionFrame.takeLatency(deviceId, Date.now());
7351
+ if (latencyMs !== null) this.ctx?.logger.info("session-decode: first detection frame after attach", {
7352
+ tags: { deviceId },
7353
+ meta: { latencyMs }
7354
+ });
7355
+ }
7299
7356
  if (reservation?.motion) runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
7300
7357
  else if (!reservation) {
7301
7358
  const sink = this.sharedMotionSinks.get(deviceId);
@@ -1,7 +1,7 @@
1
- import { B as defineCustomActions, Bt as nodePin, Ft as createEvent, Gt as _enum, H as deriveDetailCropRect, Kt as array, L as customAction, Qt as object, X as failureContributionCapability, Xt as literal, Yt as lazy, Zt as number, _ as FailureCounters, en as string, et as loadContributionCapability, ft as pipelineRunnerCapability, jt as BaseAddon, lt as pickDetailCropConvention, m as DEVICE_BACKEND_TO_FORMAT, nn as EventCategory, p as DEFAULT_NATIVE_LEASE_SETTINGS, qt as boolean, u as DEFAULT_DETAIL_CROP_CONVENTION, ut as pickNativeLeaseOverride, wt as errMsg } from "../dist-D0liXHO7.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-CQ6VBSrE.mjs";
3
- import { c as getStepDefinition, n as resolveFrameViewGeometry, t as localFrameRegistry } from "../local-frame-registry--_WjAPa2.mjs";
4
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-CAWMqnxs.mjs";
1
+ import { B as defineCustomActions, Bt as nodePin, Ft as createEvent, Gt as _enum, H as deriveDetailCropRect, Kt as array, L as customAction, Qt as object, X as failureContributionCapability, Xt as literal, Yt as lazy, Zt as number, _ as FailureCounters, en as string, et as loadContributionCapability, ft as pipelineRunnerCapability, jt as BaseAddon, lt as pickDetailCropConvention, m as DEVICE_BACKEND_TO_FORMAT, nn as EventCategory, p as DEFAULT_NATIVE_LEASE_SETTINGS, qt as boolean, u as DEFAULT_DETAIL_CROP_CONVENTION, ut as pickNativeLeaseOverride, wt as errMsg } from "../dist-D35w3ngN.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B2NS7cyi.mjs";
3
+ import { c as getStepDefinition, n as resolveFrameViewGeometry, t as localFrameRegistry } from "../local-frame-registry-AtRvlifj.mjs";
4
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-B7PzysnJ.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
7
7
  import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
@@ -12,6 +12,51 @@ import sharp from "sharp";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { PerformanceObserver } from "node:perf_hooks";
14
14
  import * as os from "node:os";
15
+ //#region src/pipeline-runner/first-detection-frame.ts
16
+ /**
17
+ * How long a camera waits, after `attachCamera`, before a decoded frame
18
+ * actually reaches the detection queue.
19
+ *
20
+ * The orchestrator already measures `session motion → attach latency` and it
21
+ * is small — 11, 13 and 18 ms across three consecutive sessions on the same
22
+ * camera. What nothing measured is the NEXT leg: from the attach to the first
23
+ * frame the detector is given. An operator reporting "detection starts late"
24
+ * is describing that leg, and answering them required reading the timestamps
25
+ * of unrelated INFO lines and inferring — which produced a wrong first answer
26
+ * (the absence of a `frame processed` line means the frame produced nothing
27
+ * notable, not that no frame arrived).
28
+ *
29
+ * One number, once per attach, so a slow start is a value rather than an
30
+ * inference.
31
+ */
32
+ /** Pending attach marks, keyed by device. */
33
+ var FirstDetectionFrameTimer = class {
34
+ attachedAt = /* @__PURE__ */ new Map();
35
+ /** A camera attached. Replaces any pending mark: the newest attach is the
36
+ * one the next frame belongs to. */
37
+ markAttached(deviceId, nowMs) {
38
+ this.attachedAt.set(deviceId, nowMs);
39
+ }
40
+ /**
41
+ * A decoded frame was admitted to the detection queue. Returns the elapsed
42
+ * ms since the attach the FIRST time it is called after one, and `null`
43
+ * every time after — so the caller logs once per attach, not once per frame.
44
+ */
45
+ takeLatency(deviceId, nowMs) {
46
+ const at = this.attachedAt.get(deviceId);
47
+ if (at === void 0) return null;
48
+ this.attachedAt.delete(deviceId);
49
+ return Math.max(0, nowMs - at);
50
+ }
51
+ /** A camera detached with no frame ever admitted — drop the mark so the
52
+ * NEXT attach is not measured from this one. Returns whether one was
53
+ * pending, which is itself the interesting fact: a session that ran and
54
+ * fed the detector nothing. */
55
+ forget(deviceId) {
56
+ return this.attachedAt.delete(deviceId);
57
+ }
58
+ };
59
+ //#endregion
15
60
  //#region src/session-decode/fork-decode-worker.ts
16
61
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
17
62
  var CHILD_ENTRY_PATH = fileURLToPath(new URL("../session-decode/decode-worker-child.js", import.meta.url));
@@ -6427,6 +6472,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
6427
6472
  if (config.motionSources.includes("analyzer")) attachment.motionUnsubscribe = await this.subscribeMotionFrames(config);
6428
6473
  const stepsCount = config.steps?.length ?? 0;
6429
6474
  const dispatch = stepsCount > 0 ? `runPipeline(${stepsCount}step${stepsCount === 1 ? "" : "s"})` : config.steps !== void 0 ? "skip(0steps)" : "runFrame(legacy)";
6475
+ const engineLabel = "node-local";
6476
+ this.firstDetectionFrame.markAttached(config.deviceId, Date.now());
6430
6477
  this.ctx.logger.info("attachCamera", {
6431
6478
  tags: { deviceId: config.deviceId },
6432
6479
  meta: {
@@ -6436,7 +6483,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
6436
6483
  detectionFps: config.detectionFps,
6437
6484
  motionSources: config.motionSources,
6438
6485
  dispatch,
6439
- engine: "node-local"
6486
+ engine: engineLabel
6440
6487
  }
6441
6488
  });
6442
6489
  return { success: true };
@@ -7285,10 +7332,20 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7285
7332
  * no second dial, no second decode, no GPU). Mirrors the shm shared-motion
7286
7333
  * seam (`subscribeMotionFrames` `shared` branch).
7287
7334
  */
7335
+ /** Attach → first decoded detection frame, one line per attach. See
7336
+ * {@link FirstDetectionFrameTimer}. */
7337
+ firstDetectionFrame = new FirstDetectionFrameTimer();
7288
7338
  enqueueSharedDetectionFrame(deviceId, frame, handle, reservation) {
7289
7339
  const runner = this.runner;
7290
7340
  if (!runner) return false;
7291
7341
  const admitted = reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
7342
+ if (admitted) {
7343
+ const latencyMs = this.firstDetectionFrame.takeLatency(deviceId, Date.now());
7344
+ if (latencyMs !== null) this.ctx?.logger.info("session-decode: first detection frame after attach", {
7345
+ tags: { deviceId },
7346
+ meta: { latencyMs }
7347
+ });
7348
+ }
7292
7349
  if (reservation?.motion) runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
7293
7350
  else if (!reservation) {
7294
7351
  const sink = this.sharedMotionSinks.get(deviceId);
@@ -1,4 +1,4 @@
1
- import { ot as parseProcStatus } from "./dist-D0liXHO7.mjs";
1
+ import { ot as parseProcStatus } from "./dist-D35w3ngN.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-M39Js6iu.js");
1
+ const require_dist = require("./dist-DSJDq5hY.js");
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_dist.__toESM(node_fs);
4
4
  let node_child_process = require("node:child_process");
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-M39Js6iu.js");
6
- const require_node = require("../node-Byy6tlFm.js");
5
+ const require_dist = require("../dist-DSJDq5hY.js");
6
+ const require_node = require("../node-DVcGI_Kr.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
9
9
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
@@ -5077,6 +5077,48 @@ function buildExportArgs(input) {
5077
5077
  * queue behind each other.
5078
5078
  */
5079
5079
  /**
5080
+ * The storage class export MP4s live in: `artifacts`, SHARED with the
5081
+ * notification-center's artifact shelf (operator 2026-09-04: one generic
5082
+ * class, not one per producer — D341), a declared SINGLETON. Never the addon
5083
+ * data dir: 8.4 GB of operator-exported clips sat in
5084
+ * `addons-data/recorder/exports`, inside every system backup, invisible to
5085
+ * occupancy and relocation, on the constrained appdata disk. And never a
5086
+ * `recordings`-class subtree: footage classes are quota-capped and swept, and
5087
+ * an export is something the operator DELIBERATELY created — its only
5088
+ * retention is its own (`expiresAt` chosen at creation + delete-after-
5089
+ * download), enforced by the export janitor and nothing else. Sharing the
5090
+ * LOCATION with the notification shelf never means sharing a SWEEP: each
5091
+ * tenant walks only its own subtree.
5092
+ *
5093
+ * Declared by BOTH consuming addons with identical cardinality — the
5094
+ * registry throws at kernel boot on a conflict, and each addon's pinning
5095
+ * spec holds its side of the agreement.
5096
+ */
5097
+ var ARTIFACTS_MEDIA_LOCATION = "artifacts";
5098
+ /**
5099
+ * `<root>/<deviceId>/exports/` — the exports TENANT subtree, DEVICE-FIRST
5100
+ * like the notification shelf's `<root>/<deviceId>/nc-artifacts/…` (operator
5101
+ * 2026-09-04: one directory per camera answers "what does this camera hold",
5102
+ * and per-device occupancy is uniform across both tenants). The ONE
5103
+ * derivation of the tenant dir — export MP4s, source playlists and the
5104
+ * ffconcat sidecar all live here; nothing may join the path a second way.
5105
+ */
5106
+ function exportTenantDirFor(root, deviceId) {
5107
+ return node_path.default.join(root, String(deviceId), "exports");
5108
+ }
5109
+ /**
5110
+ * `<root>/<deviceId>/exports/<exportId>.mp4` — the ONE derivation of an
5111
+ * export's file path, via {@link exportTenantDirFor}. Rows store no path, so
5112
+ * repointing the class root moves the whole library while every row keeps
5113
+ * resolving. A `null` root (location not yet resolved, or unresolvable)
5114
+ * REFUSES: falling back to the addon data dir is the failure mode this class
5115
+ * exists to end (D341).
5116
+ */
5117
+ function exportOutPathFor(root, deviceId, exportId) {
5118
+ if (root === null) throw new Error("artifacts storage location is not resolved — exports refuse rather than fall back to the addon data dir (D341)");
5119
+ return node_path.default.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
5120
+ }
5121
+ /**
5080
5122
  * @durable class=ledger owner=recorder
5081
5123
  * write="one row per export job, inserted by `createExport` and rewritten by the
5082
5124
  * render engine's progress/state patches and by the download hook"
@@ -7404,18 +7446,21 @@ function buildRecordingProvider(deps) {
7404
7446
  */
7405
7447
  /**
7406
7448
  * Absolute path of ONE export's source playlist:
7407
- * `<exportsDir>/<deviceId>/<exportId>.src.m3u8`. Unique per export id, so two
7408
- * concurrent renders (and any concurrent playback request) never share a file.
7449
+ * `<root>/<deviceId>/exports/<exportId>.src.m3u8` — inside the exports
7450
+ * TENANT subtree of the shared `artifacts` class root, derived through
7451
+ * {@link exportTenantDirFor} so no caller can join the path a second way.
7452
+ * Unique per export id, so two concurrent renders (and any concurrent
7453
+ * playback request) never share a file.
7409
7454
  */
7410
7455
  function exportSourcePlaylistPath(exportsDir, deviceId, exportId) {
7411
- return node_path.default.join(exportsDir, String(deviceId), `${exportId}.src.m3u8`);
7456
+ return node_path.default.join(exportTenantDirFor(exportsDir, deviceId), `${exportId}.src.m3u8`);
7412
7457
  }
7413
7458
  /**
7414
7459
  * Absolute path of the same export's FFCONCAT list — the timelapse pass-1
7415
7460
  * input (`buildConcatPassArgs`). Sibling of the m3u8, same uniqueness rules.
7416
7461
  */
7417
7462
  function exportSourceConcatPath(exportsDir, deviceId, exportId) {
7418
- return node_path.default.join(exportsDir, String(deviceId), `${exportId}.src.ffconcat`);
7463
+ return node_path.default.join(exportTenantDirFor(exportsDir, deviceId), `${exportId}.src.ffconcat`);
7419
7464
  }
7420
7465
  /**
7421
7466
  * Write the export's source playlist and return it, or null when no segment of
@@ -16377,6 +16422,14 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16377
16422
  * Every other node stays fully inert — registers no providers, serves no
16378
16423
  * playback, runs no controller. Resolved in `onInitialize`. */
16379
16424
  isRecordingNode = true;
16425
+ /**
16426
+ * Resolved root of the shared `artifacts` storage location — the export
16427
+ * MP4s' home and the GIF/clip render scratch. `null` until `onHubReachable`
16428
+ * resolves it (and forever if the location is unreachable): every consumer
16429
+ * then REFUSES with a named error instead of falling back to the addon data
16430
+ * dir (D341).
16431
+ */
16432
+ exportsRoot = null;
16380
16433
  index = new RecordingIndex();
16381
16434
  /**
16382
16435
  * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
@@ -16792,12 +16845,12 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16792
16845
  relocate: relocateEngine,
16793
16846
  gifRender: (input) => renderGif({
16794
16847
  deps: recordingDeps,
16795
- exportsDir: node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX),
16848
+ exportsDir: this.exportsDirOrThrow(),
16796
16849
  spawn: node_child_process.spawn
16797
16850
  }, input),
16798
16851
  clipRender: (input) => renderClip({
16799
16852
  deps: recordingDeps,
16800
- exportsDir: node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX),
16853
+ exportsDir: this.exportsDirOrThrow(),
16801
16854
  spawn: node_child_process.spawn
16802
16855
  }, input),
16803
16856
  locations: () => this.resolvedLocations,
@@ -16912,8 +16965,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16912
16965
  }
16913
16966
  });
16914
16967
  const exportsState = this.exportsStore();
16915
- const exportsDir = node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX);
16916
- const exportOutPath = (rec) => node_path.default.join(exportsDir, String(rec.deviceId), `${rec.id}.mp4`);
16968
+ const exportOutPath = (rec) => exportOutPathFor(this.exportsRoot, rec.deviceId, rec.id);
16917
16969
  const removeExportFile = async (p) => {
16918
16970
  await node_fs.promises.rm(p, { force: true });
16919
16971
  };
@@ -16925,7 +16977,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16925
16977
  await node_fs.promises.mkdir(node_path.default.dirname(exportOutPath(rec)), { recursive: true });
16926
16978
  return writeExportSourcePlaylist({
16927
16979
  deps: recordingDeps,
16928
- exportsDir,
16980
+ exportsDir: this.exportsDirOrThrow(),
16929
16981
  exportId: rec.id,
16930
16982
  deviceId: rec.deviceId,
16931
16983
  profile: rec.profile,
@@ -17160,6 +17212,18 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17160
17212
  async onHubReachable() {
17161
17213
  if (!this.isRecordingNode) return;
17162
17214
  await this.refreshLocations();
17215
+ try {
17216
+ this.exportsRoot = await this.ctx.api.storage.resolve.query({
17217
+ location: ARTIFACTS_MEDIA_LOCATION,
17218
+ relativePath: ""
17219
+ });
17220
+ } catch (err) {
17221
+ this.ctx.logger.error("artifacts storage location is NOT available — exports, GIF and clip renders will REFUSE until it is", { meta: {
17222
+ location: ARTIFACTS_MEDIA_LOCATION,
17223
+ error: require_dist.errMsg(err),
17224
+ hint: "the declaration is seeded when a storage-provider registers; restart this node so the manifest is re-read, or create an artifacts location. Refusing is deliberate — the addon data dir carries no media (D341)."
17225
+ } });
17226
+ }
17163
17227
  if (this.resolvedLocations.length === 0) {
17164
17228
  this.ctx.logger.warn("recorder: no recordings storage locations resolved — index not hydrated");
17165
17229
  return;
@@ -17393,6 +17457,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17393
17457
  async bootstrapExports() {
17394
17458
  const engine = this.exportEngine;
17395
17459
  if (!engine) return;
17460
+ if (this.exportsRoot === null) {
17461
+ this.ctx.logger.warn("recorder: exports bootstrap skipped — artifacts location unresolved");
17462
+ return;
17463
+ }
17396
17464
  try {
17397
17465
  await reconcileInterruptedExports({
17398
17466
  state: this.exportsStore(),
@@ -17947,11 +18015,21 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17947
18015
  placementStore() {
17948
18016
  return this.state(RECORDING_PLACEMENT_KEY, RecordingPlacementBlobSchema, EMPTY_PLACEMENT);
17949
18017
  }
18018
+ /** The artifacts class ROOT, or a NAMED refusal — never an invented path.
18019
+ * Every export-side path (MP4s, playlists, render scratch) derives from it
18020
+ * through `exportTenantDirFor` → `<root>/<deviceId>/exports/…`, beside
18021
+ * (never inside) the notification shelf's `<root>/<deviceId>/nc-artifacts/…`
18022
+ * sibling (D341). No consumer joins the path a second way. */
18023
+ exportsDirOrThrow() {
18024
+ const root = this.exportsRoot;
18025
+ if (root === null) throw new Error("artifacts storage location is not resolved — exports and footage renders refuse rather than fall back to the addon data dir (D341)");
18026
+ return root;
18027
+ }
17950
18028
  /** Retention deps shared by the sweep and the download-complete hook. */
17951
18029
  exportJanitorDeps() {
17952
18030
  return {
17953
18031
  state: this.exportsStore(),
17954
- outPath: (rec) => node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX, String(rec.deviceId), `${rec.id}.mp4`),
18032
+ outPath: (rec) => exportOutPathFor(this.exportsRoot, rec.deviceId, rec.id),
17955
18033
  removeFile: async (p) => {
17956
18034
  await node_fs.promises.rm(p, { force: true });
17957
18035
  }
@@ -18007,7 +18085,13 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
18007
18085
  res.writeHead(404).end();
18008
18086
  return;
18009
18087
  }
18010
- const absPath = outPath(rec);
18088
+ let absPath;
18089
+ try {
18090
+ absPath = outPath(rec);
18091
+ } catch {
18092
+ res.writeHead(503).end();
18093
+ return;
18094
+ }
18011
18095
  let size;
18012
18096
  try {
18013
18097
  const st = await node_fs.promises.stat(absPath);
@@ -1,5 +1,5 @@
1
- import { $ as isFirstLevelMacroClass, $t as record, B as defineCustomActions, Bt as nodePin, E as RecordingConfigSchema, Gt as _enum, It as hydrateSchema, J as evaluateSensorEdge, Kt as array, L as customAction, N as batteryCapability, Nt as DeviceFeature, Pt as DeviceType, Qt as object, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, Ut as selectAssignedProfileSlots, V as deriveBatteryPresence, Wt as sleep$1, Zt as number, bt as storageEvictableCapability, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as string, et as loadContributionCapability, g as ExportRecordSchema, i as BatteryStatusSchema, jt as BaseAddon, mt as recordingExportCapability, nn as EventCategory, pt as recordingCapability, qt as boolean, vt as resolveRecordingProfiles, wt as errMsg, x as OpsLogEntrySchema } from "../dist-D0liXHO7.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-CQ6VBSrE.mjs";
1
+ import { $ as isFirstLevelMacroClass, $t as record, B as defineCustomActions, Bt as nodePin, E as RecordingConfigSchema, Gt as _enum, It as hydrateSchema, J as evaluateSensorEdge, Kt as array, L as customAction, N as batteryCapability, Nt as DeviceFeature, Pt as DeviceType, Qt as object, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, Ut as selectAssignedProfileSlots, V as deriveBatteryPresence, Wt as sleep$1, Zt as number, bt as storageEvictableCapability, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as string, et as loadContributionCapability, g as ExportRecordSchema, i as BatteryStatusSchema, jt as BaseAddon, mt as recordingExportCapability, nn as EventCategory, pt as recordingCapability, qt as boolean, vt as resolveRecordingProfiles, wt as errMsg, x as OpsLogEntrySchema } from "../dist-D35w3ngN.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B2NS7cyi.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
5
5
  import { n as startEventLoopStallMonitor } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
@@ -5071,6 +5071,48 @@ function buildExportArgs(input) {
5071
5071
  * queue behind each other.
5072
5072
  */
5073
5073
  /**
5074
+ * The storage class export MP4s live in: `artifacts`, SHARED with the
5075
+ * notification-center's artifact shelf (operator 2026-09-04: one generic
5076
+ * class, not one per producer — D341), a declared SINGLETON. Never the addon
5077
+ * data dir: 8.4 GB of operator-exported clips sat in
5078
+ * `addons-data/recorder/exports`, inside every system backup, invisible to
5079
+ * occupancy and relocation, on the constrained appdata disk. And never a
5080
+ * `recordings`-class subtree: footage classes are quota-capped and swept, and
5081
+ * an export is something the operator DELIBERATELY created — its only
5082
+ * retention is its own (`expiresAt` chosen at creation + delete-after-
5083
+ * download), enforced by the export janitor and nothing else. Sharing the
5084
+ * LOCATION with the notification shelf never means sharing a SWEEP: each
5085
+ * tenant walks only its own subtree.
5086
+ *
5087
+ * Declared by BOTH consuming addons with identical cardinality — the
5088
+ * registry throws at kernel boot on a conflict, and each addon's pinning
5089
+ * spec holds its side of the agreement.
5090
+ */
5091
+ var ARTIFACTS_MEDIA_LOCATION = "artifacts";
5092
+ /**
5093
+ * `<root>/<deviceId>/exports/` — the exports TENANT subtree, DEVICE-FIRST
5094
+ * like the notification shelf's `<root>/<deviceId>/nc-artifacts/…` (operator
5095
+ * 2026-09-04: one directory per camera answers "what does this camera hold",
5096
+ * and per-device occupancy is uniform across both tenants). The ONE
5097
+ * derivation of the tenant dir — export MP4s, source playlists and the
5098
+ * ffconcat sidecar all live here; nothing may join the path a second way.
5099
+ */
5100
+ function exportTenantDirFor(root, deviceId) {
5101
+ return path.join(root, String(deviceId), "exports");
5102
+ }
5103
+ /**
5104
+ * `<root>/<deviceId>/exports/<exportId>.mp4` — the ONE derivation of an
5105
+ * export's file path, via {@link exportTenantDirFor}. Rows store no path, so
5106
+ * repointing the class root moves the whole library while every row keeps
5107
+ * resolving. A `null` root (location not yet resolved, or unresolvable)
5108
+ * REFUSES: falling back to the addon data dir is the failure mode this class
5109
+ * exists to end (D341).
5110
+ */
5111
+ function exportOutPathFor(root, deviceId, exportId) {
5112
+ if (root === null) throw new Error("artifacts storage location is not resolved — exports refuse rather than fall back to the addon data dir (D341)");
5113
+ return path.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
5114
+ }
5115
+ /**
5074
5116
  * @durable class=ledger owner=recorder
5075
5117
  * write="one row per export job, inserted by `createExport` and rewritten by the
5076
5118
  * render engine's progress/state patches and by the download hook"
@@ -7398,18 +7440,21 @@ function buildRecordingProvider(deps) {
7398
7440
  */
7399
7441
  /**
7400
7442
  * Absolute path of ONE export's source playlist:
7401
- * `<exportsDir>/<deviceId>/<exportId>.src.m3u8`. Unique per export id, so two
7402
- * concurrent renders (and any concurrent playback request) never share a file.
7443
+ * `<root>/<deviceId>/exports/<exportId>.src.m3u8` — inside the exports
7444
+ * TENANT subtree of the shared `artifacts` class root, derived through
7445
+ * {@link exportTenantDirFor} so no caller can join the path a second way.
7446
+ * Unique per export id, so two concurrent renders (and any concurrent
7447
+ * playback request) never share a file.
7403
7448
  */
7404
7449
  function exportSourcePlaylistPath(exportsDir, deviceId, exportId) {
7405
- return path.join(exportsDir, String(deviceId), `${exportId}.src.m3u8`);
7450
+ return path.join(exportTenantDirFor(exportsDir, deviceId), `${exportId}.src.m3u8`);
7406
7451
  }
7407
7452
  /**
7408
7453
  * Absolute path of the same export's FFCONCAT list — the timelapse pass-1
7409
7454
  * input (`buildConcatPassArgs`). Sibling of the m3u8, same uniqueness rules.
7410
7455
  */
7411
7456
  function exportSourceConcatPath(exportsDir, deviceId, exportId) {
7412
- return path.join(exportsDir, String(deviceId), `${exportId}.src.ffconcat`);
7457
+ return path.join(exportTenantDirFor(exportsDir, deviceId), `${exportId}.src.ffconcat`);
7413
7458
  }
7414
7459
  /**
7415
7460
  * Write the export's source playlist and return it, or null when no segment of
@@ -16371,6 +16416,14 @@ var RecorderV2Addon = class extends BaseAddon {
16371
16416
  * Every other node stays fully inert — registers no providers, serves no
16372
16417
  * playback, runs no controller. Resolved in `onInitialize`. */
16373
16418
  isRecordingNode = true;
16419
+ /**
16420
+ * Resolved root of the shared `artifacts` storage location — the export
16421
+ * MP4s' home and the GIF/clip render scratch. `null` until `onHubReachable`
16422
+ * resolves it (and forever if the location is unreachable): every consumer
16423
+ * then REFUSES with a named error instead of falling back to the addon data
16424
+ * dir (D341).
16425
+ */
16426
+ exportsRoot = null;
16374
16427
  index = new RecordingIndex();
16375
16428
  /**
16376
16429
  * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
@@ -16786,12 +16839,12 @@ var RecorderV2Addon = class extends BaseAddon {
16786
16839
  relocate: relocateEngine,
16787
16840
  gifRender: (input) => renderGif({
16788
16841
  deps: recordingDeps,
16789
- exportsDir: path.join(this.ctx.dataDir, EXPORTS_PREFIX),
16842
+ exportsDir: this.exportsDirOrThrow(),
16790
16843
  spawn
16791
16844
  }, input),
16792
16845
  clipRender: (input) => renderClip({
16793
16846
  deps: recordingDeps,
16794
- exportsDir: path.join(this.ctx.dataDir, EXPORTS_PREFIX),
16847
+ exportsDir: this.exportsDirOrThrow(),
16795
16848
  spawn
16796
16849
  }, input),
16797
16850
  locations: () => this.resolvedLocations,
@@ -16906,8 +16959,7 @@ var RecorderV2Addon = class extends BaseAddon {
16906
16959
  }
16907
16960
  });
16908
16961
  const exportsState = this.exportsStore();
16909
- const exportsDir = path.join(this.ctx.dataDir, EXPORTS_PREFIX);
16910
- const exportOutPath = (rec) => path.join(exportsDir, String(rec.deviceId), `${rec.id}.mp4`);
16962
+ const exportOutPath = (rec) => exportOutPathFor(this.exportsRoot, rec.deviceId, rec.id);
16911
16963
  const removeExportFile = async (p) => {
16912
16964
  await promises.rm(p, { force: true });
16913
16965
  };
@@ -16919,7 +16971,7 @@ var RecorderV2Addon = class extends BaseAddon {
16919
16971
  await promises.mkdir(path.dirname(exportOutPath(rec)), { recursive: true });
16920
16972
  return writeExportSourcePlaylist({
16921
16973
  deps: recordingDeps,
16922
- exportsDir,
16974
+ exportsDir: this.exportsDirOrThrow(),
16923
16975
  exportId: rec.id,
16924
16976
  deviceId: rec.deviceId,
16925
16977
  profile: rec.profile,
@@ -17154,6 +17206,18 @@ var RecorderV2Addon = class extends BaseAddon {
17154
17206
  async onHubReachable() {
17155
17207
  if (!this.isRecordingNode) return;
17156
17208
  await this.refreshLocations();
17209
+ try {
17210
+ this.exportsRoot = await this.ctx.api.storage.resolve.query({
17211
+ location: ARTIFACTS_MEDIA_LOCATION,
17212
+ relativePath: ""
17213
+ });
17214
+ } catch (err) {
17215
+ this.ctx.logger.error("artifacts storage location is NOT available — exports, GIF and clip renders will REFUSE until it is", { meta: {
17216
+ location: ARTIFACTS_MEDIA_LOCATION,
17217
+ error: errMsg(err),
17218
+ hint: "the declaration is seeded when a storage-provider registers; restart this node so the manifest is re-read, or create an artifacts location. Refusing is deliberate — the addon data dir carries no media (D341)."
17219
+ } });
17220
+ }
17157
17221
  if (this.resolvedLocations.length === 0) {
17158
17222
  this.ctx.logger.warn("recorder: no recordings storage locations resolved — index not hydrated");
17159
17223
  return;
@@ -17387,6 +17451,10 @@ var RecorderV2Addon = class extends BaseAddon {
17387
17451
  async bootstrapExports() {
17388
17452
  const engine = this.exportEngine;
17389
17453
  if (!engine) return;
17454
+ if (this.exportsRoot === null) {
17455
+ this.ctx.logger.warn("recorder: exports bootstrap skipped — artifacts location unresolved");
17456
+ return;
17457
+ }
17390
17458
  try {
17391
17459
  await reconcileInterruptedExports({
17392
17460
  state: this.exportsStore(),
@@ -17941,11 +18009,21 @@ var RecorderV2Addon = class extends BaseAddon {
17941
18009
  placementStore() {
17942
18010
  return this.state(RECORDING_PLACEMENT_KEY, RecordingPlacementBlobSchema, EMPTY_PLACEMENT);
17943
18011
  }
18012
+ /** The artifacts class ROOT, or a NAMED refusal — never an invented path.
18013
+ * Every export-side path (MP4s, playlists, render scratch) derives from it
18014
+ * through `exportTenantDirFor` → `<root>/<deviceId>/exports/…`, beside
18015
+ * (never inside) the notification shelf's `<root>/<deviceId>/nc-artifacts/…`
18016
+ * sibling (D341). No consumer joins the path a second way. */
18017
+ exportsDirOrThrow() {
18018
+ const root = this.exportsRoot;
18019
+ if (root === null) throw new Error("artifacts storage location is not resolved — exports and footage renders refuse rather than fall back to the addon data dir (D341)");
18020
+ return root;
18021
+ }
17944
18022
  /** Retention deps shared by the sweep and the download-complete hook. */
17945
18023
  exportJanitorDeps() {
17946
18024
  return {
17947
18025
  state: this.exportsStore(),
17948
- outPath: (rec) => path.join(this.ctx.dataDir, EXPORTS_PREFIX, String(rec.deviceId), `${rec.id}.mp4`),
18026
+ outPath: (rec) => exportOutPathFor(this.exportsRoot, rec.deviceId, rec.id),
17949
18027
  removeFile: async (p) => {
17950
18028
  await promises.rm(p, { force: true });
17951
18029
  }
@@ -18001,7 +18079,13 @@ var RecorderV2Addon = class extends BaseAddon {
18001
18079
  res.writeHead(404).end();
18002
18080
  return;
18003
18081
  }
18004
- const absPath = outPath(rec);
18082
+ let absPath;
18083
+ try {
18084
+ absPath = outPath(rec);
18085
+ } catch {
18086
+ res.writeHead(503).end();
18087
+ return;
18088
+ }
18005
18089
  let size;
18006
18090
  try {
18007
18091
  const st = await promises.stat(absPath);
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-M39Js6iu.js");
1
+ const require_dist = require("./dist-DSJDq5hY.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
4
4
  var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { wt as errMsg } from "./dist-D0liXHO7.mjs";
2
+ import { wt as errMsg } from "./dist-D35w3ngN.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-NB-CjUQf.js");
3
- const require_lazy_sharp = require("../lazy-sharp-CO-c2L9_.js");
2
+ const require_worker_protocol = require("../worker-protocol-wr4XEhUh.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-Fre6HHg6.js");
4
4
  //#region src/session-decode/color-conversion.ts
5
5
  /**
6
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -1,4 +1,4 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-CAWMqnxs.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-B7PzysnJ.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**