@camstack/addon-pipeline 1.2.130 → 1.2.132

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 (35) hide show
  1. package/dist/{addon-utils-DH78p79i.js → addon-utils-DzCn6z7v.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +332 -20
  5. package/dist/detection-pipeline/index.mjs +330 -18
  6. package/dist/{dist-Wt81pN5Y.mjs → dist-BZYUUBUx.mjs} +1900 -1714
  7. package/dist/{dist-DOw-izoI.js → dist-DBP47QOz.js} +1900 -1714
  8. package/dist/{event-loop-stall-monitor-Bld7jNCb.js → event-loop-stall-monitor-CiH_MHfO.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-CtQCOZqm.mjs → event-loop-stall-monitor-IACE22fK.mjs} +1 -1
  10. package/dist/{lazy-sharp-BbNI2ZD-.js → lazy-sharp-B_kETjyQ.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/{process-memory-1_D3EZT9.mjs → process-memory-C7rsraCR.mjs} +1 -1
  16. package/dist/{process-memory-D9H5qjDl.js → process-memory-CtXQAA3d.js} +1 -1
  17. package/dist/recorder/index.js +326 -17
  18. package/dist/recorder/index.mjs +325 -16
  19. package/dist/{segment-demux-js-BW-Q-j2A.mjs → segment-demux-js-BL1UDdiI.mjs} +1 -1
  20. package/dist/{segment-demux-js-C2jBBqGW.js → segment-demux-js-wTzFi5UW.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DlKJZ-_M.mjs} +3 -3
  25. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DXKTNEae.mjs} +1 -1
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUw_aTfK.mjs} +1 -1
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-DEVCEYgu.mjs → hostInit-kDKgfUjp.mjs} +3 -3
  30. package/dist/stream-broker/index.js +2 -2
  31. package/dist/stream-broker/index.mjs +2 -2
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-nAZl_Rsv.js → worker-protocol-C3zaz0Jv.js} +1 -1
  34. package/dist/{worker-protocol-rpnr7Ldd.mjs → worker-protocol-Cut8984B.mjs} +1 -1
  35. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { B as deriveRecordingMode, Dt as hydrateSchema, F as customAction, Gt as string, Ht as number, L as defineCustomActions, Lt as array, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, Rt as boolean, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-Wt81pN5Y.mjs";
1
+ import { B as deriveRecordingMode, Dt as hydrateSchema, F as customAction, Gt as string, Ht as number, L as defineCustomActions, Lt as array, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, Rt as boolean, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-BZYUUBUx.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
4
4
  import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-CgBCF-mE.mjs";
@@ -349,8 +349,13 @@ function fragmentRangeFor(index, offsetTicks) {
349
349
  * "nothing to read" (D49's letter, applied to a byte budget: a fallible
350
350
  * computation must never silently look like an unbind).
351
351
  */
352
- function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
353
- const { samples, mfraOffset } = index;
352
+ /**
353
+ * The last sync sample at or before `offsetTicks` — the keyframe a decoder
354
+ * must start from. Shared by {@link fragmentsWithinBudget} and
355
+ * {@link fragmentsInWindow}: both walk forward from the same anchor, they
356
+ * only differ in the condition that stops the walk.
357
+ */
358
+ function findAnchor(samples, offsetTicks) {
354
359
  let lo = 0;
355
360
  let hi = samples.length - 1;
356
361
  let anchor = 0;
@@ -363,6 +368,11 @@ function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
363
368
  lo = mid + 1;
364
369
  } else hi = mid - 1;
365
370
  }
371
+ return anchor;
372
+ }
373
+ function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
374
+ const { samples, mfraOffset } = index;
375
+ const anchor = findAnchor(samples, offsetTicks);
366
376
  const endOf = (i) => samples[i + 1]?.moofOffset ?? mfraOffset;
367
377
  const anchorStart = samples[anchor]?.moofOffset ?? 0;
368
378
  const anchorEnd = endOf(anchor);
@@ -399,6 +409,70 @@ function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
399
409
  oversizeFragment
400
410
  };
401
411
  }
