@camstack/addon-pipeline 1.2.65 → 1.2.68
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-DKrqeIfS.js → dist-Bv9CqUAF.js} +159 -14
- package/dist/{dist-B9_C-QmC.mjs → dist-CwUPxdAB.mjs} +154 -15
- package/dist/{event-loop-stall-monitor-CnJ5JGA2.js → event-loop-stall-monitor-Bz3lx_aY.js} +1 -1
- package/dist/{event-loop-stall-monitor-BucTbe9v.mjs → event-loop-stall-monitor-D2dL-_bE.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 +144 -14
- package/dist/pipeline-runner/index.mjs +144 -14
- package/dist/recorder/index.js +117 -6
- package/dist/recorder/index.mjs +117 -6
- package/dist/remote-restream-BYbAsgUf.js +89 -0
- package/dist/remote-restream-Ci7RXNGb.mjs +66 -0
- 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 +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BCyIjQkw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cl4eU4eN.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ircnzt0s.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CJAUKSrT.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DU7u9EJt.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DqLdTCfo.mjs → hostInit-_NJ0GBOO.mjs} +2 -2
- package/dist/stream-broker/index.js +54 -15
- package/dist/stream-broker/index.mjs +54 -15
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CQHY2NeB.mjs → worker-protocol-0uQE5Lfd.mjs} +1 -1
- package/dist/{worker-protocol-CDkbdSuM.js → worker-protocol-D8M43suz.js} +1 -1
- package/package.json +2 -2
- package/dist/remote-restream-BeHi78PZ.mjs +0 -25
- package/dist/remote-restream-CO36Sr30.js +0 -36
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C2tu4c14.mjs +0 -26
|
@@ -3,12 +3,12 @@ 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-Bv9CqUAF.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Bz3lx_aY.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-D8M43suz.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
11
|
-
const require_remote_restream = require("../remote-restream-
|
|
11
|
+
const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
|
|
12
12
|
let sharp = require("sharp");
|
|
13
13
|
sharp = require_chunk.__toESM(sharp);
|
|
14
14
|
let node_child_process = require("node:child_process");
|
|
@@ -1086,6 +1086,7 @@ function startSessionDecodePump(deps) {
|
|
|
1086
1086
|
releaseSpawnSlot = null;
|
|
1087
1087
|
release();
|
|
1088
1088
|
};
|
|
1089
|
+
const pumpStartedAt = Date.now();
|
|
1089
1090
|
const pump = async () => {
|
|
1090
1091
|
try {
|
|
1091
1092
|
if (deps.acquireSpawnSlot) {
|
|
@@ -1095,11 +1096,16 @@ function startSessionDecodePump(deps) {
|
|
|
1095
1096
|
return;
|
|
1096
1097
|
}
|
|
1097
1098
|
}
|
|
1099
|
+
const spawnGrantedAt = Date.now();
|
|
1098
1100
|
let firstFrameSeen = false;
|
|
1099
1101
|
for await (const frame of iterator) {
|
|
1100
1102
|
if (!firstFrameSeen) {
|
|
1101
1103
|
firstFrameSeen = true;
|
|
1102
1104
|
releaseSpawnSlotOnce();
|
|
1105
|
+
deps.onFirstFrame?.({
|
|
1106
|
+
spawnWaitMs: spawnGrantedAt - pumpStartedAt,
|
|
1107
|
+
forkToFrameMs: Date.now() - spawnGrantedAt
|
|
1108
|
+
});
|
|
1103
1109
|
}
|
|
1104
1110
|
if (stopped) {
|
|
1105
1111
|
frame.image.close();
|
|
@@ -4033,7 +4039,7 @@ function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
|
4033
4039
|
* thin delegator to this function — its two call sites are unchanged.
|
|
4034
4040
|
*/
|
|
4035
4041
|
async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
4036
|
-
const { deviceId, streamId, tag, hostname, ownerNodeId } = params;
|
|
4042
|
+
const { deviceId, streamId, tag, hostname, ownerNodeId, detectionIntent } = params;
|
|
4037
4043
|
const pin = ownerNodeId !== void 0 ? require_dist.nodePin(ownerNodeId) : void 0;
|
|
4038
4044
|
const profile = require_remote_restream.profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, pin), deviceId, streamId);
|
|
4039
4045
|
if (profile === null) {
|
|
@@ -4053,7 +4059,7 @@ async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
|
4053
4059
|
}, pin);
|
|
4054
4060
|
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
4055
4061
|
const source = {
|
|
4056
|
-
restreamUrl: rtpSource.url,
|
|
4062
|
+
restreamUrl: detectionIntent ? require_remote_restream.withDetectionIntent(rtpSource.url) : rtpSource.url,
|
|
4057
4063
|
deviceId,
|
|
4058
4064
|
codec
|
|
4059
4065
|
};
|
|
@@ -4120,6 +4126,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4120
4126
|
*/
|
|
4121
4127
|
retainedFrames = null;
|
|
4122
4128
|
attached = /* @__PURE__ */ new Map();
|
|
4129
|
+
/**
|
|
4130
|
+
* Detection decode workers parked across a detach, keyed by deviceId — see
|
|
4131
|
+
* {@link WarmDetachedDecode}. Bounded by construction: at most one entry per
|
|
4132
|
+
* camera, each self-expiring after {@link DETECTION_KEEPWARM_MS}.
|
|
4133
|
+
*/
|
|
4134
|
+
warmDetachedDetection = /* @__PURE__ */ new Map();
|
|
4123
4135
|
nodeId = "unknown";
|
|
4124
4136
|
metricsSnapshotTimer = null;
|
|
4125
4137
|
unsubMotionEvents = null;
|
|
@@ -4347,6 +4359,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4347
4359
|
attachment.motionUnsubscribe?.();
|
|
4348
4360
|
attachment.detectionUnsubscribe?.();
|
|
4349
4361
|
}
|
|
4362
|
+
for (const deviceId of [...this.warmDetachedDetection.keys()]) this.dropWarmDetectionDecode(deviceId, "shutdown");
|
|
4350
4363
|
this.attached.clear();
|
|
4351
4364
|
this.lastMotionFrameAt.clear();
|
|
4352
4365
|
this.nativeCropRegistry.clear();
|
|
@@ -5050,7 +5063,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5050
5063
|
return { success: true };
|
|
5051
5064
|
}
|
|
5052
5065
|
async detachCamera(input) {
|
|
5053
|
-
this.detachInternal(input.deviceId);
|
|
5066
|
+
this.detachInternal(input.deviceId, true);
|
|
5054
5067
|
return { success: true };
|
|
5055
5068
|
}
|
|
5056
5069
|
async reportMotion(input) {
|
|
@@ -5445,7 +5458,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5445
5458
|
*/
|
|
5446
5459
|
handleDetectionDecodeEnded(deviceId) {
|
|
5447
5460
|
const attachment = this.attached.get(deviceId);
|
|
5448
|
-
if (!attachment)
|
|
5461
|
+
if (!attachment) {
|
|
5462
|
+
this.dropWarmDetectionDecode(deviceId, "worker-died-while-parked");
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5449
5465
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
5450
5466
|
attachment.detectionUnsubscribe?.();
|
|
5451
5467
|
attachment.detectionUnsubscribe = null;
|
|
@@ -5476,7 +5492,18 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5476
5492
|
});
|
|
5477
5493
|
}, DETECTION_REDIAL_DELAY_MS).unref?.();
|
|
5478
5494
|
}
|
|
5479
|
-
|
|
5495
|
+
/**
|
|
5496
|
+
* Tear a camera down.
|
|
5497
|
+
*
|
|
5498
|
+
* `keepDecodeWarm` routes the DETECTION decode through the same keep-warm
|
|
5499
|
+
* grace the phase gate uses instead of killing it — see
|
|
5500
|
+
* {@link WarmDetachedDecode}. Set only by the {@link detachCamera} cap entry
|
|
5501
|
+
* point (the orchestrator's motion-cooldown teardown); every internal detach
|
|
5502
|
+
* — an attach replacing a stale config, a decode that died twice, shutdown —
|
|
5503
|
+
* is a hard one, because in each of those cases the worker is either wrong or
|
|
5504
|
+
* already gone.
|
|
5505
|
+
*/
|
|
5506
|
+
detachInternal(deviceId, keepDecodeWarm = false) {
|
|
5480
5507
|
const attachment = this.attached.get(deviceId);
|
|
5481
5508
|
if (!attachment) return;
|
|
5482
5509
|
this.clearOnboardAnalyzer(deviceId);
|
|
@@ -5485,7 +5512,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5485
5512
|
attachment.detectionKeepWarmTimer = null;
|
|
5486
5513
|
}
|
|
5487
5514
|
attachment.motionUnsubscribe?.();
|
|
5488
|
-
attachment.detectionUnsubscribe
|
|
5515
|
+
if (keepDecodeWarm && attachment.detectionUnsubscribe !== null) this.parkWarmDetectionDecode(deviceId, attachment.config, attachment.detectionUnsubscribe);
|
|
5516
|
+
else attachment.detectionUnsubscribe?.();
|
|
5489
5517
|
this.lastMotionFrameAt.delete(deviceId);
|
|
5490
5518
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
5491
5519
|
this.packageGovernor.forget(deviceId);
|
|
@@ -5495,7 +5523,86 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5495
5523
|
this.lastMotionAt.delete(deviceId);
|
|
5496
5524
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
5497
5525
|
this.runner?.unregisterCamera(deviceId);
|
|
5498
|
-
this.ctx?.logger.info("detachCamera", {
|
|
5526
|
+
this.ctx?.logger.info("detachCamera", {
|
|
5527
|
+
tags: { deviceId },
|
|
5528
|
+
meta: { decodeKeptWarm: this.warmDetachedDetection.has(deviceId) }
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
/**
|
|
5532
|
+
* The decode-relevant identity of a camera's detection decode. Two configs
|
|
5533
|
+
* that agree on all of it can share one worker; anything else must re-fork.
|
|
5534
|
+
* Kept in ONE place so a future decode-shaping option cannot be added to the
|
|
5535
|
+
* pump without the adoption check noticing.
|
|
5536
|
+
*/
|
|
5537
|
+
detectionDecodeIdentity(config) {
|
|
5538
|
+
return {
|
|
5539
|
+
streamId: config.detectionStreamId,
|
|
5540
|
+
fps: config.detectionFps,
|
|
5541
|
+
hostname: resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]),
|
|
5542
|
+
ownerNodeId: resolveSessionDecodeOwnerNodeId(config)
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
/**
|
|
5546
|
+
* Hold a detached camera's detection decode worker for
|
|
5547
|
+
* {@link DETECTION_KEEPWARM_MS} so the next motion event reuses it.
|
|
5548
|
+
* Replacing an existing park releases the older worker first — two parked
|
|
5549
|
+
* workers for one camera would be two live decodes and two broker leases.
|
|
5550
|
+
*/
|
|
5551
|
+
parkWarmDetectionDecode(deviceId, config, unsubscribe) {
|
|
5552
|
+
this.dropWarmDetectionDecode(deviceId, "replaced");
|
|
5553
|
+
const timer = setTimeout(() => {
|
|
5554
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5555
|
+
unsubscribe();
|
|
5556
|
+
this.ctx?.logger.info("session-decode: warm detection decode expired — worker released", {
|
|
5557
|
+
tags: { deviceId },
|
|
5558
|
+
meta: { keepWarmMs: DETECTION_KEEPWARM_MS }
|
|
5559
|
+
});
|
|
5560
|
+
}, DETECTION_KEEPWARM_MS);
|
|
5561
|
+
timer.unref?.();
|
|
5562
|
+
this.warmDetachedDetection.set(deviceId, {
|
|
5563
|
+
unsubscribe,
|
|
5564
|
+
timer,
|
|
5565
|
+
...this.detectionDecodeIdentity(config)
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
/**
|
|
5569
|
+
* Adopt a parked decode worker for a camera that is attaching again, or
|
|
5570
|
+
* `null` when there is nothing to adopt.
|
|
5571
|
+
*
|
|
5572
|
+
* A parked worker whose identity no longer matches is DROPPED here rather
|
|
5573
|
+
* than left to expire: the caller is about to fork a replacement, and letting
|
|
5574
|
+
* both run would double this camera's decode cost for the rest of the window.
|
|
5575
|
+
*/
|
|
5576
|
+
adoptWarmDetectionDecode(deviceId, config) {
|
|
5577
|
+
const warm = this.warmDetachedDetection.get(deviceId);
|
|
5578
|
+
if (!warm) return null;
|
|
5579
|
+
const wanted = this.detectionDecodeIdentity(config);
|
|
5580
|
+
if (warm.streamId !== wanted.streamId || warm.fps !== wanted.fps || warm.hostname !== wanted.hostname || warm.ownerNodeId !== wanted.ownerNodeId) {
|
|
5581
|
+
this.dropWarmDetectionDecode(deviceId, "config-changed");
|
|
5582
|
+
return null;
|
|
5583
|
+
}
|
|
5584
|
+
clearTimeout(warm.timer);
|
|
5585
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5586
|
+
this.ctx?.logger.info("session-decode: reusing the warm detection decode worker (no re-fork)", {
|
|
5587
|
+
tags: { deviceId },
|
|
5588
|
+
meta: {
|
|
5589
|
+
streamId: warm.streamId,
|
|
5590
|
+
fps: warm.fps
|
|
5591
|
+
}
|
|
5592
|
+
});
|
|
5593
|
+
return warm.unsubscribe;
|
|
5594
|
+
}
|
|
5595
|
+
/** Release a parked worker now. `reason` names why in the log. */
|
|
5596
|
+
dropWarmDetectionDecode(deviceId, reason) {
|
|
5597
|
+
const warm = this.warmDetachedDetection.get(deviceId);
|
|
5598
|
+
if (!warm) return;
|
|
5599
|
+
clearTimeout(warm.timer);
|
|
5600
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5601
|
+
warm.unsubscribe();
|
|
5602
|
+
this.ctx?.logger.info("session-decode: warm detection decode released", {
|
|
5603
|
+
tags: { deviceId },
|
|
5604
|
+
meta: { reason }
|
|
5605
|
+
});
|
|
5499
5606
|
}
|
|
5500
5607
|
/**
|
|
5501
5608
|
* Synchronously cancel the teardown timer and call the unsubscribe
|
|
@@ -5628,14 +5735,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5628
5735
|
* (see its doc for the acquire behavior) — supplies this addon's `ctx.logger`.
|
|
5629
5736
|
* Kept as a method so the two call sites need no change.
|
|
5630
5737
|
*/
|
|
5631
|
-
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5738
|
+
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId, detectionIntent) {
|
|
5632
5739
|
await this.refreshNativeLeaseSettings();
|
|
5633
5740
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5634
5741
|
deviceId,
|
|
5635
5742
|
streamId,
|
|
5636
5743
|
tag,
|
|
5637
5744
|
hostname,
|
|
5638
|
-
ownerNodeId
|
|
5745
|
+
ownerNodeId,
|
|
5746
|
+
...detectionIntent === true ? { detectionIntent: true } : {}
|
|
5639
5747
|
});
|
|
5640
5748
|
}
|
|
5641
5749
|
/**
|
|
@@ -5751,10 +5859,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5751
5859
|
const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
|
|
5752
5860
|
if (!runner) return () => {};
|
|
5753
5861
|
const retainedFrames = this.retainedFrames;
|
|
5862
|
+
const requestedAt = Date.now();
|
|
5754
5863
|
const teardown = startGuardedSessionDecode({
|
|
5755
5864
|
logger: log,
|
|
5756
|
-
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5865
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config), true),
|
|
5757
5866
|
startPump: (source) => {
|
|
5867
|
+
const dialMs = Date.now() - requestedAt;
|
|
5758
5868
|
log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
|
|
5759
5869
|
streamId: config.detectionStreamId,
|
|
5760
5870
|
codec: source.codec
|
|
@@ -5773,6 +5883,21 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5773
5883
|
return reservation.detection || reservation.motion;
|
|
5774
5884
|
},
|
|
5775
5885
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
5886
|
+
onFirstFrame: (timing) => {
|
|
5887
|
+
const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
|
|
5888
|
+
log.info("session-decode: detection first frame", {
|
|
5889
|
+
tags: { deviceId: config.deviceId },
|
|
5890
|
+
meta: {
|
|
5891
|
+
streamId: config.detectionStreamId,
|
|
5892
|
+
codec: source.codec,
|
|
5893
|
+
dialMs,
|
|
5894
|
+
spawnWaitMs: timing.spawnWaitMs,
|
|
5895
|
+
forkToFrameMs: timing.forkToFrameMs,
|
|
5896
|
+
firstFrameMs: Date.now() - requestedAt,
|
|
5897
|
+
...phaseActiveAt !== void 0 ? { sincePhaseActiveMs: Date.now() - phaseActiveAt } : {}
|
|
5898
|
+
}
|
|
5899
|
+
});
|
|
5900
|
+
},
|
|
5776
5901
|
onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
|
|
5777
5902
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5778
5903
|
const current = reservation;
|
|
@@ -5864,6 +5989,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5864
5989
|
attachment.detectionKeepWarmTimer = null;
|
|
5865
5990
|
}
|
|
5866
5991
|
if (attachment.detectionUnsubscribe !== null) return;
|
|
5992
|
+
const adopted = this.adoptWarmDetectionDecode(deviceId, attachment.config);
|
|
5993
|
+
if (adopted) {
|
|
5994
|
+
attachment.detectionUnsubscribe = adopted;
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5867
5997
|
this.subscribeDetectionFrames(attachment.config).then((unsub) => {
|
|
5868
5998
|
if (!this.attached.has(deviceId)) {
|
|
5869
5999
|
unsub?.();
|
|
@@ -1,9 +1,9 @@
|
|
|
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 { At as string, Ct as boolean, Dt as number, Et as literal, K as pipelineRunnerCapability, M as deriveDetailCropRect, Mt as EventCategory, Ot as object, St as array, Tt as lazy, U as pickDetailCropConvention, W as pickNativeLeaseOverride, dt as createEvent, gt as nodePin, j as defineCustomActions, k as customAction, s as DEFAULT_DETAIL_CROP_CONVENTION, st as BaseAddon, tt as errMsg, u as DEVICE_BACKEND_TO_FORMAT, xt as _enum } from "../dist-CwUPxdAB.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-D2dL-_bE.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-0uQE5Lfd.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
|
-
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-
|
|
6
|
+
import { i as withDetectionIntent, n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-Ci7RXNGb.mjs";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { fork } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -1079,6 +1079,7 @@ function startSessionDecodePump(deps) {
|
|
|
1079
1079
|
releaseSpawnSlot = null;
|
|
1080
1080
|
release();
|
|
1081
1081
|
};
|
|
1082
|
+
const pumpStartedAt = Date.now();
|
|
1082
1083
|
const pump = async () => {
|
|
1083
1084
|
try {
|
|
1084
1085
|
if (deps.acquireSpawnSlot) {
|
|
@@ -1088,11 +1089,16 @@ function startSessionDecodePump(deps) {
|
|
|
1088
1089
|
return;
|
|
1089
1090
|
}
|
|
1090
1091
|
}
|
|
1092
|
+
const spawnGrantedAt = Date.now();
|
|
1091
1093
|
let firstFrameSeen = false;
|
|
1092
1094
|
for await (const frame of iterator) {
|
|
1093
1095
|
if (!firstFrameSeen) {
|
|
1094
1096
|
firstFrameSeen = true;
|
|
1095
1097
|
releaseSpawnSlotOnce();
|
|
1098
|
+
deps.onFirstFrame?.({
|
|
1099
|
+
spawnWaitMs: spawnGrantedAt - pumpStartedAt,
|
|
1100
|
+
forkToFrameMs: Date.now() - spawnGrantedAt
|
|
1101
|
+
});
|
|
1096
1102
|
}
|
|
1097
1103
|
if (stopped) {
|
|
1098
1104
|
frame.image.close();
|
|
@@ -4025,7 +4031,7 @@ function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
|
4025
4031
|
* thin delegator to this function — its two call sites are unchanged.
|
|
4026
4032
|
*/
|
|
4027
4033
|
async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
4028
|
-
const { deviceId, streamId, tag, hostname, ownerNodeId } = params;
|
|
4034
|
+
const { deviceId, streamId, tag, hostname, ownerNodeId, detectionIntent } = params;
|
|
4029
4035
|
const pin = ownerNodeId !== void 0 ? nodePin(ownerNodeId) : void 0;
|
|
4030
4036
|
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, pin), deviceId, streamId);
|
|
4031
4037
|
if (profile === null) {
|
|
@@ -4045,7 +4051,7 @@ async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
|
4045
4051
|
}, pin);
|
|
4046
4052
|
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
4047
4053
|
const source = {
|
|
4048
|
-
restreamUrl: rtpSource.url,
|
|
4054
|
+
restreamUrl: detectionIntent ? withDetectionIntent(rtpSource.url) : rtpSource.url,
|
|
4049
4055
|
deviceId,
|
|
4050
4056
|
codec
|
|
4051
4057
|
};
|
|
@@ -4112,6 +4118,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4112
4118
|
*/
|
|
4113
4119
|
retainedFrames = null;
|
|
4114
4120
|
attached = /* @__PURE__ */ new Map();
|
|
4121
|
+
/**
|
|
4122
|
+
* Detection decode workers parked across a detach, keyed by deviceId — see
|
|
4123
|
+
* {@link WarmDetachedDecode}. Bounded by construction: at most one entry per
|
|
4124
|
+
* camera, each self-expiring after {@link DETECTION_KEEPWARM_MS}.
|
|
4125
|
+
*/
|
|
4126
|
+
warmDetachedDetection = /* @__PURE__ */ new Map();
|
|
4115
4127
|
nodeId = "unknown";
|
|
4116
4128
|
metricsSnapshotTimer = null;
|
|
4117
4129
|
unsubMotionEvents = null;
|
|
@@ -4339,6 +4351,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4339
4351
|
attachment.motionUnsubscribe?.();
|
|
4340
4352
|
attachment.detectionUnsubscribe?.();
|
|
4341
4353
|
}
|
|
4354
|
+
for (const deviceId of [...this.warmDetachedDetection.keys()]) this.dropWarmDetectionDecode(deviceId, "shutdown");
|
|
4342
4355
|
this.attached.clear();
|
|
4343
4356
|
this.lastMotionFrameAt.clear();
|
|
4344
4357
|
this.nativeCropRegistry.clear();
|
|
@@ -5042,7 +5055,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5042
5055
|
return { success: true };
|
|
5043
5056
|
}
|
|
5044
5057
|
async detachCamera(input) {
|
|
5045
|
-
this.detachInternal(input.deviceId);
|
|
5058
|
+
this.detachInternal(input.deviceId, true);
|
|
5046
5059
|
return { success: true };
|
|
5047
5060
|
}
|
|
5048
5061
|
async reportMotion(input) {
|
|
@@ -5437,7 +5450,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5437
5450
|
*/
|
|
5438
5451
|
handleDetectionDecodeEnded(deviceId) {
|
|
5439
5452
|
const attachment = this.attached.get(deviceId);
|
|
5440
|
-
if (!attachment)
|
|
5453
|
+
if (!attachment) {
|
|
5454
|
+
this.dropWarmDetectionDecode(deviceId, "worker-died-while-parked");
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5441
5457
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
5442
5458
|
attachment.detectionUnsubscribe?.();
|
|
5443
5459
|
attachment.detectionUnsubscribe = null;
|
|
@@ -5468,7 +5484,18 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5468
5484
|
});
|
|
5469
5485
|
}, DETECTION_REDIAL_DELAY_MS).unref?.();
|
|
5470
5486
|
}
|
|
5471
|
-
|
|
5487
|
+
/**
|
|
5488
|
+
* Tear a camera down.
|
|
5489
|
+
*
|
|
5490
|
+
* `keepDecodeWarm` routes the DETECTION decode through the same keep-warm
|
|
5491
|
+
* grace the phase gate uses instead of killing it — see
|
|
5492
|
+
* {@link WarmDetachedDecode}. Set only by the {@link detachCamera} cap entry
|
|
5493
|
+
* point (the orchestrator's motion-cooldown teardown); every internal detach
|
|
5494
|
+
* — an attach replacing a stale config, a decode that died twice, shutdown —
|
|
5495
|
+
* is a hard one, because in each of those cases the worker is either wrong or
|
|
5496
|
+
* already gone.
|
|
5497
|
+
*/
|
|
5498
|
+
detachInternal(deviceId, keepDecodeWarm = false) {
|
|
5472
5499
|
const attachment = this.attached.get(deviceId);
|
|
5473
5500
|
if (!attachment) return;
|
|
5474
5501
|
this.clearOnboardAnalyzer(deviceId);
|
|
@@ -5477,7 +5504,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5477
5504
|
attachment.detectionKeepWarmTimer = null;
|
|
5478
5505
|
}
|
|
5479
5506
|
attachment.motionUnsubscribe?.();
|
|
5480
|
-
attachment.detectionUnsubscribe
|
|
5507
|
+
if (keepDecodeWarm && attachment.detectionUnsubscribe !== null) this.parkWarmDetectionDecode(deviceId, attachment.config, attachment.detectionUnsubscribe);
|
|
5508
|
+
else attachment.detectionUnsubscribe?.();
|
|
5481
5509
|
this.lastMotionFrameAt.delete(deviceId);
|
|
5482
5510
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
5483
5511
|
this.packageGovernor.forget(deviceId);
|
|
@@ -5487,7 +5515,86 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5487
5515
|
this.lastMotionAt.delete(deviceId);
|
|
5488
5516
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
5489
5517
|
this.runner?.unregisterCamera(deviceId);
|
|
5490
|
-
this.ctx?.logger.info("detachCamera", {
|
|
5518
|
+
this.ctx?.logger.info("detachCamera", {
|
|
5519
|
+
tags: { deviceId },
|
|
5520
|
+
meta: { decodeKeptWarm: this.warmDetachedDetection.has(deviceId) }
|
|
5521
|
+
});
|
|
5522
|
+
}
|
|
5523
|
+
/**
|
|
5524
|
+
* The decode-relevant identity of a camera's detection decode. Two configs
|
|
5525
|
+
* that agree on all of it can share one worker; anything else must re-fork.
|
|
5526
|
+
* Kept in ONE place so a future decode-shaping option cannot be added to the
|
|
5527
|
+
* pump without the adoption check noticing.
|
|
5528
|
+
*/
|
|
5529
|
+
detectionDecodeIdentity(config) {
|
|
5530
|
+
return {
|
|
5531
|
+
streamId: config.detectionStreamId,
|
|
5532
|
+
fps: config.detectionFps,
|
|
5533
|
+
hostname: resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]),
|
|
5534
|
+
ownerNodeId: resolveSessionDecodeOwnerNodeId(config)
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
/**
|
|
5538
|
+
* Hold a detached camera's detection decode worker for
|
|
5539
|
+
* {@link DETECTION_KEEPWARM_MS} so the next motion event reuses it.
|
|
5540
|
+
* Replacing an existing park releases the older worker first — two parked
|
|
5541
|
+
* workers for one camera would be two live decodes and two broker leases.
|
|
5542
|
+
*/
|
|
5543
|
+
parkWarmDetectionDecode(deviceId, config, unsubscribe) {
|
|
5544
|
+
this.dropWarmDetectionDecode(deviceId, "replaced");
|
|
5545
|
+
const timer = setTimeout(() => {
|
|
5546
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5547
|
+
unsubscribe();
|
|
5548
|
+
this.ctx?.logger.info("session-decode: warm detection decode expired — worker released", {
|
|
5549
|
+
tags: { deviceId },
|
|
5550
|
+
meta: { keepWarmMs: DETECTION_KEEPWARM_MS }
|
|
5551
|
+
});
|
|
5552
|
+
}, DETECTION_KEEPWARM_MS);
|
|
5553
|
+
timer.unref?.();
|
|
5554
|
+
this.warmDetachedDetection.set(deviceId, {
|
|
5555
|
+
unsubscribe,
|
|
5556
|
+
timer,
|
|
5557
|
+
...this.detectionDecodeIdentity(config)
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
/**
|
|
5561
|
+
* Adopt a parked decode worker for a camera that is attaching again, or
|
|
5562
|
+
* `null` when there is nothing to adopt.
|
|
5563
|
+
*
|
|
5564
|
+
* A parked worker whose identity no longer matches is DROPPED here rather
|
|
5565
|
+
* than left to expire: the caller is about to fork a replacement, and letting
|
|
5566
|
+
* both run would double this camera's decode cost for the rest of the window.
|
|
5567
|
+
*/
|
|
5568
|
+
adoptWarmDetectionDecode(deviceId, config) {
|
|
5569
|
+
const warm = this.warmDetachedDetection.get(deviceId);
|
|
5570
|
+
if (!warm) return null;
|
|
5571
|
+
const wanted = this.detectionDecodeIdentity(config);
|
|
5572
|
+
if (warm.streamId !== wanted.streamId || warm.fps !== wanted.fps || warm.hostname !== wanted.hostname || warm.ownerNodeId !== wanted.ownerNodeId) {
|
|
5573
|
+
this.dropWarmDetectionDecode(deviceId, "config-changed");
|
|
5574
|
+
return null;
|
|
5575
|
+
}
|
|
5576
|
+
clearTimeout(warm.timer);
|
|
5577
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5578
|
+
this.ctx?.logger.info("session-decode: reusing the warm detection decode worker (no re-fork)", {
|
|
5579
|
+
tags: { deviceId },
|
|
5580
|
+
meta: {
|
|
5581
|
+
streamId: warm.streamId,
|
|
5582
|
+
fps: warm.fps
|
|
5583
|
+
}
|
|
5584
|
+
});
|
|
5585
|
+
return warm.unsubscribe;
|
|
5586
|
+
}
|
|
5587
|
+
/** Release a parked worker now. `reason` names why in the log. */
|
|
5588
|
+
dropWarmDetectionDecode(deviceId, reason) {
|
|
5589
|
+
const warm = this.warmDetachedDetection.get(deviceId);
|
|
5590
|
+
if (!warm) return;
|
|
5591
|
+
clearTimeout(warm.timer);
|
|
5592
|
+
this.warmDetachedDetection.delete(deviceId);
|
|
5593
|
+
warm.unsubscribe();
|
|
5594
|
+
this.ctx?.logger.info("session-decode: warm detection decode released", {
|
|
5595
|
+
tags: { deviceId },
|
|
5596
|
+
meta: { reason }
|
|
5597
|
+
});
|
|
5491
5598
|
}
|
|
5492
5599
|
/**
|
|
5493
5600
|
* Synchronously cancel the teardown timer and call the unsubscribe
|
|
@@ -5620,14 +5727,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5620
5727
|
* (see its doc for the acquire behavior) — supplies this addon's `ctx.logger`.
|
|
5621
5728
|
* Kept as a method so the two call sites need no change.
|
|
5622
5729
|
*/
|
|
5623
|
-
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5730
|
+
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId, detectionIntent) {
|
|
5624
5731
|
await this.refreshNativeLeaseSettings();
|
|
5625
5732
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5626
5733
|
deviceId,
|
|
5627
5734
|
streamId,
|
|
5628
5735
|
tag,
|
|
5629
5736
|
hostname,
|
|
5630
|
-
ownerNodeId
|
|
5737
|
+
ownerNodeId,
|
|
5738
|
+
...detectionIntent === true ? { detectionIntent: true } : {}
|
|
5631
5739
|
});
|
|
5632
5740
|
}
|
|
5633
5741
|
/**
|
|
@@ -5743,10 +5851,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5743
5851
|
const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
|
|
5744
5852
|
if (!runner) return () => {};
|
|
5745
5853
|
const retainedFrames = this.retainedFrames;
|
|
5854
|
+
const requestedAt = Date.now();
|
|
5746
5855
|
const teardown = startGuardedSessionDecode({
|
|
5747
5856
|
logger: log,
|
|
5748
|
-
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5857
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config), true),
|
|
5749
5858
|
startPump: (source) => {
|
|
5859
|
+
const dialMs = Date.now() - requestedAt;
|
|
5750
5860
|
log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
|
|
5751
5861
|
streamId: config.detectionStreamId,
|
|
5752
5862
|
codec: source.codec
|
|
@@ -5765,6 +5875,21 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5765
5875
|
return reservation.detection || reservation.motion;
|
|
5766
5876
|
},
|
|
5767
5877
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
5878
|
+
onFirstFrame: (timing) => {
|
|
5879
|
+
const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
|
|
5880
|
+
log.info("session-decode: detection first frame", {
|
|
5881
|
+
tags: { deviceId: config.deviceId },
|
|
5882
|
+
meta: {
|
|
5883
|
+
streamId: config.detectionStreamId,
|
|
5884
|
+
codec: source.codec,
|
|
5885
|
+
dialMs,
|
|
5886
|
+
spawnWaitMs: timing.spawnWaitMs,
|
|
5887
|
+
forkToFrameMs: timing.forkToFrameMs,
|
|
5888
|
+
firstFrameMs: Date.now() - requestedAt,
|
|
5889
|
+
...phaseActiveAt !== void 0 ? { sincePhaseActiveMs: Date.now() - phaseActiveAt } : {}
|
|
5890
|
+
}
|
|
5891
|
+
});
|
|
5892
|
+
},
|
|
5768
5893
|
onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
|
|
5769
5894
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5770
5895
|
const current = reservation;
|
|
@@ -5856,6 +5981,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5856
5981
|
attachment.detectionKeepWarmTimer = null;
|
|
5857
5982
|
}
|
|
5858
5983
|
if (attachment.detectionUnsubscribe !== null) return;
|
|
5984
|
+
const adopted = this.adoptWarmDetectionDecode(deviceId, attachment.config);
|
|
5985
|
+
if (adopted) {
|
|
5986
|
+
attachment.detectionUnsubscribe = adopted;
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5859
5989
|
this.subscribeDetectionFrames(attachment.config).then((unsub) => {
|
|
5860
5990
|
if (!this.attached.has(deviceId)) {
|
|
5861
5991
|
unsub?.();
|