@camstack/addon-pipeline 1.2.50 → 1.2.52
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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-Ci2-N-XC.js → dist-CzFJeiG1.js} +16214 -16021
- package/dist/{dist-hRt5QB5i.mjs → dist-gB3o0cyM.mjs} +16122 -15935
- package/dist/{event-loop-stall-monitor-B_EOn2Q1.js → event-loop-stall-monitor-DSPDQYjc.js} +1 -1
- package/dist/{event-loop-stall-monitor-DUSB3ymZ.mjs → event-loop-stall-monitor-PxIaVeyh.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +88 -18
- package/dist/pipeline-runner/index.mjs +88 -18
- package/dist/recorder/index.js +91 -4
- package/dist/recorder/index.mjs +91 -4
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DwYxOvHx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DAeBhR8B.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bl9zivOk.mjs +26 -0
- package/dist/stream-broker/{hostInit-yLEcTDS8.mjs → hostInit-DniAXKLy.mjs} +2 -2
- package/dist/stream-broker/index.js +336 -311
- package/dist/stream-broker/index.mjs +336 -311
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CxpdncMB.js → worker-protocol-BQlELGzv.js} +1 -1
- package/dist/{worker-protocol-D_6eAFbv.mjs → worker-protocol-BhhX7J4-.mjs} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-kAtChdz2.js → MaskShapeCanvas-DI4BY7W2-B1gpA9sI.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DEwkvjzt.js → MotionZonesSettings-NcxxQN8r-DV6vvNDq.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js → PrivacyMaskSettings-APgPLF7p-BmUdRoZ9.js} +1 -1
- package/embed-dist/assets/{index-Dv7fSuDA.js → index-DsGzhGCW.js} +12 -12
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FL2mrnl7.mjs +0 -26
package/dist/{event-loop-stall-monitor-DUSB3ymZ.mjs → event-loop-stall-monitor-PxIaVeyh.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { R as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-gB3o0cyM.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";
|
|
@@ -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-
|
|
5
|
+
const require_dist = require("../dist-CzFJeiG1.js");
|
|
6
6
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { L as evaluateZoneRules, V as motionDetectionCapability, dt as hydrateSchema, lt as DeviceType, ot as BaseAddon } from "../dist-gB3o0cyM.mjs";
|
|
2
2
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
6
|
+
const require_dist = require("../dist-CzFJeiG1.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DSPDQYjc.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-BQlELGzv.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
11
11
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
@@ -543,11 +543,35 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
543
543
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
544
544
|
var SIGKILL_FALLBACK_MS = 1e3;
|
|
545
545
|
/**
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
546
|
+
* How often the cold-start wait REPORTS that it is still waiting. Task 0
|
|
547
|
+
* live-measured cold-start (restream dial + hwaccel context + keyframe wait) at
|
|
548
|
+
* 5-8s, so this must tolerate a slow-but-healthy start.
|
|
549
|
+
*
|
|
550
|
+
* It is no longer the point at which the session is abandoned — see
|
|
551
|
+
* {@link DEFAULT_FIRST_FRAME_MAX_WAIT_MS}.
|
|
549
552
|
*/
|
|
550
553
|
var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
554
|
+
/**
|
|
555
|
+
* Hard ceiling on the TOTAL wait for the FIRST frame of a dial.
|
|
556
|
+
*
|
|
557
|
+
* The first decodable frame is not a fixed cost: it is one IDR interval plus
|
|
558
|
+
* every GOP the decoder has to discard. Measured on device 618 (2560x1440
|
|
559
|
+
* h264, 8.0 s IDR interval, `Invalid NAL unit` / `corrupt decoded frame` in its
|
|
560
|
+
* bitstream), dial → first decoded frame over the broker's own muted restream
|
|
561
|
+
* was 1.8 / 6.5 / 7.0 / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 /
|
|
562
|
+
* >40 s across 12 trials — against a healthy 587's 2.0 s ± 0.02.
|
|
563
|
+
*
|
|
564
|
+
* Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
|
|
565
|
+
* recovery made it worse: ending the session RE-DIALS, which throws the
|
|
566
|
+
* accumulated wait away and re-pays the RTSP setup + probe from zero, and two
|
|
567
|
+
* ends inside 60 s DETACH the camera. That loop cost 618 25.9% of its assigned
|
|
568
|
+
* decode airtime in one day (325 of 1257 ten-second windows delivered nothing).
|
|
569
|
+
*
|
|
570
|
+
* The wait is bounded instead of deadlined: the worker keeps the outstanding
|
|
571
|
+
* `pull` latched, so re-arming costs nothing but the wait, and the ceiling is
|
|
572
|
+
* what stops a genuinely dead input from holding a spawn permit forever.
|
|
573
|
+
*/
|
|
574
|
+
var DEFAULT_FIRST_FRAME_MAX_WAIT_MS = 45e3;
|
|
551
575
|
/** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
|
|
552
576
|
var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
|
|
553
577
|
/** Narrow an 'exit' payload to {@link WorkerExitInfo} (fakes may emit undefined). */
|
|
@@ -567,8 +591,10 @@ function toError(err) {
|
|
|
567
591
|
var SessionDecodeSession = class {
|
|
568
592
|
child;
|
|
569
593
|
logger;
|
|
594
|
+
deviceId;
|
|
570
595
|
firstFrameTimeoutMs;
|
|
571
596
|
frameTimeoutMs;
|
|
597
|
+
firstFrameMaxWaitMs;
|
|
572
598
|
pendingPull = null;
|
|
573
599
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
574
600
|
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
@@ -580,11 +606,15 @@ var SessionDecodeSession = class {
|
|
|
580
606
|
sigkillTimer = null;
|
|
581
607
|
pullTimeoutTimer = null;
|
|
582
608
|
firstFrameReceived = false;
|
|
583
|
-
|
|
609
|
+
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
610
|
+
firstPullAt = 0;
|
|
611
|
+
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs) {
|
|
584
612
|
this.child = child;
|
|
585
613
|
this.logger = logger;
|
|
614
|
+
this.deviceId = deviceId;
|
|
586
615
|
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
587
616
|
this.frameTimeoutMs = frameTimeoutMs;
|
|
617
|
+
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
588
618
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
589
619
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
590
620
|
}
|
|
@@ -604,6 +634,7 @@ var SessionDecodeSession = class {
|
|
|
604
634
|
*/
|
|
605
635
|
pullNext() {
|
|
606
636
|
if (this.exited) return Promise.resolve({ kind: "ended" });
|
|
637
|
+
if (this.firstPullAt === 0) this.firstPullAt = Date.now();
|
|
607
638
|
return new Promise((resolve, reject) => {
|
|
608
639
|
this.pendingPull = {
|
|
609
640
|
resolve,
|
|
@@ -785,25 +816,63 @@ var SessionDecodeSession = class {
|
|
|
785
816
|
}
|
|
786
817
|
/**
|
|
787
818
|
* Fires when a `pull` round-trip outlives its liveness timeout — the child
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
819
|
+
* has not produced a frame and has not exited.
|
|
820
|
+
*
|
|
821
|
+
* BEFORE the first frame this is not yet a failure. The worker latches the
|
|
822
|
+
* outstanding `pull` (`decode-worker-child.handlePull` sets `pendingPull` and
|
|
823
|
+
* delivers on the next decoded frame), so re-arming the timer costs nothing
|
|
824
|
+
* but the wait and the session keeps the dial, the RTSP setup and the probe
|
|
825
|
+
* it has already paid for. Only the {@link firstFrameMaxWaitMs} ceiling ends
|
|
826
|
+
* it — see that constant for the measurement that set it.
|
|
827
|
+
*
|
|
828
|
+
* AFTER the first frame the deadline is unchanged: a steady stream that stops
|
|
829
|
+
* is a hung decode, and ending settles the outstanding pull as `ended` (not a
|
|
830
|
+
* throw) so the consumer's async generator resumes cleanly, runs its
|
|
831
|
+
* `finally`, and tears the child down via {@link teardown}.
|
|
832
|
+
*
|
|
833
|
+
* Every line here carries `tags: { deviceId }` and a `reason`, because
|
|
834
|
+
* "detaching" a camera is the loudest dropped work in this path and an
|
|
835
|
+
* operator has to be able to answer "why did 618 stop" from the logs alone.
|
|
793
836
|
*/
|
|
794
837
|
handlePullTimeout(timeoutMs) {
|
|
795
838
|
this.pullTimeoutTimer = null;
|
|
796
839
|
const waiter = this.pendingPull;
|
|
797
840
|
if (!waiter) return;
|
|
841
|
+
const waitedMs = this.firstPullAt === 0 ? timeoutMs : Date.now() - this.firstPullAt;
|
|
842
|
+
if (!this.firstFrameReceived && waitedMs < this.firstFrameMaxWaitMs) {
|
|
843
|
+
this.logger.warn("session-decode-coordinator: still waiting for its first decodable frame — holding the dial", {
|
|
844
|
+
tags: { deviceId: this.deviceId },
|
|
845
|
+
meta: {
|
|
846
|
+
waitedMs,
|
|
847
|
+
reportEveryMs: timeoutMs,
|
|
848
|
+
maxWaitMs: this.firstFrameMaxWaitMs,
|
|
849
|
+
hint: "long IDR interval or a discarded GOP looks exactly like this"
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
this.armPullTimeout();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
798
855
|
this.pendingPull = null;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
856
|
+
const reason = this.firstFrameReceived ? "stalled-after-first-frame" : "no-first-frame";
|
|
857
|
+
this.logger.warn(this.firstFrameReceived ? "session-decode-coordinator: gave up waiting for the next frame — ending session" : "session-decode-coordinator: gave up waiting for the first decodable frame — ending session", {
|
|
858
|
+
tags: { deviceId: this.deviceId },
|
|
859
|
+
meta: {
|
|
860
|
+
reason,
|
|
861
|
+
waitedMs,
|
|
862
|
+
timeoutMs,
|
|
863
|
+
...this.firstFrameReceived ? {} : { maxWaitMs: this.firstFrameMaxWaitMs }
|
|
864
|
+
}
|
|
865
|
+
});
|
|
803
866
|
waiter.resolve({ kind: "ended" });
|
|
804
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Arm the next round-trip deadline. Before the first frame the timer is
|
|
870
|
+
* clamped to what is LEFT of the ceiling, so the give-up lands exactly on
|
|
871
|
+
* {@link firstFrameMaxWaitMs} instead of on the next report boundary past it
|
|
872
|
+
* — otherwise a 45 s ceiling reported every 20 s would really give up at 60 s.
|
|
873
|
+
*/
|
|
805
874
|
armPullTimeout() {
|
|
806
|
-
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : this.firstFrameTimeoutMs;
|
|
875
|
+
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : Math.max(1, Math.min(this.firstFrameTimeoutMs, this.firstFrameMaxWaitMs - (this.firstPullAt === 0 ? 0 : Date.now() - this.firstPullAt)));
|
|
807
876
|
this.pullTimeoutTimer = setTimeout(() => this.handlePullTimeout(timeoutMs), timeoutMs);
|
|
808
877
|
this.pullTimeoutTimer.unref?.();
|
|
809
878
|
}
|
|
@@ -925,8 +994,9 @@ function buildFrameImage(session, reply) {
|
|
|
925
994
|
function createSessionDecodeCoordinator(deps) {
|
|
926
995
|
const firstFrameTimeoutMs = deps.firstFrameTimeoutMs ?? DEFAULT_FIRST_FRAME_TIMEOUT_MS;
|
|
927
996
|
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
997
|
+
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
928
998
|
return { async *generateVideoFrames(source, opts) {
|
|
929
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, firstFrameTimeoutMs, frameTimeoutMs);
|
|
999
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs);
|
|
930
1000
|
session.start(source, opts);
|
|
931
1001
|
try {
|
|
932
1002
|
while (true) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-
|
|
3
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { A as defineCustomActions, Dt as object, Et as number, G as pipelineRunnerCapability, H as pickDetailCropConvention, O as customAction, St as boolean, Tt as literal, U as pickNativeLeaseOverride, bt as _enum, et as errMsg, ht as nodePin, j as deriveDetailCropRect, jt as EventCategory, kt as string, ot as BaseAddon, s as DEFAULT_DETAIL_CROP_CONVENTION, u as DEVICE_BACKEND_TO_FORMAT, ut as createEvent, wt as lazy, xt as array } from "../dist-gB3o0cyM.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-PxIaVeyh.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BhhX7J4-.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
6
6
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
@@ -536,11 +536,35 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
536
536
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
537
537
|
var SIGKILL_FALLBACK_MS = 1e3;
|
|
538
538
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
539
|
+
* How often the cold-start wait REPORTS that it is still waiting. Task 0
|
|
540
|
+
* live-measured cold-start (restream dial + hwaccel context + keyframe wait) at
|
|
541
|
+
* 5-8s, so this must tolerate a slow-but-healthy start.
|
|
542
|
+
*
|
|
543
|
+
* It is no longer the point at which the session is abandoned — see
|
|
544
|
+
* {@link DEFAULT_FIRST_FRAME_MAX_WAIT_MS}.
|
|
542
545
|
*/
|
|
543
546
|
var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
547
|
+
/**
|
|
548
|
+
* Hard ceiling on the TOTAL wait for the FIRST frame of a dial.
|
|
549
|
+
*
|
|
550
|
+
* The first decodable frame is not a fixed cost: it is one IDR interval plus
|
|
551
|
+
* every GOP the decoder has to discard. Measured on device 618 (2560x1440
|
|
552
|
+
* h264, 8.0 s IDR interval, `Invalid NAL unit` / `corrupt decoded frame` in its
|
|
553
|
+
* bitstream), dial → first decoded frame over the broker's own muted restream
|
|
554
|
+
* was 1.8 / 6.5 / 7.0 / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 /
|
|
555
|
+
* >40 s across 12 trials — against a healthy 587's 2.0 s ± 0.02.
|
|
556
|
+
*
|
|
557
|
+
* Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
|
|
558
|
+
* recovery made it worse: ending the session RE-DIALS, which throws the
|
|
559
|
+
* accumulated wait away and re-pays the RTSP setup + probe from zero, and two
|
|
560
|
+
* ends inside 60 s DETACH the camera. That loop cost 618 25.9% of its assigned
|
|
561
|
+
* decode airtime in one day (325 of 1257 ten-second windows delivered nothing).
|
|
562
|
+
*
|
|
563
|
+
* The wait is bounded instead of deadlined: the worker keeps the outstanding
|
|
564
|
+
* `pull` latched, so re-arming costs nothing but the wait, and the ceiling is
|
|
565
|
+
* what stops a genuinely dead input from holding a spawn permit forever.
|
|
566
|
+
*/
|
|
567
|
+
var DEFAULT_FIRST_FRAME_MAX_WAIT_MS = 45e3;
|
|
544
568
|
/** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
|
|
545
569
|
var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
|
|
546
570
|
/** Narrow an 'exit' payload to {@link WorkerExitInfo} (fakes may emit undefined). */
|
|
@@ -560,8 +584,10 @@ function toError(err) {
|
|
|
560
584
|
var SessionDecodeSession = class {
|
|
561
585
|
child;
|
|
562
586
|
logger;
|
|
587
|
+
deviceId;
|
|
563
588
|
firstFrameTimeoutMs;
|
|
564
589
|
frameTimeoutMs;
|
|
590
|
+
firstFrameMaxWaitMs;
|
|
565
591
|
pendingPull = null;
|
|
566
592
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
567
593
|
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
@@ -573,11 +599,15 @@ var SessionDecodeSession = class {
|
|
|
573
599
|
sigkillTimer = null;
|
|
574
600
|
pullTimeoutTimer = null;
|
|
575
601
|
firstFrameReceived = false;
|
|
576
|
-
|
|
602
|
+
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
603
|
+
firstPullAt = 0;
|
|
604
|
+
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs) {
|
|
577
605
|
this.child = child;
|
|
578
606
|
this.logger = logger;
|
|
607
|
+
this.deviceId = deviceId;
|
|
579
608
|
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
580
609
|
this.frameTimeoutMs = frameTimeoutMs;
|
|
610
|
+
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
581
611
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
582
612
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
583
613
|
}
|
|
@@ -597,6 +627,7 @@ var SessionDecodeSession = class {
|
|
|
597
627
|
*/
|
|
598
628
|
pullNext() {
|
|
599
629
|
if (this.exited) return Promise.resolve({ kind: "ended" });
|
|
630
|
+
if (this.firstPullAt === 0) this.firstPullAt = Date.now();
|
|
600
631
|
return new Promise((resolve, reject) => {
|
|
601
632
|
this.pendingPull = {
|
|
602
633
|
resolve,
|
|
@@ -778,25 +809,63 @@ var SessionDecodeSession = class {
|
|
|
778
809
|
}
|
|
779
810
|
/**
|
|
780
811
|
* Fires when a `pull` round-trip outlives its liveness timeout — the child
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
812
|
+
* has not produced a frame and has not exited.
|
|
813
|
+
*
|
|
814
|
+
* BEFORE the first frame this is not yet a failure. The worker latches the
|
|
815
|
+
* outstanding `pull` (`decode-worker-child.handlePull` sets `pendingPull` and
|
|
816
|
+
* delivers on the next decoded frame), so re-arming the timer costs nothing
|
|
817
|
+
* but the wait and the session keeps the dial, the RTSP setup and the probe
|
|
818
|
+
* it has already paid for. Only the {@link firstFrameMaxWaitMs} ceiling ends
|
|
819
|
+
* it — see that constant for the measurement that set it.
|
|
820
|
+
*
|
|
821
|
+
* AFTER the first frame the deadline is unchanged: a steady stream that stops
|
|
822
|
+
* is a hung decode, and ending settles the outstanding pull as `ended` (not a
|
|
823
|
+
* throw) so the consumer's async generator resumes cleanly, runs its
|
|
824
|
+
* `finally`, and tears the child down via {@link teardown}.
|
|
825
|
+
*
|
|
826
|
+
* Every line here carries `tags: { deviceId }` and a `reason`, because
|
|
827
|
+
* "detaching" a camera is the loudest dropped work in this path and an
|
|
828
|
+
* operator has to be able to answer "why did 618 stop" from the logs alone.
|
|
786
829
|
*/
|
|
787
830
|
handlePullTimeout(timeoutMs) {
|
|
788
831
|
this.pullTimeoutTimer = null;
|
|
789
832
|
const waiter = this.pendingPull;
|
|
790
833
|
if (!waiter) return;
|
|
834
|
+
const waitedMs = this.firstPullAt === 0 ? timeoutMs : Date.now() - this.firstPullAt;
|
|
835
|
+
if (!this.firstFrameReceived && waitedMs < this.firstFrameMaxWaitMs) {
|
|
836
|
+
this.logger.warn("session-decode-coordinator: still waiting for its first decodable frame — holding the dial", {
|
|
837
|
+
tags: { deviceId: this.deviceId },
|
|
838
|
+
meta: {
|
|
839
|
+
waitedMs,
|
|
840
|
+
reportEveryMs: timeoutMs,
|
|
841
|
+
maxWaitMs: this.firstFrameMaxWaitMs,
|
|
842
|
+
hint: "long IDR interval or a discarded GOP looks exactly like this"
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
this.armPullTimeout();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
791
848
|
this.pendingPull = null;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
849
|
+
const reason = this.firstFrameReceived ? "stalled-after-first-frame" : "no-first-frame";
|
|
850
|
+
this.logger.warn(this.firstFrameReceived ? "session-decode-coordinator: gave up waiting for the next frame — ending session" : "session-decode-coordinator: gave up waiting for the first decodable frame — ending session", {
|
|
851
|
+
tags: { deviceId: this.deviceId },
|
|
852
|
+
meta: {
|
|
853
|
+
reason,
|
|
854
|
+
waitedMs,
|
|
855
|
+
timeoutMs,
|
|
856
|
+
...this.firstFrameReceived ? {} : { maxWaitMs: this.firstFrameMaxWaitMs }
|
|
857
|
+
}
|
|
858
|
+
});
|
|
796
859
|
waiter.resolve({ kind: "ended" });
|
|
797
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Arm the next round-trip deadline. Before the first frame the timer is
|
|
863
|
+
* clamped to what is LEFT of the ceiling, so the give-up lands exactly on
|
|
864
|
+
* {@link firstFrameMaxWaitMs} instead of on the next report boundary past it
|
|
865
|
+
* — otherwise a 45 s ceiling reported every 20 s would really give up at 60 s.
|
|
866
|
+
*/
|
|
798
867
|
armPullTimeout() {
|
|
799
|
-
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : this.firstFrameTimeoutMs;
|
|
868
|
+
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : Math.max(1, Math.min(this.firstFrameTimeoutMs, this.firstFrameMaxWaitMs - (this.firstPullAt === 0 ? 0 : Date.now() - this.firstPullAt)));
|
|
800
869
|
this.pullTimeoutTimer = setTimeout(() => this.handlePullTimeout(timeoutMs), timeoutMs);
|
|
801
870
|
this.pullTimeoutTimer.unref?.();
|
|
802
871
|
}
|
|
@@ -918,8 +987,9 @@ function buildFrameImage(session, reply) {
|
|
|
918
987
|
function createSessionDecodeCoordinator(deps) {
|
|
919
988
|
const firstFrameTimeoutMs = deps.firstFrameTimeoutMs ?? DEFAULT_FIRST_FRAME_TIMEOUT_MS;
|
|
920
989
|
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
990
|
+
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
921
991
|
return { async *generateVideoFrames(source, opts) {
|
|
922
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, firstFrameTimeoutMs, frameTimeoutMs);
|
|
992
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs);
|
|
923
993
|
session.start(source, opts);
|
|
924
994
|
try {
|
|
925
995
|
while (true) {
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-CzFJeiG1.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
4
|
const require_addon_utils = require("../addon-utils-DveoBR6G.js");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
@@ -3489,12 +3489,56 @@ function activeBandAt(config, date) {
|
|
|
3489
3489
|
//#endregion
|
|
3490
3490
|
//#region src/recorder/addon/band-decision.ts
|
|
3491
3491
|
/**
|
|
3492
|
+
* Pure glue between a device's `RecordingConfig` and the v2 band engine: decide
|
|
3493
|
+
* whether recording should currently be running, and which finalized segments
|
|
3494
|
+
* an `events` band retains.
|
|
3495
|
+
*
|
|
3496
|
+
* The decision is enabled AND the band active at `now` (per the v2
|
|
3497
|
+
* `activeBandAt`/`bandActiveAt`) demands recording: a `continuous` band always
|
|
3498
|
+
* demands; an `events` band demands only within `postBufferSec` of a qualifying
|
|
3499
|
+
* trigger (B3). No bands / disabled => not recording.
|
|
3500
|
+
*
|
|
3501
|
+
* `preBufferSec` is retroactive — it is NOT a live-demand input; it retains
|
|
3502
|
+
* already-recorded segments BEFORE a trigger at keep/discard time
|
|
3503
|
+
* ({@link segmentRetainedForEvents}). This mirrors the old recorder's proven
|
|
3504
|
+
* policy split (start-on-trigger live gate + retroactive pre-buffer keep).
|
|
3505
|
+
*
|
|
3506
|
+
* Kept pure (clock + trigger state injected) so the attach/detach decision is
|
|
3507
|
+
* unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
|
|
3508
|
+
*/
|
|
3509
|
+
/**
|
|
3510
|
+
* THE one place a band's pre/post buffers are read. An ABSENT field takes
|
|
3511
|
+
* {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
|
|
3512
|
+
* is honoured verbatim.
|
|
3513
|
+
*
|
|
3514
|
+
* Both callers below used to inline `?? 0`, which read "the operator omitted the
|
|
3515
|
+
* field" as "the operator asked for a zero-length window". For the live gate
|
|
3516
|
+
* that is not a policy, it is an impossibility: nothing can ever be recorded.
|
|
3517
|
+
* Two cameras sat in exactly that state.
|
|
3518
|
+
*/
|
|
3519
|
+
function resolveBandBufferMs(band) {
|
|
3520
|
+
return {
|
|
3521
|
+
preMs: (band.preBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec) * 1e3,
|
|
3522
|
+
postMs: (band.postBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec) * 1e3
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Can this band EVER open a live recording window? False for an `events` band
|
|
3527
|
+
* whose resolved post-buffer is zero (only reachable by authoring an explicit
|
|
3528
|
+
* `0`) or which lists no trigger at all: both are "enabled, and records nothing,
|
|
3529
|
+
* forever". The recorder warns on it rather than dropping the device silently.
|
|
3530
|
+
*/
|
|
3531
|
+
function eventsBandCanEverDemand(band) {
|
|
3532
|
+
if (band.mode !== "events") return true;
|
|
3533
|
+
return (band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0) && resolveBandBufferMs(band).postMs > 0;
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3492
3536
|
* Is an `events` band's live demand window open at `atMs`? True iff a trigger
|
|
3493
3537
|
* the band listens for fired within `postBufferSec` of now. `preBufferSec` is
|
|
3494
3538
|
* deliberately NOT consulted here (see file header).
|
|
3495
3539
|
*/
|
|
3496
3540
|
function eventsBandDemanded(band, triggers, atMs) {
|
|
3497
|
-
const postMs = (band
|
|
3541
|
+
const { postMs } = resolveBandBufferMs(band);
|
|
3498
3542
|
const fresh = (lastMs) => lastMs != null && atMs - lastMs <= postMs;
|
|
3499
3543
|
if (band.triggers?.motion && fresh(triggers.lastMotionMs)) return true;
|
|
3500
3544
|
if (band.triggers?.audioThresholdDbfs != null && fresh(triggers.lastAudioMs)) return true;
|
|
@@ -3521,8 +3565,7 @@ function shouldRecordAt(config, triggers, at) {
|
|
|
3521
3565
|
* This is where `preBufferSec` retroactively retains pre-trigger footage.
|
|
3522
3566
|
*/
|
|
3523
3567
|
function segmentRetainedForEvents(segStartMs, segEndMs, band, triggers) {
|
|
3524
|
-
const preMs = (band
|
|
3525
|
-
const postMs = (band.postBufferSec ?? 0) * 1e3;
|
|
3568
|
+
const { preMs, postMs } = resolveBandBufferMs(band);
|
|
3526
3569
|
const overlaps = (lastMs) => lastMs != null && segStartMs <= lastMs + postMs && segEndMs >= lastMs - preMs;
|
|
3527
3570
|
if (band.triggers?.motion && overlaps(triggers.lastMotionMs)) return true;
|
|
3528
3571
|
if (band.triggers?.audioThresholdDbfs != null && overlaps(triggers.lastAudioMs)) return true;
|
|
@@ -4055,6 +4098,12 @@ var RecordingController = class {
|
|
|
4055
4098
|
* alive, which is the only question here.
|
|
4056
4099
|
*/
|
|
4057
4100
|
lastSegmentAt = /* @__PURE__ */ new Map();
|
|
4101
|
+
/**
|
|
4102
|
+
* Devices already named by {@link reportUnrecordable} — one warn per device
|
|
4103
|
+
* per "enabled but can never record" episode, re-armed as soon as the device
|
|
4104
|
+
* records again.
|
|
4105
|
+
*/
|
|
4106
|
+
unrecordableReported = /* @__PURE__ */ new Set();
|
|
4058
4107
|
restore = null;
|
|
4059
4108
|
tickTimer = null;
|
|
4060
4109
|
stopped = false;
|
|
@@ -4231,10 +4280,12 @@ var RecordingController = class {
|
|
|
4231
4280
|
if (this.stopped) return;
|
|
4232
4281
|
const config = await this.deps.loadConfig(deviceId);
|
|
4233
4282
|
if (!shouldRecordAt(config, this.triggersFor(deviceId), new Date(this.now()))) {
|
|
4283
|
+
this.reportUnrecordable(deviceId, config);
|
|
4234
4284
|
await this.detachDevice(deviceId);
|
|
4235
4285
|
this.restore?.remove(deviceId);
|
|
4236
4286
|
return;
|
|
4237
4287
|
}
|
|
4288
|
+
this.unrecordableReported.delete(deviceId);
|
|
4238
4289
|
if (this.active.has(deviceId)) return;
|
|
4239
4290
|
if (this.attaching.has(deviceId)) return;
|
|
4240
4291
|
this.attaching.add(deviceId);
|
|
@@ -4245,6 +4296,42 @@ var RecordingController = class {
|
|
|
4245
4296
|
}
|
|
4246
4297
|
}
|
|
4247
4298
|
/**
|
|
4299
|
+
* Name the `!wantRecording` drop when the device is ENABLED but its config can
|
|
4300
|
+
* never produce footage — the branch that hid this bug. Two shapes qualify,
|
|
4301
|
+
* and both mean "the operator asked for recording and will get none, forever":
|
|
4302
|
+
* an enabled config with no bands at all, and an `events` band that cannot
|
|
4303
|
+
* open a live window (no trigger listed, or an explicit `postBufferSec: 0`).
|
|
4304
|
+
*
|
|
4305
|
+
* NOT reported: a band that simply does not cover `now` (a night-only band at
|
|
4306
|
+
* noon), or an events band waiting for its next trigger. Those are the
|
|
4307
|
+
* feature working.
|
|
4308
|
+
*
|
|
4309
|
+
* One line per device until it records again ({@link unrecordableReported} is
|
|
4310
|
+
* cleared the moment `wantRecording` turns true), so the 30 s tick cannot turn
|
|
4311
|
+
* this into a log flood.
|
|
4312
|
+
*/
|
|
4313
|
+
reportUnrecordable(deviceId, config) {
|
|
4314
|
+
if (!config.enabled) return;
|
|
4315
|
+
if (this.unrecordableReported.has(deviceId)) return;
|
|
4316
|
+
const bands = config.bands ?? [];
|
|
4317
|
+
if (bands.length === 0) {
|
|
4318
|
+
this.unrecordableReported.add(deviceId);
|
|
4319
|
+
this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
const band = activeBandAt({ bands }, new Date(this.now()));
|
|
4323
|
+
if (band === null || eventsBandCanEverDemand(band)) return;
|
|
4324
|
+
this.unrecordableReported.add(deviceId);
|
|
4325
|
+
this.deps.logger.warn("recorder: events band can never open a recording window — nothing will ever be recorded", {
|
|
4326
|
+
tags: { deviceId },
|
|
4327
|
+
meta: {
|
|
4328
|
+
postBufferSec: band.postBufferSec,
|
|
4329
|
+
triggers: band.triggers,
|
|
4330
|
+
reason: band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0 ? "postBufferSec is 0" : "no trigger configured"
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4248
4335
|
* The attach body — extracted so the {@link attaching} in-flight guard in
|
|
4249
4336
|
* `evaluateDevice` wraps it cleanly. Resolves the profiles, spawns a
|
|
4250
4337
|
* SegmentWriter + watcher per profile, and records the set in `active`. A throw
|