412
+ /**
413
+ * Convert a TIME WINDOW `[fromTicks, toTicks)` into the set of consecutive
414
+ * fragments that covers it — the replay's primitive for "give me this many
415
+ * seconds of one segment", as opposed to {@link fragmentsWithinBudget}'s
416
+ * "give me as much as fits in N bytes" (the scrub GOP read's primitive).
417
+ *
418
+ * Stops walking forward the instant the NEXT fragment's own start time
419
+ * already reaches `toTicks`: at that point the fragments already collected
420
+ * cover the whole window, because each fragment's coverage runs up to the
421
+ * next one's start (the same relationship {@link fragmentsWithinBudget}'s
422
+ * `endOf` encodes). `maxBytes` is a SAFETY CEILING, not the primary stop
423
+ * condition — a caller with a well-formed window is expected to hit the time
424
+ * condition first; the byte cap exists so a pathological request cannot pull
425
+ * an unbounded amount of one segment into memory.
426
+ *
427
+ * Same never-empty guarantee as {@link fragmentsWithinBudget}: the anchor
428
+ * fragment is always returned, even when it alone exceeds `maxBytes`.
429
+ */
430
+ function fragmentsInWindow(index, fromTicks, toTicks, maxBytes) {
431
+ const { samples, mfraOffset } = index;
432
+ const anchor = findAnchor(samples, fromTicks);
433
+ const endOf = (i) => samples[i + 1]?.moofOffset ?? mfraOffset;
434
+ const windowCoveredAt = (i) => {
435
+ const next = samples[i + 1];
436
+ return next === void 0 || next.timeTicks >= toTicks;
437
+ };
438
+ const anchorStart = samples[anchor]?.moofOffset ?? 0;
439
+ const anchorEnd = endOf(anchor);
440
+ const fragments = [{
441
+ start: anchorStart,
442
+ end: anchorEnd,
443
+ sampleIndex: anchor
444
+ }];
445
+ let total = anchorEnd - anchorStart;
446
+ const oversizeFragment = total > maxBytes;
447
+ let i = anchor;
448
+ let reachesWindowEnd = windowCoveredAt(i);
449
+ while (!reachesWindowEnd) {
450
+ const nextIndex = i + 1;
451
+ const nextSample = samples[nextIndex];
452
+ if (nextSample === void 0) break;
453
+ const nextStart = nextSample.moofOffset;
454
+ const nextEnd = endOf(nextIndex);
455
+ const nextSize = nextEnd - nextStart;
456
+ if (total + nextSize > maxBytes) break;
457
+ fragments.push({
458
+ start: nextStart,
459
+ end: nextEnd,
460
+ sampleIndex: nextIndex
461
+ });
462
+ total += nextSize;
463
+ i = nextIndex;
464
+ reachesWindowEnd = windowCoveredAt(i);
465
+ }
466
+ const last = fragments[fragments.length - 1];
467
+ return {
468
+ fragments,
469
+ start: fragments[0].start,
470
+ end: last.end,
471
+ reachesSegmentEnd: last.end >= mfraOffset,
472
+ reachesWindowEnd,
473
+ oversizeFragment
474
+ };
475
+ }
402
476
  //#endregion
403
477
  //#region src/recorder/mfra-capture.ts
