@camstack/addon-pipeline 1.2.81 → 1.2.83

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 (26) hide show
  1. package/dist/{addon-utils-CQFlZXSn.js → addon-utils-WtpcVuzP.js} +1 -1
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/detection-pipeline/index.js +4 -4
  5. package/dist/detection-pipeline/index.mjs +2 -2
  6. package/dist/{dist-DGp0dRiq.mjs → dist-CJI_8GVY.mjs} +10 -1
  7. package/dist/{dist-B57MztMq.js → dist-CYxusT1M.js} +10 -1
  8. package/dist/{event-loop-stall-monitor-DPA6Pu_h.js → event-loop-stall-monitor-CHvvXkQS.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-8vYH44kg.mjs → event-loop-stall-monitor-D3FjNBpW.mjs} +1 -1
  10. package/dist/{lazy-sharp-BMgRSV9x.js → lazy-sharp-CTNP_35A.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/recorder/index.js +54 -28
  16. package/dist/recorder/index.mjs +53 -27
  17. package/dist/session-decode/decode-worker-child.js +2 -2
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cb8ynph1.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DKf1PFZt.mjs} +1 -1
  20. package/dist/stream-broker/{hostInit-GJipMxBe.mjs → hostInit-CP9inP-0.mjs} +1 -1
  21. package/dist/stream-broker/index.js +104 -65
  22. package/dist/stream-broker/index.mjs +104 -65
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/dist/{worker-protocol-4L4SbSAo.js → worker-protocol-34kViYAj.js} +1 -1
  25. package/dist/{worker-protocol-_e84lGuR.mjs → worker-protocol-ZYA4xGRg.mjs} +1 -1
  26. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-B57MztMq.js");
1
+ const require_dist = require("./dist-CYxusT1M.js");
2
2
  require("node:crypto");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_dist.__toESM(node_path, 1);
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B57MztMq.js");
5
+ const require_dist = require("../dist-CYxusT1M.js");
6
6
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
7
- const require_addon_utils = require("../addon-utils-CQFlZXSn.js");
7
+ const require_addon_utils = require("../addon-utils-WtpcVuzP.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_dist.__toESM(node_path);
10
10
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { D as audioAnalyzerCapability, E as audioAnalysisCapability, U as mapAudioLabelToMacro, h as HF_BASE_URL, ht as hydrateSchema, it as errMsg, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, ut as BaseAddon } from "../dist-DGp0dRiq.mjs";
2
+ import { D as audioAnalyzerCapability, E as audioAnalysisCapability, U as mapAudioLabelToMacro, h as HF_BASE_URL, ht as hydrateSchema, it as errMsg, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, ut as BaseAddon } from "../dist-CJI_8GVY.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { i as downloadFile } from "../addon-utils-A2S9D7pu.mjs";
5
5
  import * as path$1 from "node:path";
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B57MztMq.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DPA6Pu_h.js");
7
- const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.js");
5
+ const require_dist = require("../dist-CYxusT1M.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-CHvvXkQS.js");
7
+ const require_lazy_sharp = require("../lazy-sharp-CTNP_35A.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
- const require_addon_utils = require("../addon-utils-CQFlZXSn.js");
9
+ const require_addon_utils = require("../addon-utils-WtpcVuzP.js");
10
10
  let sharp = require("sharp");
11
11
  sharp = require_dist.__toESM(sharp);
12
12
  let node_child_process = require("node:child_process");
@@ -1,6 +1,6 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as runtimeDevices$1, B as enumerateInferenceDevices, Ct as sleep, Ft as EventCategory, J as pipelineExecutorCapability, L as detectionPipelineCapability, M as defaultDeviceFor$1, Nt as string, Pt as union, Tt as array, V as evaluateZoneRules, bt as parseJsonUnknown, d as DEVICE_BACKEND_TO_FORMAT, ht as hydrateSchema, it as errMsg, jt as object, mt as createEvent, nt as supportedRuntimes$1, t as APPLE_SA_TO_MACRO, ut as BaseAddon, w as YAMNET_TO_MACRO, yt as nodePin } from "../dist-DGp0dRiq.mjs";
3
- import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-8vYH44kg.mjs";
2
+ import { $ as runtimeDevices$1, B as enumerateInferenceDevices, Ct as sleep, Ft as EventCategory, J as pipelineExecutorCapability, L as detectionPipelineCapability, M as defaultDeviceFor$1, Nt as string, Pt as union, Tt as array, V as evaluateZoneRules, bt as parseJsonUnknown, d as DEVICE_BACKEND_TO_FORMAT, ht as hydrateSchema, it as errMsg, jt as object, mt as createEvent, nt as supportedRuntimes$1, t as APPLE_SA_TO_MACRO, ut as BaseAddon, w as YAMNET_TO_MACRO, yt as nodePin } from "../dist-CJI_8GVY.mjs";
3
+ import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-D3FjNBpW.mjs";
4
4
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
5
5
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
6
6
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-A2S9D7pu.mjs";
@@ -21166,7 +21166,16 @@ targets: array(object({
21166
21166
  /** A sleeping battery camera: the frame is deliberately stale and will
21167
21167
  * NOT refresh in the background. A surface should say so rather than
21168
21168
  * present it as current. */
21169
- sleeping: boolean()
21169
+ sleeping: boolean(),
21170
+ /** Current device state rendered over the cached frame. State images
21171
+ * remain authoritative even when their photographic background is
21172
+ * old; null means the link must carry a current camera frame. */
21173
+ stateReason: _enum([
21174
+ "disabled",
21175
+ "sleeping",
21176
+ "unreachable",
21177
+ "waking"
21178
+ ]).nullable()
21170
21179
  })));
21171
21180
  /**
21172
21181
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -21189,7 +21189,16 @@ targets: array(object({
21189
21189
  /** A sleeping battery camera: the frame is deliberately stale and will
21190
21190
  * NOT refresh in the background. A surface should say so rather than
21191
21191
  * present it as current. */
21192
- sleeping: boolean()
21192
+ sleeping: boolean(),
21193
+ /** Current device state rendered over the cached frame. State images
21194
+ * remain authoritative even when their photographic background is
21195
+ * old; null means the link must carry a current camera frame. */
21196
+ stateReason: _enum([
21197
+ "disabled",
21198
+ "sleeping",
21199
+ "unreachable",
21200
+ "waking"
21201
+ ]).nullable()
21193
21202
  })));
