@camstack/addon-pipeline 1.2.191 → 1.2.193
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.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +7 -7
- package/dist/detection-pipeline/index.mjs +5 -5
- package/dist/{dist-PQArEi7A.js → dist-HG5geL9t.js} +135 -1
- package/dist/{dist-Cjazw3T-.mjs → dist-mYDK58Jp.mjs} +124 -2
- package/dist/{lazy-sharp-DfnnPeG5.js → lazy-sharp-DmAwZ0s9.js} +1 -1
- package/dist/{local-frame-registry-CrLITJ5O.mjs → local-frame-registry-av9ObDG0.mjs} +1 -1
- package/dist/{local-frame-registry-DNVTAOrZ.js → local-frame-registry-x3m6SNqf.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-wFmd_eR_.js → node--my8JTOW.js} +1 -1
- package/dist/{node-KFUZT2L-.mjs → node-xRsaPwBa.mjs} +1 -1
- package/dist/pipeline-runner/index.js +255 -221
- package/dist/pipeline-runner/index.mjs +253 -219
- package/dist/{process-memory-BgX9_XQ5.mjs → process-memory-BDHR3lxe.mjs} +1 -1
- package/dist/{process-memory-B4o15Ps9.js → process-memory-CoCSce63.js} +1 -1
- package/dist/recorder/index.js +319 -8
- package/dist/recorder/index.mjs +319 -8
- package/dist/{segment-demux-js-C36b7S82.js → segment-demux-js-CBynn9Gh.js} +1 -1
- package/dist/{segment-demux-js-Cgz-NlSg.mjs → segment-demux-js-CXvgKfsn.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +45 -10
- package/dist/session-decode/decode-worker-child.mjs +44 -9
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-YWGmRusr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CUjbLOm9.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D8AK-_F6.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-CuZBoqi0.mjs → hostInit-DnBCyuSA.mjs} +3 -3
- package/dist/stream-broker/index.js +93 -43
- package/dist/stream-broker/index.mjs +93 -43
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-n4hf2NsC.mjs → worker-protocol-C0R1DDC0.mjs} +1 -1
- package/dist/{worker-protocol-BulRQMSK.js → worker-protocol-DCsi4YS1.js} +1 -1
- package/package.json +1 -1
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
1
|
+
import { $t as number, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as errMsg, Ft as DeviceFeature, Gt as selectAssignedProfileSlots, H as deriveBatteryPresence, Ht as nodePin, It as DeviceType, Jt as array, Kt as sleep$1, Nt as BaseAddon, P as batteryCapability, R as customAction, Rt as hydrateSchema, St as storageEvictableCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Yt as boolean, bt as resolveRecordingProfiles, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as object, et as isFirstLevelMacroClass, g as ExportRecordSchema, gt as recordingSignalCapability, ht as recordingExportCapability, i as BatteryStatusSchema, in as EventCategory, mt as recordingCapability, nn as string, qt as _enum, tn as record, tt as loadContributionCapability, x as OpsLogEntrySchema } from "../dist-mYDK58Jp.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-xRsaPwBa.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";
|
|
@@ -12517,6 +12517,7 @@ function triggerKey(source) {
|
|
|
12517
12517
|
case "audio": return "audio";
|
|
12518
12518
|
case "object": return `object:${source.macroClass}`;
|
|
12519
12519
|
case "sensor": return `sensor:${source.sourceDeviceId}`;
|
|
12520
|
+
case "signal": return "signal";
|
|
12520
12521
|
}
|
|
12521
12522
|
}
|
|
12522
12523
|
/**
|
|
@@ -12547,6 +12548,7 @@ function bandTriggerSources(band) {
|
|
|
12547
12548
|
kind: "sensor",
|
|
12548
12549
|
sourceDeviceId
|
|
12549
12550
|
});
|
|
12551
|
+
if (t.deviceSignal === true) sources.push({ kind: "signal" });
|
|
12550
12552
|
return sources;
|
|
12551
12553
|
}
|
|
12552
12554
|
/**
|
|
@@ -12601,7 +12603,69 @@ function withWindows(state, source, windows) {
|
|
|
12601
12603
|
const key = triggerKey(source);
|
|
12602
12604
|
if (windows.length === 0) next.delete(key);
|
|
12603
12605
|
else next.set(key, windows);
|
|
12604
|
-
return { windows: next };
|
|
12606
|
+
return withHeld({ windows: next }, state.held);
|
|
12607
|
+
}
|
|
12608
|
+
/** The same state with `held` replaced — absent when there is nothing held, so
|
|
12609
|
+
* a state that holds nothing is shaped exactly like one built before holds
|
|
12610
|
+
* existed. */
|
|
12611
|
+
function withHeld(state, held) {
|
|
12612
|
+
if (held === void 0 || held.size === 0) {
|
|
12613
|
+
const { held: _dropped, ...rest } = state;
|
|
12614
|
+
return rest;
|
|
12615
|
+
}
|
|
12616
|
+
return {
|
|
12617
|
+
...state,
|
|
12618
|
+
held
|
|
12619
|
+
};
|
|
12620
|
+
}
|
|
12621
|
+
/** When the source rose, or null when it is not held. */
|
|
12622
|
+
function heldSinceMs(state, source) {
|
|
12623
|
+
return state.held?.get(triggerKey(source)) ?? null;
|
|
12624
|
+
}
|
|
12625
|
+
/**
|
|
12626
|
+
* Raise a LEVEL source: from `atMs` on, the live gate demands and the keep
|
|
12627
|
+
* gate retains, until {@link releaseTrigger}. Idempotent — a second rise of a
|
|
12628
|
+
* source already held keeps the ORIGINAL rise, because the level never fell in
|
|
12629
|
+
* between and the footage since then belongs to the same episode. A re-emitted
|
|
12630
|
+
* `active: true` (a slice rewritten for another field, a reconcile read) must
|
|
12631
|
+
* not shorten the keep span.
|
|
12632
|
+
*/
|
|
12633
|
+
function holdTrigger(state, source, atMs) {
|
|
12634
|
+
const key = triggerKey(source);
|
|
12635
|
+
if (state.held?.has(key)) return state;
|
|
12636
|
+
const held = new Map(state.held);
|
|
12637
|
+
held.set(key, atMs);
|
|
12638
|
+
return withHeld(state, held);
|
|
12639
|
+
}
|
|
12640
|
+
/**
|
|
12641
|
+
* Lower a LEVEL source: the hold becomes the window `[since, atMs]`, folded
|
|
12642
|
+
* through the SAME merge every other source uses — so a rise landing inside
|
|
12643
|
+
* the post-buffer of the previous fall extends that episode rather than
|
|
12644
|
+
* opening a second one, exactly as consecutive motion triggers do. From here
|
|
12645
|
+
* on the band's `postBufferSec` runs and the schedule arms the close.
|
|
12646
|
+
*
|
|
12647
|
+
* A release of a source that is not held returns the SAME object: a falling
|
|
12648
|
+
* edge the recorder never saw rise (a runner respawn mid-job, a reconcile
|
|
12649
|
+
* read) has nothing to close, and the caller can skip a write.
|
|
12650
|
+
*/
|
|
12651
|
+
function releaseTrigger(state, source, atMs, mergeGapMs) {
|
|
12652
|
+
const key = triggerKey(source);
|
|
12653
|
+
const since = state.held?.get(key);
|
|
12654
|
+
if (since === void 0) return state;
|
|
12655
|
+
const held = new Map(state.held);
|
|
12656
|
+
held.delete(key);
|
|
12657
|
+
const span = {
|
|
12658
|
+
firstMs: since,
|
|
12659
|
+
lastMs: Math.max(since, atMs)
|
|
12660
|
+
};
|
|
12661
|
+
const windows = triggerWindowsFor(state, source);
|
|
12662
|
+
const newest = windows[windows.length - 1];
|
|
12663
|
+
const merged = newest !== void 0 && span.firstMs - newest.lastMs <= mergeGapMs ? [...windows.slice(0, -1), {
|
|
12664
|
+
firstMs: Math.min(newest.firstMs, span.firstMs),
|
|
12665
|
+
lastMs: Math.max(newest.lastMs, span.lastMs)
|
|
12666
|
+
}] : [...windows, span];
|
|
12667
|
+
const evicted = Math.max(0, merged.length - 32);
|
|
12668
|
+
return withWindows(withHeld(state, held), source, evicted > 0 ? merged.slice(evicted) : merged);
|
|
12605
12669
|
}
|
|
12606
12670
|
/**
|
|
12607
12671
|
* Fold one qualifying trigger into the history, immutably. It EXTENDS the newest
|
|
@@ -12643,7 +12707,7 @@ function pruneTriggerHistory(state, postMs, nowMs) {
|
|
|
12643
12707
|
if (live.length !== windows.length) changed = true;
|
|
12644
12708
|
if (live.length > 0) next.set(key, live);
|
|
12645
12709
|
}
|
|
12646
|
-
return changed ? { windows: next } : state;
|
|
12710
|
+
return changed ? withHeld({ windows: next }, state.held) : state;
|
|
12647
12711
|
}
|
|
12648
12712
|
/** Most recent trigger for a source, or null when none is retained. */
|
|
12649
12713
|
function latestTriggerMs(state, source) {
|
|
@@ -12658,6 +12722,7 @@ function latestTriggerMs(state, source) {
|
|
|
12658
12722
|
function eventsBandDemanded(band, triggers, atMs) {
|
|
12659
12723
|
const { postMs } = resolveBandBufferMs(band);
|
|
12660
12724
|
return bandTriggerSources(band).some((source) => {
|
|
12725
|
+
if (heldSinceMs(triggers, source) !== null) return true;
|
|
12661
12726
|
const lastMs = latestTriggerMs(triggers, source);
|
|
12662
12727
|
return lastMs !== null && atMs - lastMs <= postMs;
|
|
12663
12728
|
});
|
|
@@ -12731,7 +12796,11 @@ function segmentRetainedForEvents(segStartMs, segEndMs, band, triggers, attach)
|
|
|
12731
12796
|
if (segStartMs <= window.lastMs + postMs) return true;
|
|
12732
12797
|
return attach.attachedAtMs <= window.lastMs + postMs && segStartMs <= deadlineMs;
|
|
12733
12798
|
};
|
|
12734
|
-
return bandTriggerSources(band).some((source) =>
|
|
12799
|
+
return bandTriggerSources(band).some((source) => {
|
|
12800
|
+
const since = heldSinceMs(triggers, source);
|
|
12801
|
+
if (since !== null && segEndMs >= since - preMs) return true;
|
|
12802
|
+
return triggerWindowsFor(triggers, source).some(overlaps);
|
|
12803
|
+
});
|
|
12735
12804
|
}
|
|
12736
12805
|
/**
|
|
12737
12806
|
* By how many ms a discarded segment missed being kept — its start past the
|
|
@@ -12753,7 +12822,10 @@ function segmentMissedByMs(segStartMs, band, triggers, attach) {
|
|
|
12753
12822
|
const windowEndMs = window.lastMs + postMs;
|
|
12754
12823
|
return attach.attachedAtMs <= windowEndMs ? Math.max(windowEndMs, deadlineMs) : windowEndMs;
|
|
12755
12824
|
};
|
|
12756
|
-
const
|
|
12825
|
+
const sources = bandTriggerSources(band);
|
|
12826
|
+
const holds = sources.map((source) => heldSinceMs(triggers, source)).filter((s) => s !== null);
|
|
12827
|
+
if (holds.length > 0) return Math.min(-1, segStartMs - Math.min(...holds));
|
|
12828
|
+
const considered = sources.flatMap((source) => [...triggerWindowsFor(triggers, source)]);
|
|
12757
12829
|
if (considered.length === 0) return null;
|
|
12758
12830
|
return segStartMs - Math.max(...considered.map(latestKeptStart));
|
|
12759
12831
|
}
|
|
@@ -12937,7 +13009,9 @@ function nextBandEdgeMs(config, nowMs) {
|
|
|
12937
13009
|
function eventsWindowCloseMs(band, triggers, nowMs) {
|
|
12938
13010
|
if (band.mode !== "events") return null;
|
|
12939
13011
|
const { postMs } = resolveBandBufferMs(band);
|
|
12940
|
-
const
|
|
13012
|
+
const sources = bandTriggerSources(band);
|
|
13013
|
+
if (sources.some((source) => heldSinceMs(triggers, source) !== null)) return null;
|
|
13014
|
+
const open = sources.map((source) => latestTriggerMs(triggers, source)).filter((ms) => ms !== null).map((ms) => ms + postMs).filter((ms) => ms >= nowMs);
|
|
12941
13015
|
if (open.length === 0) return null;
|
|
12942
13016
|
return Math.max(...open) + 1;
|
|
12943
13017
|
}
|
|
@@ -13945,6 +14019,25 @@ var PLACEMENT_TICK_MS = 3e4;
|
|
|
13945
14019
|
*/
|
|
13946
14020
|
var RECONCILE_TICK_MS = 12e4;
|
|
13947
14021
|
/**
|
|
14022
|
+
* How long a `recording-signal` hold may stand WITHOUT a read confirming it
|
|
14023
|
+
* before the reconcile releases it anyway.
|
|
14024
|
+
*
|
|
14025
|
+
* The hold is opened by an event (D8: telemetry, may be dropped) and closed by
|
|
14026
|
+
* an event; the reconcile beat re-reads the level through RPC every
|
|
14027
|
+
* {@link RECONCILE_TICK_MS} and that read is what normally keeps or ends it. This
|
|
14028
|
+
* bound is for the case where the READ itself keeps failing — the device-state
|
|
14029
|
+
* door unreachable for hours — so that one lost falling edge can never pin a
|
|
14030
|
+
* camera for ever (D11). It counts from the last confirmation (the rise event,
|
|
14031
|
+
* or the last read that said `active`), never from the rise alone: a job that
|
|
14032
|
+
* genuinely lasts longer than this, with the level re-read every two minutes,
|
|
14033
|
+
* is never cut.
|
|
14034
|
+
*
|
|
14035
|
+
* Four hours: the longest whole-house clean-and-mop observed is under three,
|
|
14036
|
+
* and a device-state door down for four hours is an outage that has already
|
|
14037
|
+
* cost more than one recording.
|
|
14038
|
+
*/
|
|
14039
|
+
var SIGNAL_HOLD_MAX_MS = 4 * 36e5;
|
|
14040
|
+
/**
|
|
13948
14041
|
* Bound on the ONE store read at the head of every placement and reconcile pass
|
|
13949
14042
|
* (`enabledDeviceIds` → `readDeviceConfigs`).
|
|
13950
14043
|
*
|
|
@@ -14115,6 +14208,18 @@ var RecordingController = class {
|
|
|
14115
14208
|
* {@link TriggerState}. */
|
|
14116
14209
|
triggers = /* @__PURE__ */ new Map();
|
|
14117
14210
|
/**
|
|
14211
|
+
* Per device, when the `recording-signal` hold was last CONFIRMED — by the
|
|
14212
|
+
* rise event or by a reconcile read that said `active`. The clock
|
|
14213
|
+
* {@link SIGNAL_HOLD_MAX_MS} counts from. Deleted with the hold.
|
|
14214
|
+
*/
|
|
14215
|
+
signalConfirmedAt = /* @__PURE__ */ new Map();
|
|
14216
|
+
/**
|
|
14217
|
+
* Per device, how many CONSECUTIVE reconcile reads have said `active: false`
|
|
14218
|
+
* while a hold stood. Reset by any read that says `active` and by the rise
|
|
14219
|
+
* event. Reaches {@link SIGNAL_RELEASE_READS} → the reconcile releases.
|
|
14220
|
+
*/
|
|
14221
|
+
signalContradictions = /* @__PURE__ */ new Map();
|
|
14222
|
+
/**
|
|
14118
14223
|
* Last sign of life per `deviceId:profile` — seeded at attach (so the grace
|
|
14119
14224
|
* window starts at the pin, not at the first segment) and stamped on every
|
|
14120
14225
|
* finalize. Read by {@link checkIdleWriters}. Stamped BEFORE the keep/discard
|
|
@@ -14386,7 +14491,8 @@ var RecordingController = class {
|
|
|
14386
14491
|
async reconcileTriggerHistory(deviceId) {
|
|
14387
14492
|
const loadMotion = this.deps.loadMotionEvents;
|
|
14388
14493
|
const loadObject = this.deps.loadObjectEvents;
|
|
14389
|
-
|
|
14494
|
+
const loadSignal = this.deps.loadDeviceSignal;
|
|
14495
|
+
if (loadMotion === void 0 && loadObject === void 0 && loadSignal === void 0 || this.stopped) return;
|
|
14390
14496
|
try {
|
|
14391
14497
|
const config = await this.deps.loadConfig(deviceId);
|
|
14392
14498
|
if (config.enabled !== true) return;
|
|
@@ -14404,6 +14510,10 @@ var RecordingController = class {
|
|
|
14404
14510
|
};
|
|
14405
14511
|
for (const source of bandTriggerSources(band)) {
|
|
14406
14512
|
if (this.stopped) return;
|
|
14513
|
+
if (source.kind === "signal") {
|
|
14514
|
+
if (loadSignal !== void 0) await this.reconcileSignalLevel(deviceId, source, postMs);
|
|
14515
|
+
continue;
|
|
14516
|
+
}
|
|
14407
14517
|
const rows = await durableRowsFor(source);
|
|
14408
14518
|
if (rows === null || rows.length === 0) continue;
|
|
14409
14519
|
if (this.stopped) return;
|
|
@@ -14436,6 +14546,164 @@ var RecordingController = class {
|
|
|
14436
14546
|
}
|
|
14437
14547
|
}
|
|
14438
14548
|
/**
|
|
14549
|
+
* Compare the `recording-signal` HOLD with the slice the device publishes
|
|
14550
|
+
* now, and correct the hold the way D49 allows: a read that raises the level
|
|
14551
|
+
* is applied at once (footage is what is at stake); a read that lowers it is
|
|
14552
|
+
* applied only when {@link SIGNAL_RELEASE_READS} consecutive reads agree; a
|
|
14553
|
+
* read that fails changes nothing — except that a hold nobody has confirmed
|
|
14554
|
+
* for {@link SIGNAL_HOLD_MAX_MS} is released anyway, and says so.
|
|
14555
|
+
*
|
|
14556
|
+
* Never throws: called from the head of the reconcile beat.
|
|
14557
|
+
*/
|
|
14558
|
+
async reconcileSignalLevel(deviceId, source, postMs) {
|
|
14559
|
+
const load = this.deps.loadDeviceSignal;
|
|
14560
|
+
if (load === void 0) return;
|
|
14561
|
+
const nowMs = this.now();
|
|
14562
|
+
const since = heldSinceMs(this.triggersFor(deviceId), source);
|
|
14563
|
+
let level;
|
|
14564
|
+
try {
|
|
14565
|
+
level = await load(deviceId);
|
|
14566
|
+
} catch (err) {
|
|
14567
|
+
this.deps.logger.warn("recorder: device signal read failed — the hold stands", {
|
|
14568
|
+
tags: { deviceId },
|
|
14569
|
+
meta: {
|
|
14570
|
+
held: since !== null,
|
|
14571
|
+
error: errMsg(err)
|
|
14572
|
+
}
|
|
14573
|
+
});
|
|
14574
|
+
this.releaseIfHeldPastBound(deviceId, source, since, postMs, nowMs);
|
|
14575
|
+
return;
|
|
14576
|
+
}
|
|
14577
|
+
if (this.stopped) return;
|
|
14578
|
+
if (level?.active === true) {
|
|
14579
|
+
this.signalConfirmedAt.set(deviceId, nowMs);
|
|
14580
|
+
this.signalContradictions.delete(deviceId);
|
|
14581
|
+
if (since !== null) return;
|
|
14582
|
+
const rise = level.lastChangedAt > 0 ? Math.min(level.lastChangedAt, nowMs) : nowMs;
|
|
14583
|
+
this.triggers.set(deviceId, holdTrigger(this.triggersFor(deviceId), source, rise));
|
|
14584
|
+
this.deps.logger.info("recorder: device signal recovered from the slice — the bus had not delivered the rise", {
|
|
14585
|
+
tags: { deviceId },
|
|
14586
|
+
meta: {
|
|
14587
|
+
since: rise,
|
|
14588
|
+
lastChangedAt: level.lastChangedAt
|
|
14589
|
+
}
|
|
14590
|
+
});
|
|
14591
|
+
return;
|
|
14592
|
+
}
|
|
14593
|
+
if (since === null) return;
|
|
14594
|
+
const contradictions = (this.signalContradictions.get(deviceId) ?? 0) + 1;
|
|
14595
|
+
if (contradictions < 2) {
|
|
14596
|
+
this.signalContradictions.set(deviceId, contradictions);
|
|
14597
|
+
this.deps.logger.debug("recorder: device signal read contradicts the hold — waiting for a second read to agree", {
|
|
14598
|
+
tags: { deviceId },
|
|
14599
|
+
meta: {
|
|
14600
|
+
since,
|
|
14601
|
+
reads: contradictions,
|
|
14602
|
+
absent: level === null
|
|
14603
|
+
}
|
|
14604
|
+
});
|
|
14605
|
+
return;
|
|
14606
|
+
}
|
|
14607
|
+
this.applySignalRelease(deviceId, source, nowMs, postMs);
|
|
14608
|
+
this.deps.logger.info("recorder: device signal released by reconcile — two reads agreed the level is down", {
|
|
14609
|
+
tags: { deviceId },
|
|
14610
|
+
meta: {
|
|
14611
|
+
since,
|
|
14612
|
+
heldMs: nowMs - since,
|
|
14613
|
+
absent: level === null
|
|
14614
|
+
}
|
|
14615
|
+
});
|
|
14616
|
+
}
|
|
14617
|
+
/** The bound on a hold the reads could not confirm. Warns, because it DROPS
|
|
14618
|
+
* the rest of an episode the operator may still want. */
|
|
14619
|
+
releaseIfHeldPastBound(deviceId, source, since, postMs, nowMs) {
|
|
14620
|
+
if (since === null) return;
|
|
14621
|
+
const confirmedAt = this.signalConfirmedAt.get(deviceId) ?? since;
|
|
14622
|
+
if (nowMs - confirmedAt <= 144e5) return;
|
|
14623
|
+
this.applySignalRelease(deviceId, source, nowMs, postMs);
|
|
14624
|
+
this.deps.logger.warn("recorder: device signal held past its bound with no read confirming it — released, the episode may be cut", {
|
|
14625
|
+
tags: { deviceId },
|
|
14626
|
+
meta: {
|
|
14627
|
+
since,
|
|
14628
|
+
confirmedAt,
|
|
14629
|
+
heldMs: nowMs - since,
|
|
14630
|
+
boundMs: SIGNAL_HOLD_MAX_MS
|
|
14631
|
+
}
|
|
14632
|
+
});
|
|
14633
|
+
}
|
|
14634
|
+
/** Lower the hold into a window and forget the per-device signal clocks. */
|
|
14635
|
+
applySignalRelease(deviceId, source, atMs, postMs) {
|
|
14636
|
+
const pruned = pruneTriggerHistory(this.triggersFor(deviceId), postMs, atMs);
|
|
14637
|
+
this.triggers.set(deviceId, releaseTrigger(pruned, source, atMs, postMs));
|
|
14638
|
+
this.signalConfirmedAt.delete(deviceId);
|
|
14639
|
+
this.signalContradictions.delete(deviceId);
|
|
14640
|
+
}
|
|
14641
|
+
/**
|
|
14642
|
+
* Feed the camera's own `recording-signal` LEVEL — the counterpart of
|
|
14643
|
+
* `onTrigger` for `RecordingTriggers.deviceSignal`, fed from the
|
|
14644
|
+
* `DeviceStateChanged` subscription in `recorder/addon/index.ts`.
|
|
14645
|
+
*
|
|
14646
|
+
* `active: true` HOLDS the source open (idempotent: a re-emitted slice never
|
|
14647
|
+
* moves the rise); `active: false` releases it into a window ending at `atMs`,
|
|
14648
|
+
* after which the band's `postBufferSec` runs and `evaluateDevice` arms the
|
|
14649
|
+
* close exactly as it does for a motion episode. Both converge at once: the
|
|
14650
|
+
* rise is what wakes a battery camera and pins its source (the robot's
|
|
14651
|
+
* monitor opens through `battery.wakeForStream` on the attach), the fall is
|
|
14652
|
+
* what lets the source be released and the monitor close.
|
|
14653
|
+
*
|
|
14654
|
+
* No config read on this path (D49): the hold is recorded unconditionally —
|
|
14655
|
+
* it costs one map entry — and `evaluateDevice`, which reads the last-known
|
|
14656
|
+
* config, decides whether any band listens. A camera whose bands do not name
|
|
14657
|
+
* the signal simply never demands.
|
|
14658
|
+
*/
|
|
14659
|
+
async onSignalLevel(deviceId, source, active, atMs) {
|
|
14660
|
+
if (this.stopped) return;
|
|
14661
|
+
const before = this.triggersFor(deviceId);
|
|
14662
|
+
if (active) {
|
|
14663
|
+
const held = holdTrigger(before, source, atMs);
|
|
14664
|
+
this.signalConfirmedAt.set(deviceId, this.now());
|
|
14665
|
+
this.signalContradictions.delete(deviceId);
|
|
14666
|
+
if (held !== before) {
|
|
14667
|
+
this.triggers.set(deviceId, held);
|
|
14668
|
+
this.deps.logger.info("recorder: device signal raised — recording window held open", {
|
|
14669
|
+
tags: { deviceId },
|
|
14670
|
+
meta: {
|
|
14671
|
+
source: triggerKey(source),
|
|
14672
|
+
atMs
|
|
14673
|
+
}
|
|
14674
|
+
});
|
|
14675
|
+
}
|
|
14676
|
+
} else {
|
|
14677
|
+
const postMs = await this.postBufferMsFor(deviceId, atMs);
|
|
14678
|
+
if (this.stopped) return;
|
|
14679
|
+
const since = heldSinceMs(this.triggersFor(deviceId), source);
|
|
14680
|
+
if (since === null) return;
|
|
14681
|
+
this.applySignalRelease(deviceId, source, atMs, postMs);
|
|
14682
|
+
this.deps.logger.info("recorder: device signal fell — post-buffer running, then the window closes", {
|
|
14683
|
+
tags: { deviceId },
|
|
14684
|
+
meta: {
|
|
14685
|
+
source: triggerKey(source),
|
|
14686
|
+
since,
|
|
14687
|
+
atMs,
|
|
14688
|
+
heldMs: atMs - since,
|
|
14689
|
+
postMs
|
|
14690
|
+
}
|
|
14691
|
+
});
|
|
14692
|
+
}
|
|
14693
|
+
try {
|
|
14694
|
+
await this.evaluateDevice(deviceId);
|
|
14695
|
+
} catch (err) {
|
|
14696
|
+
this.deps.logger.warn("recorder: device signal attach deferred — stream not available yet (retries on next broker ready/reconcile)", {
|
|
14697
|
+
tags: { deviceId },
|
|
14698
|
+
meta: {
|
|
14699
|
+
source: triggerKey(source),
|
|
14700
|
+
active,
|
|
14701
|
+
error: errMsg(err)
|
|
14702
|
+
}
|
|
14703
|
+
});
|
|
14704
|
+
}
|
|
14705
|
+
}
|
|
14706
|
+
/**
|
|
14439
14707
|
* Re-evaluate one device after a config change (called from `setDeviceConfig`).
|
|
14440
14708
|
* Failures propagate so the caller can surface them, but the persisted config
|
|
14441
14709
|
* has already been written by the time this runs.
|
|
@@ -16538,6 +16806,13 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16538
16806
|
*/
|
|
16539
16807
|
sensorTriggerUnsub = null;
|
|
16540
16808
|
/**
|
|
16809
|
+
* Unsubscribe handle for the `DeviceStateChanged` subscription filtered on
|
|
16810
|
+
* the camera's own `recording-signal` slice — the feed behind
|
|
16811
|
+
* `RecordingTriggers.deviceSignal`. A LEVEL, so it has no edge memory of its
|
|
16812
|
+
* own: the controller holds and releases (`onSignalLevel`).
|
|
16813
|
+
*/
|
|
16814
|
+
signalTriggerUnsub = null;
|
|
16815
|
+
/**
|
|
16541
16816
|
* Edge memory + reverse index (source device → cameras) for the sensor
|
|
16542
16817
|
* trigger. Constructed once per addon instance: `startedAtMs` is the floor a
|
|
16543
16818
|
* first-sighting rise must beat, and a fresh instance per boot is exactly
|
|
@@ -16789,6 +17064,26 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16789
17064
|
limit: TRIGGER_RECONCILE_ROW_LIMIT
|
|
16790
17065
|
})).filter((event) => (event.source ?? "pipeline") === "pipeline").map((event) => event.timestamp);
|
|
16791
17066
|
},
|
|
17067
|
+
/**
|
|
17068
|
+
* The DURABLE half of the DEVICE-SIGNAL source: the camera's own
|
|
17069
|
+
* `recording-signal` slice, read back through the kernel's state door.
|
|
17070
|
+
* `null` when the device has never written it; a slice that does not
|
|
17071
|
+
* parse is a REJECTION, not a level — the reconcile must not read
|
|
17072
|
+
* garbage as "down" and release on it.
|
|
17073
|
+
*/
|
|
17074
|
+
loadDeviceSignal: async (deviceId) => {
|
|
17075
|
+
const slice = await this.ctx.api.deviceState.getCapSlice.query({
|
|
17076
|
+
deviceId,
|
|
17077
|
+
capName: recordingSignalCapability.name
|
|
17078
|
+
});
|
|
17079
|
+
if (slice === null) return null;
|
|
17080
|
+
const parsed = RecordingSignalStatusSchema.safeParse(slice);
|
|
17081
|
+
if (!parsed.success) throw new Error(`recording-signal slice for device ${deviceId} does not parse`);
|
|
17082
|
+
return {
|
|
17083
|
+
active: parsed.data.active,
|
|
17084
|
+
lastChangedAt: parsed.data.lastChangedAt
|
|
17085
|
+
};
|
|
17086
|
+
},
|
|
16792
17087
|
locations: () => this.resolvedLocations,
|
|
16793
17088
|
placeProfile: (deviceId, profile) => this.placeProfile(deviceId, profile),
|
|
16794
17089
|
onPlacementTick: (deviceIds) => this.recomputePlacement(deviceIds),
|
|
@@ -17124,6 +17419,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
17124
17419
|
this.detectionTriggerUnsub = null;
|
|
17125
17420
|
this.sensorTriggerUnsub?.();
|
|
17126
17421
|
this.sensorTriggerUnsub = null;
|
|
17422
|
+
this.signalTriggerUnsub?.();
|
|
17423
|
+
this.signalTriggerUnsub = null;
|
|
17127
17424
|
await this.controller?.stop();
|
|
17128
17425
|
this.controller = null;
|
|
17129
17426
|
this.segmentStore = null;
|
|
@@ -17296,6 +17593,20 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
17296
17593
|
});
|
|
17297
17594
|
});
|
|
17298
17595
|
}
|
|
17596
|
+
if (this.signalTriggerUnsub === null) this.signalTriggerUnsub = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
|
|
17597
|
+
const parsed = parseSensorStateChanged(event.data);
|
|
17598
|
+
if (parsed === null || parsed.capName !== recordingSignalCapability.name) return;
|
|
17599
|
+
const level = RecordingSignalStatusSchema.safeParse(parsed.slice);
|
|
17600
|
+
if (!level.success) {
|
|
17601
|
+
this.ctx.logger.warn("recorder: device signal slice does not parse — ignored", {
|
|
17602
|
+
tags: { deviceId: parsed.deviceId },
|
|
17603
|
+
meta: { slice: parsed.slice }
|
|
17604
|
+
});
|
|
17605
|
+
return;
|
|
17606
|
+
}
|
|
17607
|
+
const atMs = event.timestamp instanceof Date ? event.timestamp.getTime() : Date.now();
|
|
17608
|
+
this.controller?.onSignalLevel(parsed.deviceId, { kind: "signal" }, level.data.active, atMs);
|
|
17609
|
+
});
|
|
17299
17610
|
try {
|
|
17300
17611
|
await this.segmentHours?.load();
|
|
17301
17612
|
this.segmentHours?.repairAggregates();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-HG5geL9t.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 {
|
|
2
|
+
import { Et as errMsg } from "./dist-mYDK58Jp.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-
|
|
3
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
2
|
+
const require_worker_protocol = require("../worker-protocol-DCsi4YS1.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.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.
|
|
@@ -450,6 +450,17 @@ var LeaseAdmissionLedger = class {
|
|
|
450
450
|
get markedSize() {
|
|
451
451
|
return this.marked.size;
|
|
452
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* The runner is DONE with `frameId` before the worker ever leased it — its
|
|
455
|
+
* result came back (or it was dropped) before the next pull superseded it.
|
|
456
|
+
* Withdrawing the mark makes the supersede decline the frame, so it is
|
|
457
|
+
* freed at once instead of leased after its own release and pinned until
|
|
458
|
+
* the bound evicts it. Measured 2026-09-06: on every busy worker
|
|
459
|
+
* `leaseReleaseMisses ≈ leaseAdmitted` and `holdOverflow ≈ admitted − 8`.
|
|
460
|
+
*/
|
|
461
|
+
unmark(frameId) {
|
|
462
|
+
this.marked.delete(frameId);
|
|
463
|
+
}
|
|
453
464
|
/** Whether `frameId` is inside the remembered marked window. */
|
|
454
465
|
isMarked(frameId) {
|
|
455
466
|
return this.marked.has(frameId);
|
|
@@ -656,7 +667,7 @@ function toBufferMissRung(facts) {
|
|
|
656
667
|
/** The error line the runner sees: the rung first, the numbers after it. */
|
|
657
668
|
function describeToBufferMiss(facts) {
|
|
658
669
|
const rung = toBufferMissRung(facts);
|
|
659
|
-
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}}`;
|
|
670
|
+
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
|
|
660
671
|
}
|
|
661
672
|
//#endregion
|
|
662
673
|
//#region src/session-decode/native-tile-geometry.ts
|
|
@@ -1453,6 +1464,8 @@ var NATIVE_TILE_JPEG_QUALITY = 90;
|
|
|
1453
1464
|
* eager capture) — it does not disable retention. Default 15000.
|
|
1454
1465
|
*/
|
|
1455
1466
|
var NATIVE_LEASE_ACTIVITY_WINDOW_MS = NATIVE_LEASE_KNOBS.values.activityMs;
|
|
1467
|
+
/** How many early-withdrawn frame ids the worker remembers for the miss line. */
|
|
1468
|
+
var UNMARKED_MEMORY = 256;
|
|
1456
1469
|
/**
|
|
1457
1470
|
* How long lease capture stays off after the download path proves broken, and
|
|
1458
1471
|
* the ceiling that backoff climbs to. Doubling from the base, so the Nth
|
|
@@ -1886,6 +1899,10 @@ var DecodeWorkerChild = class {
|
|
|
1886
1899
|
/** Releases that found a held frame / that found nothing to release. */
|
|
1887
1900
|
leaseReleased = 0;
|
|
1888
1901
|
leaseReleaseMisses = 0;
|
|
1902
|
+
/** Releases that beat the supersede and withdrew the mark instead. */
|
|
1903
|
+
leaseReleasedEarly = 0;
|
|
1904
|
+
/** Frame ids withdrawn early → by whom (insertion-ordered, bounded). */
|
|
1905
|
+
unmarkedBy = /* @__PURE__ */ new Map();
|
|
1889
1906
|
/** Child-side skips only (HW-guard drops + probe/degrade frees); slot drops add {@link FrameSlot.droppedCount}. */
|
|
1890
1907
|
framesSkipped = 0;
|
|
1891
1908
|
/** Frames actually handed to the parent as a `frame` reply. */
|
|
@@ -1970,7 +1987,7 @@ var DecodeWorkerChild = class {
|
|
|
1970
1987
|
this.handleCutTiles(message.frameId, message.bboxes);
|
|
1971
1988
|
return;
|
|
1972
1989
|
case "releaseNativeLease":
|
|
1973
|
-
this.noteLeaseRelease(this.leaseStore.release(message.frameId));
|
|
1990
|
+
this.noteLeaseRelease(message.frameId, this.leaseStore.release(message.frameId), "release");
|
|
1974
1991
|
return;
|
|
1975
1992
|
case "markInferred":
|
|
1976
1993
|
this.admission.mark(message.frameId);
|
|
@@ -2034,7 +2051,7 @@ var DecodeWorkerChild = class {
|
|
|
2034
2051
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
2035
2052
|
const rssMb = Math.round(process.memoryUsage().rss / 1048576);
|
|
2036
2053
|
const ageS = Math.round((now - this.startedAt) / 1e3);
|
|
2037
|
-
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2054
|
+
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2038
2055
|
this.lastMetricsAt = now;
|
|
2039
2056
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
2040
2057
|
if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
|
|
@@ -2072,9 +2089,26 @@ var DecodeWorkerChild = class {
|
|
|
2072
2089
|
this.pendingPull = true;
|
|
2073
2090
|
this.frames.markPendingPull();
|
|
2074
2091
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2092
|
+
/**
|
|
2093
|
+
* Account for a release. A release that found no lease is the runner being
|
|
2094
|
+
* done BEFORE the supersede leased the frame: withdraw its mark so the
|
|
2095
|
+
* supersede declines it (`LeaseAdmissionLedger.unmark`), and count it.
|
|
2096
|
+
*/
|
|
2097
|
+
noteLeaseRelease(frameId, held, by) {
|
|
2098
|
+
if (held) {
|
|
2099
|
+
this.leaseReleased += 1;
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
this.leaseReleaseMisses += 1;
|
|
2103
|
+
if (this.admission.isMarked(frameId)) {
|
|
2104
|
+
this.admission.unmark(frameId);
|
|
2105
|
+
this.leaseReleasedEarly += 1;
|
|
2106
|
+
this.unmarkedBy.set(frameId, by);
|
|
2107
|
+
if (this.unmarkedBy.size > UNMARKED_MEMORY) {
|
|
2108
|
+
const oldest = this.unmarkedBy.keys().next().value;
|
|
2109
|
+
if (oldest !== void 0) this.unmarkedBy.delete(oldest);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2078
2112
|
}
|
|
2079
2113
|
async handleToBuffer(frameId, opts) {
|
|
2080
2114
|
const frame = this.leaseStore.get(frameId)?.frame ?? this.frames.toBuffer(frameId);
|
|
@@ -2085,7 +2119,8 @@ var DecodeWorkerChild = class {
|
|
|
2085
2119
|
marked: this.admission.isMarked(frameId),
|
|
2086
2120
|
leaseFrames: this.leaseStore.size,
|
|
2087
2121
|
holdFrames: NATIVE_LEASE_HOLD_FRAMES,
|
|
2088
|
-
holdOverflow: this.leaseStore.overflowCount
|
|
2122
|
+
holdOverflow: this.leaseStore.overflowCount,
|
|
2123
|
+
...this.unmarkedBy.has(frameId) ? { unmarkedBy: this.unmarkedBy.get(frameId) } : {}
|
|
2089
2124
|
}), frameId);
|
|
2090
2125
|
return;
|
|
2091
2126
|
}
|
|
@@ -2320,7 +2355,7 @@ var DecodeWorkerChild = class {
|
|
|
2320
2355
|
this.sceneStore.put(frameId, [scene]);
|
|
2321
2356
|
}
|
|
2322
2357
|
} finally {
|
|
2323
|
-
this.noteLeaseRelease(this.leaseStore.release(frameId));
|
|
2358
|
+
this.noteLeaseRelease(frameId, this.leaseStore.release(frameId), "cut");
|
|
2324
2359
|
}
|
|
2325
2360
|
}
|
|
2326
2361
|
/**
|