404
478
  /**
@@ -4438,6 +4512,34 @@ var SCRUB_GOP_BUDGET_MAX_BYTES = 15e5;
4438
4512
  function ticksToMs(ticks, timescale) {
4439
4513
  return Math.round(ticks / timescale * 1e3);
4440
4514
  }
4515
+ /** ms → ticks on the video timescale — the inverse of {@link ticksToMs}. */
4516
+ function msToTicks(ms, timescale) {
4517
+ return ms / 1e3 * timescale;
4518
+ }
4519
+ /**
4520
+ * Steps 1-2 shared by {@link readGopRange} and {@link readGopWindowRange}:
4521
+ * locate the `mfra` (seeded or from the tail), read the `ftyp`+`moov` head,
4522
+ * and pick the VIDEO track's sync table (never the audio one, which lists
4523
+ * every `moof` and would send a seek to bytes that do not open a GOP).
4524
+ * `null` on anything that does not look like a recorded segment.
4525
+ */
4526
+ async function resolveSegmentIndex(file, seededIndex, read) {
4527
+ const parsed = seededIndex === void 0 ? await parseTail(file, read) : null;
4528
+ if (seededIndex === void 0 && parsed === null) return null;
4529
+ const index = seededIndex ?? parsed.index;
4530
+ const firstMoof = index.samples[0]?.moofOffset ?? 0;
4531
+ if (firstMoof <= 0 || firstMoof >= file.size) return null;
4532
+ const head = await read(0, firstMoof);
4533
+ const init = parseInitInfo(head);
4534
+ if (init === null) return null;
4535
+ const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
4536
+ if (videoIndex === null) return null;
4537
+ return {
4538
+ head,
4539
+ init,
4540
+ videoIndex
4541
+ };
4542
+ }
4441
4543
  /**
4442
4544
  * Read the single GOP covering `epochMs` from `file`, by byte range.
4443
4545
  *
@@ -4453,17 +4555,10 @@ async function readGopRange(file, args) {
4453
4555
  bytesRead += chunk.length;
4454
4556
  return chunk;
4455
4557
  };
4456
- const parsed = args.seededIndex === void 0 ? await parseTail(file, read) : null;
4457
- if (args.seededIndex === void 0 && parsed === null) return null;
4458
- const index = args.seededIndex ?? parsed.index;
4459
- const firstMoof = index.samples[0]?.moofOffset ?? 0;
4460
- if (firstMoof <= 0 || firstMoof >= file.size) return null;
4461
- const head = await read(0, firstMoof);
4462
- const init = parseInitInfo(head);
4463
- if (init === null) return null;
4464
- const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
4465
- if (videoIndex === null) return null;
4466
- const offsetTicks = Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale;
4558
+ const resolved = await resolveSegmentIndex(file, args.seededIndex, read);
4559
+ if (resolved === null) return null;
4560
+ const { head, init, videoIndex } = resolved;
4561
+ const offsetTicks = msToTicks(Math.max(0, args.epochMs - args.segmentStartMs), init.videoTimescale);
4467
4562
  let start;
4468
4563
  let end;
4469
4564
  let firstSampleIndex;
@@ -4508,6 +4603,49 @@ async function readGopRange(file, args) {
4508
4603
  };
4509
4604
  }
4510
4605
  /**
4606
+ * Read the fragments covering `[fromMs, toMs)` from `file`, by byte range —
4607
+ * the WINDOW twin of {@link readGopRange}'s single instant. Same shape, same
4608
+ * standalone-demuxable `ftyp`+`moov` head, same `null`-on-unparseable
4609
+ * contract; the only difference is the stop condition
4610
+ * ({@link fragmentsInWindow} instead of {@link fragmentRangeFor} /
4611
+ * {@link fragmentsWithinBudget}).
4612
+ */
4613
+ async function readGopWindowRange(file, args) {
4614
+ let bytesRead = 0;
4615
+ const read = async (offset, length) => {
4616
+ const chunk = await file.read(offset, length);
4617
+ bytesRead += chunk.length;
4618
+ return chunk;
4619
+ };
4620
+ const resolved = await resolveSegmentIndex(file, args.seededIndex, read);
4621
+ if (resolved === null) return null;
4622
+ const { head, init, videoIndex } = resolved;
4623
+ const fromOffsetMs = Math.max(0, args.fromMs - args.segmentStartMs);
4624
+ const toOffsetMs = Math.max(fromOffsetMs, args.toMs - args.segmentStartMs);
4625
+ const plan = fragmentsInWindow(videoIndex, msToTicks(fromOffsetMs, init.videoTimescale), msToTicks(toOffsetMs, init.videoTimescale), args.maxBytes);
4626
+ const { start, end } = plan;
4627
+ if (start <= 0 || end <= start || end > file.size) return null;
4628
+ const fragment = await read(start, end - start);
4629
+ const firstSampleIndex = plan.fragments[0].sampleIndex;
4630
+ const lastSampleIndex = plan.fragments[plan.fragments.length - 1].sampleIndex;
4631
+ const startTicks = videoIndex.samples[firstSampleIndex]?.timeTicks ?? 0;
4632
+ const nextTicks = videoIndex.samples[lastSampleIndex + 1]?.timeTicks;
4633
+ const gopStartOffsetMs = ticksToMs(startTicks, init.videoTimescale);
4634
+ const gopDurMs = nextTicks !== void 0 ? ticksToMs(nextTicks - startTicks, init.videoTimescale) : Math.max(1, args.segmentDurMs - gopStartOffsetMs);
4635
+ const data = new Uint8Array(head.length + fragment.length);
4636
+ data.set(head, 0);
4637
+ data.set(fragment, head.length);
4638
+ return {
4639
+ data,
4640
+ gopStartMs: args.segmentStartMs + gopStartOffsetMs,
4641
+ gopDurMs,
4642
+ bytesRead,
4643
+ reachesSegmentEnd: plan.reachesSegmentEnd,
4644
+ reachesWindowEnd: plan.reachesWindowEnd,
4645
+ oversizeFragment: plan.oversizeFragment
4646
+ };
4647
+ }
4648
+ /**
4511
4649
  * Fetch + parse the `mfra` tail, honouring `MfraParseError.needBytes` with ONE
4512
4650
  * widened retry. Null on anything else — bytes that are not a segment.
4513
4651
  */