21194
21203
  /**
21195
21204
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-B57MztMq.js");
1
+ const require_dist = require("./dist-CYxusT1M.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- import { H as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-DGp0dRiq.mjs";
1
+ import { H as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-CJI_8GVY.mjs";
2
2
  import { PerformanceObserver } from "node:perf_hooks";
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-B57MztMq.js");
1
+ const require_dist = require("./dist-CYxusT1M.js");
2
2
  let node_url = require("node:url");
3
3
  let node_module = require("node:module");
4
4
  let node_path = require("node:path");
@@ -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-B57MztMq.js");
6
- const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.js");
5
+ const require_dist = require("../dist-CYxusT1M.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-CTNP_35A.js");
7
7
  let node_path = require("node:path");
8
8
  let node_fs = require("node:fs");
9
9
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { G as motionDetectionCapability, V as evaluateZoneRules, ht as hydrateSchema, pt as DeviceType, ut as BaseAddon } from "../dist-DGp0dRiq.mjs";
1
+ import { G as motionDetectionCapability, V as evaluateZoneRules, ht as hydrateSchema, pt as DeviceType, ut as BaseAddon } from "../dist-CJI_8GVY.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
3
3
  import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B57MztMq.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DPA6Pu_h.js");
7
- const require_worker_protocol = require("../worker-protocol-4L4SbSAo.js");
5
+ const require_dist = require("../dist-CYxusT1M.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-CHvvXkQS.js");
7
+ const require_worker_protocol = require("../worker-protocol-34kViYAj.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
- const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.js");
9
+ const require_lazy_sharp = require("../lazy-sharp-CTNP_35A.js");
10
10
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
11
11
  let sharp = require("sharp");
12
12
  sharp = require_dist.__toESM(sharp);
@@ -1,6 +1,6 @@
1
- import { At as number, Et as boolean, F as deriveDetailCropRect, Ft as EventCategory, K as pickDetailCropConvention, N as defineCustomActions, Nt as string, Ot as lazy, Tt as array, Y as pipelineRunnerCapability, c as DEFAULT_DETAIL_CROP_CONVENTION, d as DEVICE_BACKEND_TO_FORMAT, it as errMsg, j as customAction, jt as object, kt as literal, mt as createEvent, q as pickNativeLeaseOverride, ut as BaseAddon, wt as _enum, yt as nodePin } from "../dist-DGp0dRiq.mjs";
2
- import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-8vYH44kg.mjs";
3
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-_e84lGuR.mjs";
1
+ import { At as number, Et as boolean, F as deriveDetailCropRect, Ft as EventCategory, K as pickDetailCropConvention, N as defineCustomActions, Nt as string, Ot as lazy, Tt as array, Y as pipelineRunnerCapability, c as DEFAULT_DETAIL_CROP_CONVENTION, d as DEVICE_BACKEND_TO_FORMAT, it as errMsg, j as customAction, jt as object, kt as literal, mt as createEvent, q as pickNativeLeaseOverride, ut as BaseAddon, wt as _enum, yt as nodePin } from "../dist-CJI_8GVY.mjs";
2
+ import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-D3FjNBpW.mjs";
3
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-ZYA4xGRg.mjs";
4
4
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
6
6
  import { i as withDetectionIntent, n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-Ci7RXNGb.mjs";
@@ -1,6 +1,6 @@
1
- const require_dist = require("../dist-B57MztMq.js");
1
+ const require_dist = require("../dist-CYxusT1M.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
- const require_addon_utils = require("../addon-utils-CQFlZXSn.js");
3
+ const require_addon_utils = require("../addon-utils-WtpcVuzP.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_child_process = require("node:child_process");
6
6
  let node_path = require("node:path");
@@ -7482,6 +7482,17 @@ async function recoverStagingOrphans(dir, input, deps) {
7482
7482
  };
7483
7483
  }
7484
7484
  /**
7485
+ * Staging directories recovered at once in {@link recoverAllStagedOrphans}.
7486
+ * The pass is serial stat+mkdir+rename per segment over what is usually a
7487
+ * network/FUSE share (shfs): measured on the live hub at 40 454 ms for 46
7488
+ * files in 28 dirs — ~880 ms per file of pure round-trip latency, nothing
7489
+ * CPU-bound (2026-08-15). Dirs are independent by construction (each relocate
7490
+ * is a same-fs rename of a distinct file), so lanes are free; WITHIN a dir the
7491
+ * pass stays serial so the shutdown guard still stops after at most one
7492
+ * in-flight relocate per lane.
7493
+ */
7494
+ var STAGING_BOOT_DIR_CONCURRENCY = 4;
7495
+ /**
7485
7496
  * Boot reconcile: relocate every previous-run segment stranded in staging, on
7486
7497
  * every location, for every camera — attached or not.
7487
7498
  *
@@ -7516,6 +7527,7 @@ async function recoverAllStagedOrphans(deps) {
7516
7527
  locations: locations.map((l) => l.id),
7517
7528
  bootMs: deps.bootMs
7518
7529
  } });
7530
+ const work = [];
7519
7531
  for (const location of locations) {
7520
7532
  const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
7521
7533
  let deviceDirs;
@@ -7525,7 +7537,6 @@ async function recoverAllStagedOrphans(deps) {
7525
7537
  continue;
7526
7538
  }
7527
7539
  for (const deviceDir of deviceDirs) {
7528
- if (aborted) break;
7529
7540
  const deviceId = Number(deviceDir);
7530
7541
  if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
7531
7542
  let profileDirs;
@@ -7536,37 +7547,52 @@ async function recoverAllStagedOrphans(deps) {
7536
7547
  }
7537
7548
  const segmentSeconds = await deps.segmentSecondsFor(deviceId);
7538
7549
  for (const profile of profileDirs) {
7539
- if (aborted) break;
7540
7550
  if (!KNOWN_PROFILES.has(profile)) continue;
7541
- const dir = `${stagingRoot}/${deviceDir}/${profile}`;
7542
- dirs += 1;
7543
- const report = await recoverStagingOrphans(dir, {
7544
- writerStartedMs: deps.bootMs,
7551
+ work.push({
7552
+ location,
7553
+ deviceId,
7554
+ profile,
7555
+ dir: `${stagingRoot}/${deviceDir}/${profile}`,
7545
7556
  segmentSeconds
7546
- }, {
7547
- listDir: deps.listDir,
7548
- readPlaylist: deps.readPlaylist,
7549
- finalize: (orphan, flatAbsPath) => deps.finalize({
7550
- deviceId,
7551
- profile,
7552
- locationId: location.id,
7553
- locationRoot: location.root,
7554
- flatAbsPath,
7555
- startMs: orphan.startMs,
7556
- durMs: orphan.durMs
7557
- }),
7558
- logger: withDeviceTag(deps.logger, deviceId),
7559
- chunkSize: deps.chunkSize,
7560
- yieldBetween,
7561
- shouldStop: deps.shouldStop
7562
7557
  });
7563
- found += report.found;
7564
- recovered += report.recovered;
7565
- failed += report.failed;
7566
- if (report.aborted) aborted = true;
7567
7558
  }
7568
7559
  }
7569
7560
  }
7561
+ const queue = [...work];
7562
+ const lane = async () => {
7563
+ for (;;) {
7564
+ if (aborted || deps.shouldStop?.() === true) return;
7565
+ const item = queue.shift();
7566
+ if (item === void 0) return;
7567
+ dirs += 1;
7568
+ const report = await recoverStagingOrphans(item.dir, {
7569
+ writerStartedMs: deps.bootMs,
7570
+ segmentSeconds: item.segmentSeconds
7571
+ }, {
7572
+ listDir: deps.listDir,
7573
+ readPlaylist: deps.readPlaylist,
7574
+ finalize: (orphan, flatAbsPath) => deps.finalize({
7575
+ deviceId: item.deviceId,
7576
+ profile: item.profile,
7577
+ locationId: item.location.id,
7578
+ locationRoot: item.location.root,
7579
+ flatAbsPath,
7580
+ startMs: orphan.startMs,
7581
+ durMs: orphan.durMs
7582
+ }),
7583
+ logger: withDeviceTag(deps.logger, item.deviceId),
7584
+ chunkSize: deps.chunkSize,
7585
+ yieldBetween,
7586
+ shouldStop: deps.shouldStop
7587
+ });
7588
+ found += report.found;
7589
+ recovered += report.recovered;
7590
+ failed += report.failed;
7591
+ if (report.aborted) aborted = true;
7592
+ }
7593
+ };
7594
+ const lanes = Math.min(STAGING_BOOT_DIR_CONCURRENCY, Math.max(1, queue.length));
7595
+ await Promise.all(Array.from({ length: lanes }, lane));
7570
7596
  deps.logger.info("recorder: staging boot reconcile complete", { meta: {
7571
7597
  dirs,
7572
7598
  found,
@@ -1,4 +1,4 @@
1
- import { At as number, Ft as EventCategory, I as deriveRecordingMode, Mt as record, Nt as string, S as RecordingConfigSchema, St as selectAssignedProfileSlots, Tt as array, X as recordingCapability, Z as recordingExportCapability, et as storageEvictableCapability, f as EVENT_PAD_MS, ht as hydrateSchema, it as errMsg, jt as object, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, pt as DeviceType, ut as BaseAddon, v as OpsLogEntrySchema, x as RECORDING_EXPORT_MAX_READ_BYTES, yt as nodePin } from "../dist-DGp0dRiq.mjs";
1
+ import { At as number, Ft as EventCategory, I as deriveRecordingMode, Mt as record, Nt as string, S as RecordingConfigSchema, St as selectAssignedProfileSlots, Tt as array, X as recordingCapability, Z as recordingExportCapability, et as storageEvictableCapability, f as EVENT_PAD_MS, ht as hydrateSchema, it as errMsg, jt as object, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, pt as DeviceType, ut as BaseAddon, v as OpsLogEntrySchema, x as RECORDING_EXPORT_MAX_READ_BYTES, yt as nodePin } from "../dist-CJI_8GVY.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-A2S9D7pu.mjs";
4
4
  import { randomUUID } from "node:crypto";
@@ -7481,6 +7481,17 @@ async function recoverStagingOrphans(dir, input, deps) {
7481
7481
  };
7482
7482
  }
7483
7483
  /**
7484
+ * Staging directories recovered at once in {@link recoverAllStagedOrphans}.
7485
+ * The pass is serial stat+mkdir+rename per segment over what is usually a
7486
+ * network/FUSE share (shfs): measured on the live hub at 40 454 ms for 46
7487
+ * files in 28 dirs — ~880 ms per file of pure round-trip latency, nothing
7488
+ * CPU-bound (2026-08-15). Dirs are independent by construction (each relocate
7489
+ * is a same-fs rename of a distinct file), so lanes are free; WITHIN a dir the
7490
+ * pass stays serial so the shutdown guard still stops after at most one
7491
+ * in-flight relocate per lane.
7492
+ */
7493
+ var STAGING_BOOT_DIR_CONCURRENCY = 4;
7494
+ /**
7484
7495
  * Boot reconcile: relocate every previous-run segment stranded in staging, on
7485
7496
  * every location, for every camera — attached or not.
7486
7497
  *
@@ -7515,6 +7526,7 @@ async function recoverAllStagedOrphans(deps) {
7515
7526
  locations: locations.map((l) => l.id),
7516
7527
  bootMs: deps.bootMs
7517
7528
  } });
7529
+ const work = [];
7518
7530
  for (const location of locations) {
7519
7531
  const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
7520
7532
  let deviceDirs;
@@ -7524,7 +7536,6 @@ async function recoverAllStagedOrphans(deps) {
7524
7536
  continue;
7525
7537
  }
7526
7538
  for (const deviceDir of deviceDirs) {
7527
- if (aborted) break;
7528
7539
  const deviceId = Number(deviceDir);
7529
7540
  if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
7530
7541
  let profileDirs;
@@ -7535,37 +7546,52 @@ async function recoverAllStagedOrphans(deps) {
7535
7546
  }
7536
7547
  const segmentSeconds = await deps.segmentSecondsFor(deviceId);
7537
7548
  for (const profile of profileDirs) {
7538
- if (aborted) break;
7539
7549
  if (!KNOWN_PROFILES.has(profile)) continue;
7540
- const dir = `${stagingRoot}/${deviceDir}/${profile}`;
7541
- dirs += 1;
7542
- const report = await recoverStagingOrphans(dir, {
7543
- writerStartedMs: deps.bootMs,
7550
+ work.push({
7551
+ location,
7552
+ deviceId,
7553
+ profile,
7554
+ dir: `${stagingRoot}/${deviceDir}/${profile}`,
7544
7555
  segmentSeconds
7545
- }, {
7546
- listDir: deps.listDir,
7547
- readPlaylist: deps.readPlaylist,
7548
- finalize: (orphan, flatAbsPath) => deps.finalize({
7549
- deviceId,
7550
- profile,
7551
- locationId: location.id,
7552
- locationRoot: location.root,
7553
- flatAbsPath,
7554
- startMs: orphan.startMs,
7555
- durMs: orphan.durMs
7556
- }),
7557
- logger: withDeviceTag(deps.logger, deviceId),
7558
- chunkSize: deps.chunkSize,
7559
- yieldBetween,
7560
- shouldStop: deps.shouldStop
7561
7556
  });
7562
- found += report.found;
7563
- recovered += report.recovered;
7564
- failed += report.failed;
7565
- if (report.aborted) aborted = true;
7566
7557
  }
7567
7558
  }
7568
7559
  }
7560
+ const queue = [...work];
7561
+ const lane = async () => {
7562
+ for (;;) {
7563
+ if (aborted || deps.shouldStop?.() === true) return;
7564
+ const item = queue.shift();
7565
+ if (item === void 0) return;
7566
+ dirs += 1;
7567
+ const report = await recoverStagingOrphans(item.dir, {
7568
+ writerStartedMs: deps.bootMs,
7569
+ segmentSeconds: item.segmentSeconds
7570
+ }, {
7571
+ listDir: deps.listDir,
7572
+ readPlaylist: deps.readPlaylist,
7573
+ finalize: (orphan, flatAbsPath) => deps.finalize({
7574
+ deviceId: item.deviceId,
7575
+ profile: item.profile,
7576
+ locationId: item.location.id,
7577
+ locationRoot: item.location.root,
7578
+ flatAbsPath,
7579
+ startMs: orphan.startMs,
7580
+ durMs: orphan.durMs
7581
+ }),
7582
+ logger: withDeviceTag(deps.logger, item.deviceId),
7583
+ chunkSize: deps.chunkSize,
7584
+ yieldBetween,
7585
+ shouldStop: deps.shouldStop
7586
+ });
7587
+ found += report.found;
7588
+ recovered += report.recovered;
7589
+ failed += report.failed;
7590
+ if (report.aborted) aborted = true;
7591
+ }
7592
+ };
7593
+ const lanes = Math.min(STAGING_BOOT_DIR_CONCURRENCY, Math.max(1, queue.length));
7594
+ await Promise.all(Array.from({ length: lanes }, lane));
7569
7595
  deps.logger.info("recorder: staging boot reconcile complete", { meta: {
7570
7596
  dirs,
7571
7597
  found,
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-4L4SbSAo.js");
3
- const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.js");
2
+ const require_worker_protocol = require("../worker-protocol-34kViYAj.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-CTNP_35A.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-_e84lGuR.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-ZYA4xGRg.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.77",
21
+ version: "1.2.78",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.77",
39
+ version: "1.2.78",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B57MztMq.js");
5
+ const require_dist = require("../dist-CYxusT1M.js");
6
6
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_dist.__toESM(node_crypto, 1);
@@ -207,7 +207,9 @@ function formatProbedSummary(declared, snapshot, opts) {
207
207
  * on our own initiative, and says nothing about waking it because somebody
208
208
  * pressed play. That asymmetry is the whole design: brokers stay AVAILABLE
209
209
  * (definable, from the durable catalog) without being ACTIVE, and the dial is
210
- * the only thing that touches the camera.
210
+ * the only thing that touches the camera. Concurrent/retried requests for the
211
+ * same waking device share one transaction; a client retry must not become a
212
+ * second physical wake.
211
213
  *
212
214
  * ## Honest states
213
215
  *
@@ -243,10 +245,48 @@ var BatteryStreamUnavailableError = class extends Error {
243
245
  };
244
246
  /** Bound on the firmware wake. Matches the cap's own sensible range. */
245
247
  var WAKE_FOR_STREAM_TIMEOUT_MS = 8e3;
246
- /** Bound on the post-wake catalog nudge. Sized off the camera's ~14s awake
247
- * window: long enough for a real `onWakeTransition` catalog build to land,
248
- * short enough that a viewer is not left staring at nothing. */
249
- var STREAM_PARAMS_SETTLE_TIMEOUT_MS = 6e3;
248
+ /**
249
+ * Bound AFTER firmware acknowledgement for the provider to rebuild/publish
250
+ * stream parameters and the broker to observe them. Field cameras can
251
+ * acknowledge several seconds before their media/catalog plane is usable.
252
+ */
253
+ var BROKER_SETTLE_TIMEOUT_MS = 2e4;
254
+ /** Reconcile cadence while the acknowledged camera finishes waking. */
255
+ var BROKER_SETTLE_POLL_INTERVAL_MS = 1500;
256
+ function wait(timeoutMs) {
257
+ return new Promise((resolve) => {
258
+ setTimeout(resolve, timeoutMs).unref?.();
259
+ });
260
+ }
261
+ /**
262
+ * Device-scoped single-flight for wake-on-play. A wake applies to the whole
263
+ * camera, not one profile/brokerId, so concurrent profile requests and client
264
+ * retries all await the same bounded transaction.
265
+ */
266
+ var BatteryWakeOnPlaySingleFlight = class {
267
+ inFlight = /* @__PURE__ */ new Map();
268
+ async run(deviceId, deps) {
269
+ const existing = this.inFlight.get(deviceId);
270
+ if (existing) {
271
+ await existing;
272
+ if (!deps.hasBroker()) await deps.ensureBroker().catch((err) => {
273
+ deps.logger.debug("wake-on-play: joined demand broker retry failed", {
274
+ tags: { deviceId },
275
+ meta: { error: err instanceof Error ? err.message : String(err) }
276
+ });
277
+ });
278
+ if (!deps.hasBroker()) throw new BatteryStreamUnavailableError(deviceId, "waking", "the shared wake completed but this stream profile is not available yet — try again");
279
+ return;
280
+ }
281
+ const transaction = wakeBatteryDeviceForPlay(deviceId, deps);
282
+ this.inFlight.set(deviceId, transaction);
283
+ try {
284
+ await transaction;
285
+ } finally {
286
+ if (this.inFlight.get(deviceId) === transaction) this.inFlight.delete(deviceId);
287
+ }
288
+ }
289
+ };
250
290
  /**
251
291
  * Make a broker available for a battery camera that has none, by asking the
252
292
  * camera to wake.
@@ -257,26 +297,25 @@ var STREAM_PARAMS_SETTLE_TIMEOUT_MS = 6e3;
257
297
  * {@link BatteryStreamUnavailableError} only for a battery camera we could not
258
298
  * produce a stream for — with the reason named.
259
299
  *
260
- * Retries the broker resolution exactly ONCE after the wake. A second wake
261
- * would be us deciding the camera should be up, which is the proactive
262
- * behaviour the contract forbids; the operator pressing play again is a new
263
- * demand and gets a new wake.
300
+ * After an acknowledged wake, repeatedly reconciles the provider catalog and
301
+ * retries broker resolution within one field-sized bound. It never issues a
302
+ * second wake inside that transaction.
264
303
  */