@@ -4799,6 +4937,16 @@ var RANGE_MERGE_GAP_MS = 5e3;
4799
4937
  * hour but covers the requested epoch is hydrated too. Recorder segments are
4800
4938
  * currently much shorter; the margin keeps this boundary future-safe. */
4801
4939
  var LOCATE_HYDRATE_MARGIN_MS = 6e4;
4940
+ /**
4941
+ * Safety ceiling for one `readWindowBytes` answer — NOT the primary stop
4942
+ * condition (the requested `toMs` is; see `fragmentsInWindow` in
4943
+ * `mfra-index.ts`). Sized off `high`'s own bandwidth hint above
4944
+ * (`PROFILE_BANDWIDTH.high` = 4 Mbps = 500 000 B/s): a 30 s replay window at
4945
+ * that rate is ~15 MB, so 20 MB covers it with headroom while still bounding
4946
+ * a pathological request. A window this call actually truncates is reported
4947
+ * (`reachesRequestedEnd: false`) rather than silently shortened.
4948
+ */
4949
+ var WINDOW_READ_MAX_BYTES = 2e7;
4802
4950
  /** Empty "no playback" manifest. */
4803
4951
  function noPlayback(deviceId) {
4804
4952
  return {
@@ -5316,6 +5464,76 @@ function buildRecordingProvider(deps) {
5316
5464
  gopDurMs: seg.durMs
5317
5465
  };
5318
5466
  },
5467
+ readWindowBytes: async ({ deviceId, profile, startMs, fromMs, toMs }) => {
5468
+ const { abs, seg } = resolveSegmentFile(deps, deviceId, profile, startMs);
5469
+ const segmentEndMs = seg.startMs + seg.durMs;
5470
+ if (toMs > segmentEndMs) {
5471
+ deps.logger.warn("recording: readWindowBytes refused — window runs past this segment, not silently clipped", {
5472
+ tags: { deviceId },
5473
+ meta: {
5474
+ profile,
5475
+ startMs,
5476
+ fromMs,
5477
+ toMs,
5478
+ segmentEndMs
5479
+ }
5480
+ });
5481
+ return {
5482
+ kind: "spans-multiple-segments",
5483
+ segmentEndMs
5484
+ };
5485
+ }
5486
+ const file = await (deps.openRangeReadable ?? openRangeFromDisk)(abs, seg.bytes);
5487
+ try {
5488
+ const gop = await readGopWindowRange(file, {
5489
+ segmentStartMs: seg.startMs,
5490
+ segmentDurMs: seg.durMs,
5491
+ fromMs,
5492
+ toMs,
5493
+ seededIndex: deps.mfraFor?.(deviceId, profile, startMs),
5494
+ maxBytes: WINDOW_READ_MAX_BYTES
5495
+ });
5496
+ if (gop !== null) {
5497
+ if (gop.reachesWindowEnd !== true) deps.logger.warn("recording: readWindowBytes truncated by its safety byte cap before covering the requested window", {
5498
+ tags: { deviceId },
5499
+ meta: {
5500
+ profile,
5501
+ startMs,
5502
+ fromMs,
5503
+ toMs,
5504
+ bytes: gop.data.length,
5505
+ WINDOW_READ_MAX_BYTES
5506
+ }
5507
+ });
5508
+ return {
5509
+ kind: "ok",
5510
+ data: gop.data,
5511
+ gopStartMs: gop.gopStartMs,
5512
+ gopDurMs: gop.gopDurMs,
5513
+ reachesRequestedEnd: gop.reachesWindowEnd === true
5514
+ };
5515
+ }
5516
+ } finally {
5517
+ await file.close();
5518
+ }
5519
+ deps.logger.warn("recording: readWindowBytes fell back to the WHOLE segment (mfra/moov unparseable)", {
5520
+ tags: { deviceId },
5521
+ meta: {
5522
+ profile,
5523
+ startMs,
5524
+ fromMs,
5525
+ toMs
5526
+ }
5527
+ });
5528
+ const data = await readFile(abs);
5529
+ return {
5530
+ kind: "ok",
5531
+ data: Uint8Array.from(data),
5532
+ gopStartMs: seg.startMs,
5533
+ gopDurMs: seg.durMs,
5534
+ reachesRequestedEnd: true
5535
+ };
5536
+ },
5319
5537
  setDeviceConfig: async ({ deviceId, config }) => {
5320
5538
  const saved = await saveDeviceConfig(deps.configStore, deviceId, config);
5321
5539
  if (deps.onConfigChanged) try {
@@ -7520,7 +7738,46 @@ function resolveBandBufferMs(band) {
7520
7738
  */
7521
7739
  function eventsBandCanEverDemand(band) {
7522
7740
  if (band.mode !== "events") return true;
7523
- return (band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0) && resolveBandBufferMs(band).postMs > 0;
7741
+ return bandTriggerSources(band).length > 0 && resolveBandBufferMs(band).postMs > 0;
7742
+ }
7743
+ /**
7744
+ * LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
7745
+ * una banda diventano `TriggerSource[]`. `eventsBandCanEverDemand` e
7746
+ * `RecordingController.reportUnrecordable` lo leggono invece di ripetere
7747
+ * l'if-ladder motion/audio a mano.
7748
+ */
7749
+ function bandTriggerSources(band) {
7750
+ const t = band.triggers;
7751
+ if (t === void 0) return [];
7752
+ const sources = [];
7753
+ if (t.motion === true) sources.push("motion");
7754
+ if (t.audioThresholdDbfs !== void 0) sources.push("audio");
7755
+ return sources;
7756
+ }
7757
+ /**
7758
+ * Does this audio window's level clear the ACTIVE band's configured
7759
+ * threshold?
7760
+ *
7761
+ * `PipelineAudioInferenceResultPayload.frame.level.dbfs`
7762
+ * (`packages/types/src/types/detection.ts:448`) and
7763
+ * `RecordingTriggers.audioThresholdDbfs` are the SAME unit and the SAME
7764
+ * scale — dBFS, 0 = full scale, more negative = quieter — produced by
7765
+ * `audio-analyzer-provider.ts:77` (`20 * log10(rms)`). No conversion exists
7766
+ * between them because none is needed: the one other place in this repo that
7767
+ * compares the two values does the same bare `>=`
7768
+ * (`notification-center/audio-rule-matcher.ts:221`). Inventing a conversion
7769
+ * here would recreate the exact defect this track exists to close, only
7770
+ * harder to see because a wrong scale still fires occasionally.
7771
+ *
7772
+ * `band` must be the band ACTIVE AT the audio window's own timestamp — a
7773
+ * device can carry several bands with different thresholds (day/night), and
7774
+ * only one is ever "the" threshold at a given instant. The caller
7775
+ * (`RecordingController.onAudioLevel`) resolves it the same way
7776
+ * `postBufferMsFor` resolves the post-buffer.
7777
+ */
7778
+ function audioLevelQualifies(band, dbfs) {
7779
+ const threshold = band?.triggers?.audioThresholdDbfs;
7780
+ return threshold !== void 0 && dbfs >= threshold;
7524
7781
  }
7525
7782
  /** No trigger has been seen for this device yet. */
7526
7783
  var EMPTY_TRIGGER_STATE = {
@@ -9383,6 +9640,43 @@ var RecordingController = class {
9383
9640
  }
9384
9641
  }
9385
9642
  /**
9643
+ * Feed one audio window's LEVEL into the trigger pipeline — the counterpart
9644
+ * of `onTrigger('motion', …)` for the `audioThresholdDbfs` source, and the
9645
+ * feed that source was missing until 2026-08-26 (D62: authorized by the UI,
9646
+ * read by every pure function in `band-decision.ts`, alimented by nobody).
9647
+ *
9648
+ * Qualification happens HERE, not upstream like motion's `data.detected`,
9649
+ * because it depends on a PER-BAND threshold: `audioThresholdDbfs` is
9650
+ * authored per band, not once per device, so it needs the same config read
9651
+ * {@link postBufferMsFor} already does to resolve which band is active at
9652
+ * `atMs` before it can know which threshold applies.
9653
+ *
9654
+ * Fed from the event bus (telemetry, may be dropped — D8/D11), so a
9655
+ * config-read failure here drops only THIS window's qualification test
9656
+ * rather than guessing a threshold — logged, never silent, and never fatal:
9657
+ * the device's recordability does not depend on any single audio sample,
9658
+ * the next window (or motion) can still open the band.
9659
+ */
9660
+ async onAudioLevel(deviceId, dbfs, atMs) {
9661
+ if (this.stopped) return;
9662
+ let band;
9663
+ try {
9664
+ band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
9665
+ } catch (err) {
9666
+ this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
9667
+ tags: { deviceId },
9668
+ meta: {
9669
+ atMs,
9670
+ dbfs,
9671
+ error: errMsg(err)
9672
+ }
9673
+ });
9674
+ return;
9675
+ }
9676
+ if (!audioLevelQualifies(band, dbfs)) return;
9677
+ await this.onTrigger(deviceId, "audio", atMs);
9678
+ }
9679
+ /**
9386
9680
  * The post-buffer that governs this device's trigger history right now: it is
9387
9681
  * both the MERGE GAP (a trigger landing inside the open live window belongs to
9388
9682
  * the running episode) and the retention horizon. Falls back to the shipped
@@ -9497,7 +9791,7 @@ var RecordingController = class {
9497
9791
  meta: {
9498
9792
  postBufferSec: band.postBufferSec,
9499
9793
  triggers: band.triggers,
9500
- reason: band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0 ? "postBufferSec is 0" : "no trigger configured"
9794
+ reason: bandTriggerSources(band).length > 0 ? "postBufferSec is 0" : "no trigger configured"
9501
9795
  }
9502
9796
  });
9503
9797
  }
@@ -11551,6 +11845,14 @@ var RecorderV2Addon = class extends BaseAddon {
11551
11845
  eventMap = new EventMap();
11552
11846
  /** Unsubscribe handle for the EventCapture motion subscription. */
11553
11847
  eventUnsub = null;
11848
+ /**
11849
+ * Unsubscribe handle for the `PipelineAudioInferenceResult` audio-level
11850
+ * subscription — the feed `audioThresholdDbfs` was missing (D62, Traccia 0
11851
+ * of `docs/design/plans/2026-08-26-trigger-detection-e-sensori.md`). Kept
11852
+ * separate from `eventUnsub` (a distinct bus category) but guarded and torn
11853
+ * down the same way, idempotent re-entry included.
11854
+ */
11855
+ audioTriggerUnsub = null;
11554
11856
  /** Resolved recordings locations (id + root). Refreshed on boot + rescan.
11555
11857
  * Already ALIAS-GUARDED: a writable same-type duplicate of another
11556
11858
  * location's physical root arrives here demoted to read-only. */
@@ -12022,6 +12324,8 @@ var RecorderV2Addon = class extends BaseAddon {
12022
12324
  async onShutdown() {
12023
12325
  this.eventUnsub?.();
12024
12326
  this.eventUnsub = null;
12327
+ this.audioTriggerUnsub?.();
12328
+ this.audioTriggerUnsub = null;
12025
12329
  await this.controller?.stop();
12026
12330
  this.controller = null;
12027
12331
  this.segmentStore = null;
@@ -12096,6 +12400,11 @@ var RecorderV2Addon = class extends BaseAddon {
12096
12400
  this.controller?.onTrigger(deviceId, "motion", atMs);
12097
12401
  }
12098
12402
  });
12403
+ if (this.audioTriggerUnsub === null) this.audioTriggerUnsub = this.ctx.eventBus.subscribe({ category: EventCategory.PipelineAudioInferenceResult }, (event) => {
12404
+ const dbfs = event.data.frame.level?.dbfs;
12405
+ if (dbfs === void 0) return;
12406
+ this.controller?.onAudioLevel(event.data.deviceId, dbfs, event.data.frame.timestamp);
12407
+ });
12099
12408
  try {
12100
12409
  await this.segmentHours?.load();
12101
12410
  this.segmentHours?.hydrateIndex(this.index);
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { gt as errMsg } from "./dist-Wt81pN5Y.mjs";
2
+ import { gt as errMsg } from "./dist-BZYUUBUx.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,4 +1,4 @@
1
- const require_dist = require("./dist-DOw-izoI.js");
1
+ const require_dist = require("./dist-DBP47QOz.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,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-nAZl_Rsv.js");
3
- const require_lazy_sharp = require("../lazy-sharp-BbNI2ZD-.js");
2
+ const require_worker_protocol = require("../worker-protocol-C3zaz0Jv.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-B_kETjyQ.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-rpnr7Ldd.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-Cut8984B.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs";
1
+ import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUw_aTfK.mjs";
2
2
  import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
- import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DXKTNEae.mjs";
5
5
  import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.32",
6
+ version: "1.2.34",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.110",
21
+ version: "1.2.112",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.73",
36
+ version: "1.2.75",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",