265
304
  async function wakeBatteryDeviceForPlay(deviceId, deps) {
266
305
  if (deps.hasBroker()) return;
267
306
  if (await deps.isBatteryDevice(deviceId).catch(() => null) !== true) return;
268
307
  const facts = await deps.readBatteryFacts(deviceId).catch(() => null);
269
- const nowMs = (deps.nowMs ?? Date.now)();
308
+ const observedAt = (deps.nowMs ?? Date.now)();
270
309
  const presence = require_dist.deriveBatteryPresence({
271
310
  status: facts,
272
- nowMs
311
+ nowMs: observedAt
273
312
  });
274
313
  if (presence === "unreachable") {
275
314
  deps.logger.warn("wake-on-play refused — battery camera is unreachable, not asleep", {
276
315
  tags: { deviceId },
277
316
  meta: {
278
317
  lastContactAt: facts?.lastContactAt ?? 0,
279
- silenceMs: nowMs - (facts?.lastContactAt ?? 0)
318
+ silenceMs: observedAt - (facts?.lastContactAt ?? 0)
280
319
  }
281
320
  });
282
321
  throw new BatteryStreamUnavailableError(deviceId, "unreachable", "nothing has been heard from it for hours — check the battery or its network");
@@ -292,29 +331,46 @@ async function wakeBatteryDeviceForPlay(deviceId, deps) {
292
331
  awoke: false,
293
332
  durationMs: 0
294
333
  }));
295
- const nudged = await deps.awaitStreamParamsChanged(deviceId, STREAM_PARAMS_SETTLE_TIMEOUT_MS).catch(() => false);
296
- await deps.reconcileCatalog(deviceId).catch((err) => {
297
- deps.logger.debug("wake-on-play: catalog reconcile failed", {
298
- tags: { deviceId },
299
- meta: { error: err instanceof Error ? err.message : String(err) }
300
- });
301
- });
302
- await deps.ensureBroker().catch((err) => {
303
- deps.logger.debug("wake-on-play: broker retry failed", {
304
- tags: { deviceId },
305
- meta: { error: err instanceof Error ? err.message : String(err) }
334
+ const nowMs = deps.nowMs ?? Date.now;
335
+ const settleStartedAt = nowMs();
336
+ const settleDeadline = settleStartedAt + (wake.awoke ? BROKER_SETTLE_TIMEOUT_MS : 0);
337
+ const maxAttempts = wake.awoke ? Math.ceil(BROKER_SETTLE_TIMEOUT_MS / BROKER_SETTLE_POLL_INTERVAL_MS) + 1 : 1;
338
+ let attempts = 0;
339
+ while (attempts < maxAttempts) {
340
+ attempts += 1;
341
+ await deps.reconcileCatalog(deviceId).catch((err) => {
342
+ deps.logger.debug("wake-on-play: catalog reconcile failed", {
343
+ tags: { deviceId },
344
+ meta: {
345
+ attempt: attempts,
346
+ error: err instanceof Error ? err.message : String(err)
347
+ }
348
+ });
306
349
  });
307
- });
308
- if (deps.hasBroker()) {
309
- deps.logger.info("wake-on-play succeeded broker available after wake", {
310
- tags: { deviceId },
311
- meta: {
312
- awoke: wake.awoke,
313
- wakeMs: wake.durationMs,
314
- nudged
315
- }
350
+ await deps.ensureBroker().catch((err) => {
351
+ deps.logger.debug("wake-on-play: broker retry failed", {
352
+ tags: { deviceId },
353
+ meta: {
354
+ attempt: attempts,
355
+ error: err instanceof Error ? err.message : String(err)
356
+ }
357
+ });
316
358
  });
317
- return;
359
+ if (deps.hasBroker()) {
360
+ deps.logger.info("wake-on-play succeeded — broker available after wake", {
361
+ tags: { deviceId },
362
+ meta: {
363
+ awoke: wake.awoke,
364
+ wakeMs: wake.durationMs,
365
+ settleMs: nowMs() - settleStartedAt,
366
+ attempts
367
+ }
368
+ });
369
+ return;
370
+ }
371
+ const remainingMs = settleDeadline - nowMs();
372
+ if (remainingMs <= 0 || attempts >= maxAttempts) break;
373
+ await (deps.wait ?? wait)(Math.min(BROKER_SETTLE_POLL_INTERVAL_MS, remainingMs));
318
374
  }
319
375
  const outcome = wake.awoke ? "waking" : "sleeping";
320
376
  deps.logger.warn("wake-on-play exhausted — no stream after the wake", {
@@ -322,7 +378,8 @@ async function wakeBatteryDeviceForPlay(deviceId, deps) {
322
378
  meta: {
323
379
  awoke: wake.awoke,
324
380
  wakeMs: wake.durationMs,
325
- nudged,
381
+ settleMs: nowMs() - settleStartedAt,
382
+ attempts,
326
383
  outcome
327
384
  }
328
385
  });
@@ -14401,6 +14458,13 @@ var StreamBrokerManager = class StreamBrokerManager {
14401
14458
  */
14402
14459
  brokerEnsures = /* @__PURE__ */ new Map();
14403
14460
  /**
14461
+ * Device-scoped wake-on-play transactions. A camera wake is shared by every
14462
+ * profile, so Viewer retries or concurrent sessions must join the existing
14463
+ * sleeping → waking → broker-settle transaction instead of issuing another
14464
+ * `battery.wakeForStream` mutation.
14465
+ */
14466
+ batteryWakeOnPlay = new BatteryWakeOnPlaySingleFlight();
14467
+ /**
14404
14468
  * brokerId → the profile tier a remote-ensured adapter was created for
14405
14469
  * (Slice 3b Task 2). `this.assignments` only tracks LOCALLY-owned devices,
14406
14470
  * so the `profileTierResolver` closure below always misses for a remote
@@ -15206,7 +15270,8 @@ var StreamBrokerManager = class StreamBrokerManager {
15206
15270
  /**
15207
15271
  * Wake-on-play (D173). Called by `WebrtcSessionProvider` when the normal
15208
15272
  * resolution left no broker: if the device is battery-operated, ask the
15209
- * camera to wake, re-pull its catalog, and retry the resolution ONCE.
15273
+ * camera to wake, then poll catalog reconciliation + broker resolution
15274
+ * through one bounded settle transaction.
15210
15275
  *
15211
15276
  * Throws `BatteryStreamUnavailableError` — naming `sleeping` / `waking` /
15212
15277
  * `unreachable` — instead of letting `BrokerWebrtcServer` answer a healthy,
@@ -15219,7 +15284,7 @@ var StreamBrokerManager = class StreamBrokerManager {
15219
15284
  if (this.disabledDevices.has(deviceId)) return;
15220
15285
  const api = this.api;
15221
15286
  if (!api) return;
15222
- await wakeBatteryDeviceForPlay(deviceId, {
15287
+ await this.batteryWakeOnPlay.run(deviceId, {
15223
15288
  logger: this.logger,
15224
15289
  isBatteryDevice: async (id) => {
15225
15290
  const dev = await api.deviceManager.getDevice.query({ deviceId: id }).catch(() => null);
@@ -15242,38 +15307,12 @@ var StreamBrokerManager = class StreamBrokerManager {
15242
15307
  deviceId: id,
15243
15308
  timeoutMs
15244
15309
  }),
15245
- awaitStreamParamsChanged: (id, timeoutMs) => this.awaitStreamParamsChanged(id, timeoutMs),
15246
15310
  reconcileCatalog: (id) => this.reconcileDeviceCatalog(id),
15247
15311
  ensureBroker: () => this.ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId),
15248
15312
  hasBroker: () => this.hasRegisteredBroker(brokerId)
15249
15313
  });
15250
15314
  }
15251
15315
  /**
15252
- * Bounded wait for the provider's `StreamParamsChanged` nudge for one
15253
- * device. Resolves `false` on timeout — a timeout is information, not a
15254
- * failure: a catalog restored from the durable slice announces nothing, and
15255
- * the caller reconciles either way.
15256
- */
15257
- awaitStreamParamsChanged(deviceId, timeoutMs) {
15258
- const bus = this.eventBus;
15259
- if (!bus) return Promise.resolve(false);
15260
- return new Promise((resolve) => {
15261
- let settled = false;
15262
- const finish = (value) => {
15263
- if (settled) return;
15264
- settled = true;
15265
- clearTimeout(timer);
15266
- unsubscribe();
15267
- resolve(value);
15268
- };
15269
- const unsubscribe = bus.subscribe({ category: require_dist.EventCategory.StreamParamsChanged }, (event) => {
15270
- if (event.data.deviceId === deviceId) finish(true);
15271
- });
15272
- const timer = setTimeout(() => finish(false), timeoutMs);
15273
- timer.unref?.();
15274
- });
15275
- }
15276
- /**
15277
15316
  * Single-flight body for `ensureBrokerForWebrtcSession`'s remote-adapter
15278
15317
  * get-or-create — resolves the ingest owner, and for `remote-adapter` mode
15279
15318
  * constructs the `RemoteEncodedSourceBroker`, registers it with the WebRTC
@@ -35045,9 +35084,9 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35045
35084
  });
35046
35085
  });
35047
35086
  };
35048
- await (this.brokerManager?.reconcileAllCatalogsAtStartup().catch((err) => {
35087
+ this.brokerManager?.reconcileAllCatalogsAtStartup().catch((err) => {
35049
35088
  this.ctx.logger.warn("catalog startup reconcile failed", { meta: { error: require_dist.errMsg(err) } });
35050
- }) ?? Promise.resolve());
35089
+ }) ?? Promise.resolve();
35051
35090
  const reconcileMs = Math.max(5, this.config.catalogReconcileIntervalSec) * 1e3;
35052
35091
  this.catalogReconcileTimer = setInterval(() => reconcileAll("poll"), reconcileMs);
35053
35092
  this.subscribe({ category: require_dist.EventCategory.DeviceRegistered }, (event) => {
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "../chunk-DnnnRqeS.mjs";
2
- import { A as createHwAccelCache, At as number, C as RingBuffer, Dt as discriminatedUnion, Et as boolean, Ft as EventCategory, Mt as record, Nt as string, O as batteryCapability, P as deriveBatteryPresence, Pt as union, Q as resolveEgressDecodeHwAccel, R as egressTranscodeSharingKey, T as addonWidgetsSourceCapability, Tt as array, W as maskUrlCredentials, _t as makeProfileBrokerId, at as AUDIO_PRESETS, b as RATE_CONTROL_TIGHT, ct as invocationFromEncodeProfile, dt as CAM_PROFILE_ORDER, ft as DeviceFeature, gt as isEvent, i as BatteryStatusSchema, it as errMsg, jt as object, k as cameraStreamsCapability, kt as literal, lt as isSoftwareDecode, mt as createEvent, ot as Fmp4BoxSplitter, p as EncodeProfileSchema, pt as DeviceType, rt as webrtcSessionCapability, st as buildFfmpegArgs$1, tt as streamBrokerCapability, ut as BaseAddon, vt as makeSourceBrokerId, wt as _enum, xt as parseProfileBrokerId, y as RATE_CONTROL_RELAXED, yt as nodePin, z as egressTransportFromRequest } from "../dist-DGp0dRiq.mjs";
2
+ import { A as createHwAccelCache, At as number, C as RingBuffer, Dt as discriminatedUnion, Et as boolean, Ft as EventCategory, Mt as record, Nt as string, O as batteryCapability, P as deriveBatteryPresence, Pt as union, Q as resolveEgressDecodeHwAccel, R as egressTranscodeSharingKey, T as addonWidgetsSourceCapability, Tt as array, W as maskUrlCredentials, _t as makeProfileBrokerId, at as AUDIO_PRESETS, b as RATE_CONTROL_TIGHT, ct as invocationFromEncodeProfile, dt as CAM_PROFILE_ORDER, ft as DeviceFeature, gt as isEvent, i as BatteryStatusSchema, it as errMsg, jt as object, k as cameraStreamsCapability, kt as literal, lt as isSoftwareDecode, mt as createEvent, ot as Fmp4BoxSplitter, p as EncodeProfileSchema, pt as DeviceType, rt as webrtcSessionCapability, st as buildFfmpegArgs$1, tt as streamBrokerCapability, ut as BaseAddon, vt as makeSourceBrokerId, wt as _enum, xt as parseProfileBrokerId, y as RATE_CONTROL_RELAXED, yt as nodePin, z as egressTransportFromRequest } from "../dist-CJI_8GVY.mjs";
3
3
  import { n as profileForStreamId, r as parseRestreamPath } from "../remote-restream-Ci7RXNGb.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import * as crypto$1 from "node:crypto";
@@ -202,7 +202,9 @@ function formatProbedSummary(declared, snapshot, opts) {
202
202
  * on our own initiative, and says nothing about waking it because somebody
203
203
  * pressed play. That asymmetry is the whole design: brokers stay AVAILABLE
204
204
  * (definable, from the durable catalog) without being ACTIVE, and the dial is
205
- * the only thing that touches the camera.
205
+ * the only thing that touches the camera. Concurrent/retried requests for the
206
+ * same waking device share one transaction; a client retry must not become a
207
+ * second physical wake.
206
208
  *
207
209
  * ## Honest states
208
210
  *
@@ -238,10 +240,48 @@ var BatteryStreamUnavailableError = class extends Error {
238
240
  };
239
241
  /** Bound on the firmware wake. Matches the cap's own sensible range. */
240
242
  var WAKE_FOR_STREAM_TIMEOUT_MS = 8e3;
241
- /** Bound on the post-wake catalog nudge. Sized off the camera's ~14s awake
242
- * window: long enough for a real `onWakeTransition` catalog build to land,
243
- * short enough that a viewer is not left staring at nothing. */
244
- var STREAM_PARAMS_SETTLE_TIMEOUT_MS = 6e3;
243
+ /**
244
+ * Bound AFTER firmware acknowledgement for the provider to rebuild/publish
245
+ * stream parameters and the broker to observe them. Field cameras can
246
+ * acknowledge several seconds before their media/catalog plane is usable.
247
+ */
248
+ var BROKER_SETTLE_TIMEOUT_MS = 2e4;
249
+ /** Reconcile cadence while the acknowledged camera finishes waking. */
250
+ var BROKER_SETTLE_POLL_INTERVAL_MS = 1500;
251
+ function wait(timeoutMs) {
252
+ return new Promise((resolve) => {
253
+ setTimeout(resolve, timeoutMs).unref?.();
254
+ });
255
+ }
256
+ /**
257
+ * Device-scoped single-flight for wake-on-play. A wake applies to the whole
258
+ * camera, not one profile/brokerId, so concurrent profile requests and client
259
+ * retries all await the same bounded transaction.
260
+ */
261
+ var BatteryWakeOnPlaySingleFlight = class {
262
+ inFlight = /* @__PURE__ */ new Map();
263
+ async run(deviceId, deps) {
264
+ const existing = this.inFlight.get(deviceId);
265
+ if (existing) {
266
+ await existing;
267
+ if (!deps.hasBroker()) await deps.ensureBroker().catch((err) => {
268
+ deps.logger.debug("wake-on-play: joined demand broker retry failed", {
269
+ tags: { deviceId },
270
+ meta: { error: err instanceof Error ? err.message : String(err) }
271
+ });
272
+ });
273
+ if (!deps.hasBroker()) throw new BatteryStreamUnavailableError(deviceId, "waking", "the shared wake completed but this stream profile is not available yet — try again");
274
+ return;
275
+ }
276
+ const transaction = wakeBatteryDeviceForPlay(deviceId, deps);
277
+ this.inFlight.set(deviceId, transaction);
278
+ try {
279
+ await transaction;
280
+ } finally {
281
+ if (this.inFlight.get(deviceId) === transaction) this.inFlight.delete(deviceId);
282
+ }
283
+ }
284
+ };
245
285
  /**
246
286
  * Make a broker available for a battery camera that has none, by asking the
247
287
  * camera to wake.
@@ -252,26 +292,25 @@ var STREAM_PARAMS_SETTLE_TIMEOUT_MS = 6e3;
252
292
  * {@link BatteryStreamUnavailableError} only for a battery camera we could not
253
293
  * produce a stream for — with the reason named.
254
294
  *
255
- * Retries the broker resolution exactly ONCE after the wake. A second wake
256
- * would be us deciding the camera should be up, which is the proactive
257
- * behaviour the contract forbids; the operator pressing play again is a new
258
- * demand and gets a new wake.
295
+ * After an acknowledged wake, repeatedly reconciles the provider catalog and
296
+ * retries broker resolution within one field-sized bound. It never issues a
297
+ * second wake inside that transaction.
259
298
  */
260
299
  async function wakeBatteryDeviceForPlay(deviceId, deps) {
261
300
  if (deps.hasBroker()) return;
262
301
  if (await deps.isBatteryDevice(deviceId).catch(() => null) !== true) return;
263
302
  const facts = await deps.readBatteryFacts(deviceId).catch(() => null);
264
- const nowMs = (deps.nowMs ?? Date.now)();
303
+ const observedAt = (deps.nowMs ?? Date.now)();
265
304
  const presence = deriveBatteryPresence({
266
305
  status: facts,
267
- nowMs
306
+ nowMs: observedAt
268
307
  });
269
308
  if (presence === "unreachable") {
270
309
  deps.logger.warn("wake-on-play refused — battery camera is unreachable, not asleep", {
271
310
  tags: { deviceId },
272
311
  meta: {
273
312
  lastContactAt: facts?.lastContactAt ?? 0,
274
- silenceMs: nowMs - (facts?.lastContactAt ?? 0)
313
+ silenceMs: observedAt - (facts?.lastContactAt ?? 0)
275
314
  }
276
315
  });
277
316
  throw new BatteryStreamUnavailableError(deviceId, "unreachable", "nothing has been heard from it for hours — check the battery or its network");
@@ -287,29 +326,46 @@ async function wakeBatteryDeviceForPlay(deviceId, deps) {
287
326
  awoke: false,
288
327
  durationMs: 0
289
328
  }));
290
- const nudged = await deps.awaitStreamParamsChanged(deviceId, STREAM_PARAMS_SETTLE_TIMEOUT_MS).catch(() => false);
291
- await deps.reconcileCatalog(deviceId).catch((err) => {
292
- deps.logger.debug("wake-on-play: catalog reconcile failed", {
293
- tags: { deviceId },
294
- meta: { error: err instanceof Error ? err.message : String(err) }
295
- });
296
- });
297
- await deps.ensureBroker().catch((err) => {
298
- deps.logger.debug("wake-on-play: broker retry failed", {
299
- tags: { deviceId },
300
- meta: { error: err instanceof Error ? err.message : String(err) }
329
+ const nowMs = deps.nowMs ?? Date.now;
330
+ const settleStartedAt = nowMs();
331
+ const settleDeadline = settleStartedAt + (wake.awoke ? BROKER_SETTLE_TIMEOUT_MS : 0);
332
+ const maxAttempts = wake.awoke ? Math.ceil(BROKER_SETTLE_TIMEOUT_MS / BROKER_SETTLE_POLL_INTERVAL_MS) + 1 : 1;
333
+ let attempts = 0;
334
+ while (attempts < maxAttempts) {
335
+ attempts += 1;
336
+ await deps.reconcileCatalog(deviceId).catch((err) => {
337
+ deps.logger.debug("wake-on-play: catalog reconcile failed", {
338
+ tags: { deviceId },
339
+ meta: {
340
+ attempt: attempts,
341
+ error: err instanceof Error ? err.message : String(err)
342
+ }
343
+ });
301
344
  });
302
- });
303
- if (deps.hasBroker()) {
304
- deps.logger.info("wake-on-play succeeded broker available after wake", {
305
- tags: { deviceId },
306
- meta: {
307
- awoke: wake.awoke,
308
- wakeMs: wake.durationMs,
309
- nudged
310
- }
345
+ await deps.ensureBroker().catch((err) => {
346
+ deps.logger.debug("wake-on-play: broker retry failed", {
347
+ tags: { deviceId },
348
+ meta: {
349
+ attempt: attempts,
350
+ error: err instanceof Error ? err.message : String(err)
351
+ }
352
+ });
311
353
  });
312
- return;
354
+ if (deps.hasBroker()) {
355
+ deps.logger.info("wake-on-play succeeded — broker available after wake", {
356
+ tags: { deviceId },
357
+ meta: {
358
+ awoke: wake.awoke,
359
+ wakeMs: wake.durationMs,
360
+ settleMs: nowMs() - settleStartedAt,
361
+ attempts
362
+ }
363
+ });
364
+ return;
365
+ }
366
+ const remainingMs = settleDeadline - nowMs();
367
+ if (remainingMs <= 0 || attempts >= maxAttempts) break;
368
+ await (deps.wait ?? wait)(Math.min(BROKER_SETTLE_POLL_INTERVAL_MS, remainingMs));
313
369
  }
314
370
  const outcome = wake.awoke ? "waking" : "sleeping";
315
371
  deps.logger.warn("wake-on-play exhausted — no stream after the wake", {
@@ -317,7 +373,8 @@ async function wakeBatteryDeviceForPlay(deviceId, deps) {
317
373
  meta: {
318
374
  awoke: wake.awoke,
319
375
  wakeMs: wake.durationMs,
320
- nudged,
376
+ settleMs: nowMs() - settleStartedAt,
377
+ attempts,
321
378
  outcome
322
379
  }
323
380
  });
@@ -14396,6 +14453,13 @@ var StreamBrokerManager = class StreamBrokerManager {
14396
14453
  */
14397
14454
  brokerEnsures = /* @__PURE__ */ new Map();
14398
14455
  /**
14456
+ * Device-scoped wake-on-play transactions. A camera wake is shared by every
14457
+ * profile, so Viewer retries or concurrent sessions must join the existing
14458
+ * sleeping → waking → broker-settle transaction instead of issuing another
14459
+ * `battery.wakeForStream` mutation.
14460
+ */
14461
+ batteryWakeOnPlay = new BatteryWakeOnPlaySingleFlight();
14462
+ /**
14399
14463
  * brokerId → the profile tier a remote-ensured adapter was created for
14400
14464
  * (Slice 3b Task 2). `this.assignments` only tracks LOCALLY-owned devices,
14401
14465
  * so the `profileTierResolver` closure below always misses for a remote
@@ -15201,7 +15265,8 @@ var StreamBrokerManager = class StreamBrokerManager {
15201
15265
  /**
15202
15266
  * Wake-on-play (D173). Called by `WebrtcSessionProvider` when the normal
15203
15267
  * resolution left no broker: if the device is battery-operated, ask the
15204
- * camera to wake, re-pull its catalog, and retry the resolution ONCE.
15268
+ * camera to wake, then poll catalog reconciliation + broker resolution
15269
+ * through one bounded settle transaction.
15205
15270
  *
15206
15271
  * Throws `BatteryStreamUnavailableError` — naming `sleeping` / `waking` /
15207
15272
  * `unreachable` — instead of letting `BrokerWebrtcServer` answer a healthy,
@@ -15214,7 +15279,7 @@ var StreamBrokerManager = class StreamBrokerManager {
15214
15279
  if (this.disabledDevices.has(deviceId)) return;
15215
15280
  const api = this.api;
15216
15281
  if (!api) return;
15217
- await wakeBatteryDeviceForPlay(deviceId, {
15282
+ await this.batteryWakeOnPlay.run(deviceId, {
15218
15283
  logger: this.logger,
15219
15284
  isBatteryDevice: async (id) => {
15220
15285
  const dev = await api.deviceManager.getDevice.query({ deviceId: id }).catch(() => null);
@@ -15237,38 +15302,12 @@ var StreamBrokerManager = class StreamBrokerManager {
15237
15302
  deviceId: id,
15238
15303
  timeoutMs
15239
15304
  }),
15240
- awaitStreamParamsChanged: (id, timeoutMs) => this.awaitStreamParamsChanged(id, timeoutMs),
15241
15305
  reconcileCatalog: (id) => this.reconcileDeviceCatalog(id),
15242
15306
  ensureBroker: () => this.ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId),
15243
15307
  hasBroker: () => this.hasRegisteredBroker(brokerId)
15244
15308
  });
15245
15309
  }
15246
15310
  /**
15247
- * Bounded wait for the provider's `StreamParamsChanged` nudge for one
15248
- * device. Resolves `false` on timeout — a timeout is information, not a
15249
- * failure: a catalog restored from the durable slice announces nothing, and
15250
- * the caller reconciles either way.
15251
- */
15252
- awaitStreamParamsChanged(deviceId, timeoutMs) {
15253
- const bus = this.eventBus;
15254
- if (!bus) return Promise.resolve(false);
15255
- return new Promise((resolve) => {
15256
- let settled = false;
15257
- const finish = (value) => {
15258
- if (settled) return;
15259
- settled = true;
15260
- clearTimeout(timer);
15261
- unsubscribe();
15262
- resolve(value);
15263
- };
15264
- const unsubscribe = bus.subscribe({ category: EventCategory.StreamParamsChanged }, (event) => {
15265
- if (event.data.deviceId === deviceId) finish(true);
15266
- });
15267
- const timer = setTimeout(() => finish(false), timeoutMs);
15268
- timer.unref?.();
15269
- });
15270
- }
15271
- /**
15272
15311
  * Single-flight body for `ensureBrokerForWebrtcSession`'s remote-adapter
15273
15312
  * get-or-create — resolves the ingest owner, and for `remote-adapter` mode
15274
15313
  * constructs the `RemoteEncodedSourceBroker`, registers it with the WebRTC
@@ -35037,9 +35076,9 @@ var StreamBrokerAddon = class extends BaseAddon {
35037
35076
  });
35038
35077
  });
35039
35078
  };
35040
- await (this.brokerManager?.reconcileAllCatalogsAtStartup().catch((err) => {
35079
+ this.brokerManager?.reconcileAllCatalogsAtStartup().catch((err) => {
35041
35080
  this.ctx.logger.warn("catalog startup reconcile failed", { meta: { error: errMsg(err) } });
35042
- }) ?? Promise.resolve());
35081
+ }) ?? Promise.resolve();
35043
35082
  const reconcileMs = Math.max(5, this.config.catalogReconcileIntervalSec) * 1e3;
35044
35083
  this.catalogReconcileTimer = setInterval(() => reconcileAll("poll"), reconcileMs);
35045
35084
  this.subscribe({ category: EventCategory.DeviceRegistered }, (event) => {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cb8ynph1.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DKf1PFZt.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-B57MztMq.js");
1
+ const require_dist = require("./dist-CYxusT1M.js");
2
2
  //#region src/session-decode/native-lease-config.ts
3
3
  /**
4
4
  * Resolution of the native-frame hold / tile knobs for one decode worker.
@@ -1,4 +1,4 @@
1
- import { _ as NativeLeaseSettingsSchema, g as NativeLeaseAdmissionSchema, u as DEFAULT_NATIVE_LEASE_SETTINGS } from "./dist-DGp0dRiq.mjs";
1
+ import { _ as NativeLeaseSettingsSchema, g as NativeLeaseAdmissionSchema, u as DEFAULT_NATIVE_LEASE_SETTINGS } from "./dist-CJI_8GVY.mjs";
2
2
  //#region src/session-decode/native-lease-config.ts
3
3
  /**
4
4
  * Resolution of the native-frame hold / tile knobs for one decode worker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.81",
3
+ "version": "1.2.83",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",