@camstack/addon-pipeline 1.2.213 → 1.2.215
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/{default-detection-model-B-PccVsO.mjs → default-detection-model-C2vYSX3O.mjs} +1 -1
- package/dist/{default-detection-model-BZTjgdod.js → default-detection-model-f0PImZnh.js} +1 -1
- package/dist/detection-pipeline/index.js +6 -7
- package/dist/detection-pipeline/index.mjs +5 -6
- package/dist/{dist-CyfP2nFh.js → dist-DoHcimdu.js} +42 -1
- package/dist/{dist-CsI90TVi.mjs → dist-DttqKvsQ.mjs} +42 -1
- package/dist/{lazy-sharp-BawPy_a9.js → lazy-sharp-BeeQGCUu.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-CkBqyM-0.js → node-6uF72wri.js} +1 -1
- package/dist/{node-6Pb_t1Fm.mjs → node-NtM7__8q.mjs} +1 -1
- package/dist/pipeline-runner/index.js +216 -26
- package/dist/pipeline-runner/index.mjs +214 -24
- package/dist/{process-memory-B8yoOaEl.js → process-memory-BRdyl5fE.js} +1 -1
- package/dist/{process-memory-B_-QzMHn.mjs → process-memory-CddpCDWL.mjs} +1 -1
- package/dist/recorder/index.js +14 -9
- package/dist/recorder/index.mjs +14 -9
- package/dist/{segment-demux-js-oOpXzk2J.js → segment-demux-js-CkJPdyDH.js} +1 -1
- package/dist/{segment-demux-js-CBvM6zIM.mjs → segment-demux-js-CsFtPVzH.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +187 -8
- package/dist/session-decode/decode-worker-child.mjs +186 -7
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cf0O5S13.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZIja2AOe.mjs} +3 -3
- 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-3eT4CIOk.mjs → hostInit-DpatMS4g.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DNqdtZzZ.mjs → worker-protocol-BKwkLZWT.mjs} +5 -2
- package/dist/{worker-protocol-CYopQrkK.js → worker-protocol-BWO2WNjw.js} +5 -2
- package/package.json +1 -1
- package/dist/event-loop-stall-monitor-DXLNBMkY.mjs +0 -138
- package/dist/event-loop-stall-monitor-DaUBcb13.js +0 -149
|
@@ -2,14 +2,13 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
7
|
-
const require_default_detection_model = require("../default-detection-model-
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
5
|
+
const require_dist = require("../dist-DoHcimdu.js");
|
|
6
|
+
const require_node = require("../node-6uF72wri.js");
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-f0PImZnh.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-BWO2WNjw.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
10
|
+
const require_lazy_sharp = require("../lazy-sharp-BeeQGCUu.js");
|
|
11
11
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
12
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
13
12
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
14
13
|
let node_os = require("node:os");
|
|
15
14
|
node_os = require_dist.__toESM(node_os);
|
|
@@ -18,6 +17,7 @@ let sharp = require("sharp");
|
|
|
18
17
|
sharp = require_dist.__toESM(sharp);
|
|
19
18
|
let node_url = require("node:url");
|
|
20
19
|
let node_perf_hooks = require("node:perf_hooks");
|
|
20
|
+
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
21
21
|
//#region src/session-decode/fork-decode-worker.ts
|
|
22
22
|
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
23
23
|
var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
|
|
@@ -1010,6 +1010,7 @@ var SessionDecodeSession = class {
|
|
|
1010
1010
|
frameTimeoutMs;
|
|
1011
1011
|
firstFrameMaxWaitMs;
|
|
1012
1012
|
onDialTiming;
|
|
1013
|
+
onPreRollReport;
|
|
1013
1014
|
pendingPull = null;
|
|
1014
1015
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
1015
1016
|
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
@@ -1023,7 +1024,7 @@ var SessionDecodeSession = class {
|
|
|
1023
1024
|
firstFrameReceived = false;
|
|
1024
1025
|
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
1025
1026
|
firstPullAt = 0;
|
|
1026
|
-
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
|
|
1027
|
+
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming, onPreRollReport) {
|
|
1027
1028
|
this.child = child;
|
|
1028
1029
|
this.logger = logger;
|
|
1029
1030
|
this.deviceId = deviceId;
|
|
@@ -1031,6 +1032,7 @@ var SessionDecodeSession = class {
|
|
|
1031
1032
|
this.frameTimeoutMs = frameTimeoutMs;
|
|
1032
1033
|
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
1033
1034
|
this.onDialTiming = onDialTiming;
|
|
1035
|
+
this.onPreRollReport = onPreRollReport;
|
|
1034
1036
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
1035
1037
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
1036
1038
|
}
|
|
@@ -1221,6 +1223,17 @@ var SessionDecodeSession = class {
|
|
|
1221
1223
|
streamInfoProbed: raw.streamInfoProbed
|
|
1222
1224
|
});
|
|
1223
1225
|
return;
|
|
1226
|
+
case "preRollReport":
|
|
1227
|
+
this.onPreRollReport?.({
|
|
1228
|
+
offered: raw.offered,
|
|
1229
|
+
inferred: raw.inferred,
|
|
1230
|
+
dropped: raw.dropped,
|
|
1231
|
+
droppedByReason: raw.droppedByReason,
|
|
1232
|
+
strideMs: raw.strideMs,
|
|
1233
|
+
subsampled: raw.subsampled,
|
|
1234
|
+
spanMs: raw.spanMs
|
|
1235
|
+
});
|
|
1236
|
+
return;
|
|
1224
1237
|
}
|
|
1225
1238
|
}
|
|
1226
1239
|
handleExit(info) {
|
|
@@ -1445,7 +1458,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1445
1458
|
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
1446
1459
|
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
1447
1460
|
return { async *generateVideoFrames(source, opts) {
|
|
1448
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
|
|
1461
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
|
|
1449
1462
|
session.start(source, opts);
|
|
1450
1463
|
try {
|
|
1451
1464
|
while (true) {
|
|
@@ -1454,7 +1467,9 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1454
1467
|
const { reply } = outcome;
|
|
1455
1468
|
yield {
|
|
1456
1469
|
timestamp: reply.timestamp,
|
|
1457
|
-
image: buildFrameImage(session, reply)
|
|
1470
|
+
image: buildFrameImage(session, reply),
|
|
1471
|
+
...reply.capturedAt !== void 0 ? { capturedAt: reply.capturedAt } : {},
|
|
1472
|
+
...reply.preRoll === true ? { preRoll: true } : {}
|
|
1458
1473
|
};
|
|
1459
1474
|
}
|
|
1460
1475
|
} finally {
|
|
@@ -1468,10 +1483,18 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1468
1483
|
* Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
|
|
1469
1484
|
* the detection working width (see {@link resolveWorkingResize}) so the ROI
|
|
1470
1485
|
* scaler ships a small frame instead of the full source resolution.
|
|
1471
|
-
* `capturedAt` is
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1486
|
+
* `capturedAt` is the frame's OWN moment: the worker's media clock when it has
|
|
1487
|
+
* one (`media-clock.ts`), else `Date.now()` as before, so downstream frameAge
|
|
1488
|
+
* metrics keep diffing against the wall clock (mirroring the shm-ring source,
|
|
1489
|
+
* which stamps commit time) — this is what lets Step 5 MEASURE session-decode
|
|
1490
|
+
* frameAge vs the shm-ring cameras.
|
|
1491
|
+
*
|
|
1492
|
+
* The fallback is deliberate and must stay: a `Date.now()` here for a frame
|
|
1493
|
+
* replayed out of ~4 s of retained history would date its detections, its
|
|
1494
|
+
* track's `firstSeen`, its media and the notification's instant at SESSION
|
|
1495
|
+
* OPEN. That is a silently wrong timestamp — nothing downstream can tell it
|
|
1496
|
+
* from a right one — and it is a worse failure than the late birth the pre-roll
|
|
1497
|
+
* pass exists to fix.
|
|
1475
1498
|
*/
|
|
1476
1499
|
async function toDecodedFrame(frame, format, viewSpec) {
|
|
1477
1500
|
const geometry = viewSpec ? require_default_detection_model.resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
|
|
@@ -1486,7 +1509,8 @@ async function toDecodedFrame(frame, format, viewSpec) {
|
|
|
1486
1509
|
height: geometry?.output.height ?? frame.image.height,
|
|
1487
1510
|
format: viewSpec?.format ?? format,
|
|
1488
1511
|
timestamp: frame.timestamp,
|
|
1489
|
-
capturedAt: Date.now()
|
|
1512
|
+
capturedAt: frame.capturedAt ?? Date.now(),
|
|
1513
|
+
...frame.preRoll === true ? { preRoll: true } : {}
|
|
1490
1514
|
};
|
|
1491
1515
|
}
|
|
1492
1516
|
/**
|
|
@@ -1507,7 +1531,8 @@ function startSessionDecodePump(deps) {
|
|
|
1507
1531
|
dialTiming = timing;
|
|
1508
1532
|
},
|
|
1509
1533
|
...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
|
|
1510
|
-
...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
|
|
1534
|
+
...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {},
|
|
1535
|
+
...deps.onPreRollReport ? { onPreRollReport: deps.onPreRollReport } : {}
|
|
1511
1536
|
}).generateVideoFrames(deps.source, deps.opts);
|
|
1512
1537
|
let stopped = false;
|
|
1513
1538
|
let releaseSpawnSlot = null;
|
|
@@ -1549,7 +1574,7 @@ function startSessionDecodePump(deps) {
|
|
|
1549
1574
|
}
|
|
1550
1575
|
let imageOwnershipTransferred = false;
|
|
1551
1576
|
try {
|
|
1552
|
-
if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame()) continue;
|
|
1577
|
+
if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame(frame.preRoll === true)) continue;
|
|
1553
1578
|
const image = frame.image;
|
|
1554
1579
|
const cropFn = image.nativeCrop;
|
|
1555
1580
|
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
@@ -1558,7 +1583,7 @@ function startSessionDecodePump(deps) {
|
|
|
1558
1583
|
markInferred: () => image.markInferred?.()
|
|
1559
1584
|
}) : void 0;
|
|
1560
1585
|
if (deps.frameRegistry && deps.onFrameRef && deps.inlineWhen?.() !== true) {
|
|
1561
|
-
const capturedAt = Date.now();
|
|
1586
|
+
const capturedAt = frame.capturedAt ?? Date.now();
|
|
1562
1587
|
const ref = deps.frameRegistry.register(image, {
|
|
1563
1588
|
timestamp: frame.timestamp,
|
|
1564
1589
|
capturedAt
|
|
@@ -1572,7 +1597,8 @@ function startSessionDecodePump(deps) {
|
|
|
1572
1597
|
format: ref.format,
|
|
1573
1598
|
timestamp: ref.timestamp,
|
|
1574
1599
|
capturedAt,
|
|
1575
|
-
frameRef: ref
|
|
1600
|
+
frameRef: ref,
|
|
1601
|
+
...frame.preRoll === true ? { preRoll: true } : {}
|
|
1576
1602
|
};
|
|
1577
1603
|
try {
|
|
1578
1604
|
if (!await deps.onFrameRef(lazyFrame, nativeCrop)) deps.frameRegistry.release(ref, "drop");
|
|
@@ -2425,7 +2451,7 @@ var EMPTY_SAMPLE = {
|
|
|
2425
2451
|
*/
|
|
2426
2452
|
function applyStallTick(sample, lateByMs, gcWindow, reportThresholdMs) {
|
|
2427
2453
|
if (lateByMs < reportThresholdMs) return sample;
|
|
2428
|
-
const verdict =
|
|
2454
|
+
const verdict = (0, _camstack_system_addon_utils.attributeStall)(lateByMs, Math.round(gcWindow.totalMs), gcWindow.count, Math.round(gcWindow.longestMs));
|
|
2429
2455
|
return {
|
|
2430
2456
|
count: sample.count + 1,
|
|
2431
2457
|
maxMs: Math.max(sample.maxMs, lateByMs),
|
|
@@ -4703,6 +4729,44 @@ function ownFrame(frame) {
|
|
|
4703
4729
|
};
|
|
4704
4730
|
}
|
|
4705
4731
|
//#endregion
|
|
4732
|
+
//#region src/pipeline-runner/pre-roll-lane.ts
|
|
4733
|
+
var PreRollLane = class {
|
|
4734
|
+
items = [];
|
|
4735
|
+
capacity;
|
|
4736
|
+
clone;
|
|
4737
|
+
dispose;
|
|
4738
|
+
_refused = 0;
|
|
4739
|
+
constructor(options) {
|
|
4740
|
+
this.capacity = Math.max(0, options.capacity);
|
|
4741
|
+
this.clone = options.clone;
|
|
4742
|
+
this.dispose = options.dispose;
|
|
4743
|
+
}
|
|
4744
|
+
/** Append one entry. `false` ⇒ the lane is full and the caller must log it. */
|
|
4745
|
+
enqueue(item) {
|
|
4746
|
+
if (this.items.length >= this.capacity) {
|
|
4747
|
+
this._refused++;
|
|
4748
|
+
return false;
|
|
4749
|
+
}
|
|
4750
|
+
this.items.push(this.clone(item));
|
|
4751
|
+
return true;
|
|
4752
|
+
}
|
|
4753
|
+
/** Oldest first — the point of the lane. */
|
|
4754
|
+
dequeue() {
|
|
4755
|
+
return this.items.shift();
|
|
4756
|
+
}
|
|
4757
|
+
get size() {
|
|
4758
|
+
return this.items.length;
|
|
4759
|
+
}
|
|
4760
|
+
/** Entries refused because the lane was full, for the session's report line. */
|
|
4761
|
+
get refused() {
|
|
4762
|
+
return this._refused;
|
|
4763
|
+
}
|
|
4764
|
+
clear() {
|
|
4765
|
+
for (const item of this.items) this.dispose?.(item);
|
|
4766
|
+
this.items.length = 0;
|
|
4767
|
+
}
|
|
4768
|
+
};
|
|
4769
|
+
//#endregion
|
|
4706
4770
|
//#region src/pipeline-runner/semaphore.ts
|
|
4707
4771
|
/**
|
|
4708
4772
|
* Counting semaphore with FIFO waiter queue. Used by the runner to bound
|
|
@@ -5021,8 +5085,6 @@ var PipelineTimingSampler = class {
|
|
|
5021
5085
|
this.audioSamples.clear();
|
|
5022
5086
|
}
|
|
5023
5087
|
};
|
|
5024
|
-
//#endregion
|
|
5025
|
-
//#region src/pipeline-runner/runner.ts
|
|
5026
5088
|
/**
|
|
5027
5089
|
* Bound on one `processFrame` call. Chosen well above the slowest legitimate
|
|
5028
5090
|
* inference seen in production (8 617 ms, GPU contention on the hub) so the
|
|
@@ -5197,6 +5259,14 @@ var PipelineRunner = class {
|
|
|
5197
5259
|
registration,
|
|
5198
5260
|
motionQueue,
|
|
5199
5261
|
detectionQueue,
|
|
5262
|
+
preRollQueue: new PreRollLane({
|
|
5263
|
+
capacity: this.config.maxPreRollQueueDepth ?? 12,
|
|
5264
|
+
clone: ownDetectionEntry,
|
|
5265
|
+
dispose: (entry) => {
|
|
5266
|
+
if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
|
|
5267
|
+
if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
|
|
5268
|
+
}
|
|
5269
|
+
}),
|
|
5200
5270
|
inferenceTimes: [],
|
|
5201
5271
|
shedFrames: 0,
|
|
5202
5272
|
inflight: 0,
|
|
@@ -5243,6 +5313,7 @@ var PipelineRunner = class {
|
|
|
5243
5313
|
if (state.phase === "active") this.detectionStreamHandler?.(deviceId, "unsubscribe");
|
|
5244
5314
|
state.motionQueue.clear();
|
|
5245
5315
|
state.detectionQueue.clear();
|
|
5316
|
+
state.preRollQueue.clear();
|
|
5246
5317
|
this.timingSampler.clearDroppedFramesSource(deviceId);
|
|
5247
5318
|
this.cameras.delete(deviceId);
|
|
5248
5319
|
this.lastAdmittedAt.delete(deviceId);
|
|
@@ -5324,6 +5395,59 @@ var PipelineRunner = class {
|
|
|
5324
5395
|
maxInflightPerCamera: cap
|
|
5325
5396
|
};
|
|
5326
5397
|
}
|
|
5398
|
+
/**
|
|
5399
|
+
* Queue one PRE-ROLL frame — a frame decoded out of the restreamer's retained
|
|
5400
|
+
* history at session open, carrying its own capture moment
|
|
5401
|
+
* (`session-decode/media-clock.ts`), not the moment it was decoded.
|
|
5402
|
+
*
|
|
5403
|
+
* Deliberately NOT routed through {@link reserveDetectionFrame}: that gate
|
|
5404
|
+
* paces on WALL CLOCK, and a catch-up burst delivers seconds of media inside
|
|
5405
|
+
* milliseconds of wall clock, so the governor would reject the whole burst
|
|
5406
|
+
* bar one frame. The burst's own bound is a MEDIA-time stride applied
|
|
5407
|
+
* upstream, under an explicit frame + wall-clock budget
|
|
5408
|
+
* (`session-decode/pre-roll-burst.ts`); by the time a frame reaches here it
|
|
5409
|
+
* has already been chosen.
|
|
5410
|
+
*
|
|
5411
|
+
* `false` means the lane is full or the camera is not taking detection
|
|
5412
|
+
* frames. The caller LOGS that: a burst silently truncated by the lane and a
|
|
5413
|
+
* burst that fit read identically otherwise, and this is a branch that drops
|
|
5414
|
+
* work.
|
|
5415
|
+
*/
|
|
5416
|
+
enqueuePreRollFrame(deviceId, frame, handle) {
|
|
5417
|
+
const state = this.cameras.get(deviceId);
|
|
5418
|
+
if (!state || state.phase !== "active") return false;
|
|
5419
|
+
return state.preRollQueue.enqueue({
|
|
5420
|
+
frame,
|
|
5421
|
+
handle
|
|
5422
|
+
});
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5425
|
+
* Whether one more history frame would be taken. Asked BEFORE pixels are
|
|
5426
|
+
* materialized so a burst that has already filled the lane costs no RGB
|
|
5427
|
+
* conversion, no IPC and no retention work — the same pixel-free discipline
|
|
5428
|
+
* the live path gets from `reserveDetectionFrame`.
|
|
5429
|
+
*/
|
|
5430
|
+
preRollLaneAccepting(deviceId) {
|
|
5431
|
+
const state = this.cameras.get(deviceId);
|
|
5432
|
+
if (!state || state.phase !== "active") return false;
|
|
5433
|
+
return state.preRollQueue.size < (this.config.maxPreRollQueueDepth ?? 12);
|
|
5434
|
+
}
|
|
5435
|
+
/** Frames of history waiting for a spare permit, for the session report line. */
|
|
5436
|
+
preRollQueueDepth(deviceId) {
|
|
5437
|
+
return this.cameras.get(deviceId)?.preRollQueue.size ?? 0;
|
|
5438
|
+
}
|
|
5439
|
+
/** Frames this camera lane REFUSED because it was full. */
|
|
5440
|
+
preRollRefusedCount(deviceId) {
|
|
5441
|
+
return this.cameras.get(deviceId)?.preRollQueue.refused ?? 0;
|
|
5442
|
+
}
|
|
5443
|
+
/**
|
|
5444
|
+
* Whether the node has a permit to spare for history right now — i.e. one
|
|
5445
|
+
* would still be free after the burst took one. See
|
|
5446
|
+
* {@link PRE_ROLL_PERMIT_RESERVE}.
|
|
5447
|
+
*/
|
|
5448
|
+
preRollDispatchable() {
|
|
5449
|
+
return this.semaphore.available > 1;
|
|
5450
|
+
}
|
|
5327
5451
|
enqueueReservedDetectionFrame(deviceId, frame, handle) {
|
|
5328
5452
|
const state = this.cameras.get(deviceId);
|
|
5329
5453
|
if (!state || state.phase !== "active") return false;
|
|
@@ -5754,7 +5878,7 @@ var PipelineRunner = class {
|
|
|
5754
5878
|
entry: state.detectionQueue.dequeue(),
|
|
5755
5879
|
state
|
|
5756
5880
|
};
|
|
5757
|
-
if (this.defaultRoundRobinKeys.length === 0) return
|
|
5881
|
+
if (this.defaultRoundRobinKeys.length === 0) return this.pickNextPreRollFrame();
|
|
5758
5882
|
const startIndex = this.defaultRoundRobinIndex;
|
|
5759
5883
|
for (let i = 0; i < this.defaultRoundRobinKeys.length; i++) {
|
|
5760
5884
|
const idx = (startIndex + i) % this.defaultRoundRobinKeys.length;
|
|
@@ -5773,6 +5897,30 @@ var PipelineRunner = class {
|
|
|
5773
5897
|
};
|
|
5774
5898
|
}
|
|
5775
5899
|
}
|
|
5900
|
+
return this.pickNextPreRollFrame();
|
|
5901
|
+
}
|
|
5902
|
+
/**
|
|
5903
|
+
* The catch-up lane, reached ONLY after every camera's live queue came back
|
|
5904
|
+
* empty. That ordering IS the priority rule: a live frame is always picked
|
|
5905
|
+
* first, so a pre-roll frame can never delay one — and the burst simply
|
|
5906
|
+
* stops progressing while the camera is busy, which is the correct behaviour
|
|
5907
|
+
* for one-shot work whose value does not decay.
|
|
5908
|
+
*/
|
|
5909
|
+
pickNextPreRollFrame() {
|
|
5910
|
+
if (!this.preRollDispatchable()) return null;
|
|
5911
|
+
for (const [deviceId, state] of this.cameras) {
|
|
5912
|
+
if (state.phase !== "active") continue;
|
|
5913
|
+
if (state.preRollQueue.size === 0) continue;
|
|
5914
|
+
if (state.detectionQueue.size > 0) continue;
|
|
5915
|
+
if (state.inflight >= this.maxInflightPerCamera()) continue;
|
|
5916
|
+
const entry = state.preRollQueue.dequeue();
|
|
5917
|
+
if (!entry) continue;
|
|
5918
|
+
return {
|
|
5919
|
+
deviceId,
|
|
5920
|
+
entry,
|
|
5921
|
+
state
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5776
5924
|
return null;
|
|
5777
5925
|
}
|
|
5778
5926
|
};
|
|
@@ -8576,10 +8724,23 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8576
8724
|
/** Attach → first decoded detection frame, one line per attach. See
|
|
8577
8725
|
* {@link FirstDetectionFrameTimer}. */
|
|
8578
8726
|
firstDetectionFrame = new FirstDetectionFrameTimer();
|
|
8727
|
+
/**
|
|
8728
|
+
* Frames this camera's pre-roll catch-up burst may send to inference.
|
|
8729
|
+
*
|
|
8730
|
+
* The switch and the budget are ONE number here on purpose: `false` and
|
|
8731
|
+
* `0` mean the same thing to every layer below (no burst is constructed, the
|
|
8732
|
+
* history is decoded to reach a GOP and then discarded exactly as it was
|
|
8733
|
+
* before D411), and two independent ways to say it is two ways to disagree.
|
|
8734
|
+
*/
|
|
8735
|
+
preRollBudgetFor(config) {
|
|
8736
|
+
if (!config.preRollInferenceEnabled) return 0;
|
|
8737
|
+
const frames = config.preRollInferenceFrames;
|
|
8738
|
+
return Number.isFinite(frames) && frames > 0 ? frames : 0;
|
|
8739
|
+
}
|
|
8579
8740
|
enqueueSharedDetectionFrame(deviceId, frame, handle, reservation) {
|
|
8580
8741
|
const runner = this.runner;
|
|
8581
8742
|
if (!runner) return false;
|
|
8582
|
-
const admitted = reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
8743
|
+
const admitted = frame.preRoll === true ? runner.enqueuePreRollFrame(deviceId, frame, handle) : reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
8583
8744
|
if (admitted) {
|
|
8584
8745
|
const latencyMs = this.firstDetectionFrame.takeLatency(deviceId, Date.now());
|
|
8585
8746
|
if (latencyMs !== null) this.ctx?.logger.info("session-decode: first detection frame after attach", {
|
|
@@ -8697,9 +8858,19 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8697
8858
|
source,
|
|
8698
8859
|
opts: {
|
|
8699
8860
|
fps: config.detectionFps,
|
|
8700
|
-
format: "rgb"
|
|
8861
|
+
format: "rgb",
|
|
8862
|
+
preRollMs: require_restream_intent.preRollRequestOf(source.restreamUrl),
|
|
8863
|
+
preRollMaxFrames: this.preRollBudgetFor(config)
|
|
8701
8864
|
},
|
|
8702
|
-
shouldMaterializeFrame: () => {
|
|
8865
|
+
shouldMaterializeFrame: (preRoll) => {
|
|
8866
|
+
if (preRoll) {
|
|
8867
|
+
const accepting = runner.preRollLaneAccepting(config.deviceId);
|
|
8868
|
+
reservation = {
|
|
8869
|
+
detection: accepting,
|
|
8870
|
+
motion: false
|
|
8871
|
+
};
|
|
8872
|
+
return accepting;
|
|
8873
|
+
}
|
|
8703
8874
|
reservation = this.reserveSharedDetectionFrame(config.deviceId);
|
|
8704
8875
|
const materialize = reservation.detection || reservation.motion;
|
|
8705
8876
|
this.retention?.noteFrame({
|
|
@@ -8733,7 +8904,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8733
8904
|
materialized: false,
|
|
8734
8905
|
workerToRunnerBytes: 0
|
|
8735
8906
|
});
|
|
8736
|
-
const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
|
|
8907
|
+
const admitted = current.detection && (frame.preRoll === true ? runner.enqueuePreRollFrame(config.deviceId, frame, handle) : runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle));
|
|
8737
8908
|
if (admitted) nativeCrop?.markInferred();
|
|
8738
8909
|
if (current.motion) {
|
|
8739
8910
|
const motion = await resolveMotionFrameView(require_default_detection_model.localFrameRegistry, frame.frameRef);
|
|
@@ -8750,6 +8921,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8750
8921
|
}
|
|
8751
8922
|
} : { inlineViewSpec: SPLIT_DETECTION_VIEW },
|
|
8752
8923
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
8924
|
+
onPreRollReport: (report) => {
|
|
8925
|
+
log.info("session-decode: pre-roll catch-up burst", {
|
|
8926
|
+
tags: { deviceId: config.deviceId },
|
|
8927
|
+
meta: {
|
|
8928
|
+
streamId: config.detectionStreamId,
|
|
8929
|
+
requestedMs: require_restream_intent.preRollRequestOf(source.restreamUrl),
|
|
8930
|
+
spanMs: report.spanMs,
|
|
8931
|
+
offered: report.offered,
|
|
8932
|
+
inferred: report.inferred,
|
|
8933
|
+
dropped: report.dropped,
|
|
8934
|
+
droppedByReason: report.droppedByReason,
|
|
8935
|
+
strideMs: report.strideMs,
|
|
8936
|
+
subsampled: report.subsampled,
|
|
8937
|
+
laneRefused: runner.preRollRefusedCount(config.deviceId),
|
|
8938
|
+
budgetFrames: this.preRollBudgetFor(config)
|
|
8939
|
+
}
|
|
8940
|
+
});
|
|
8941
|
+
},
|
|
8753
8942
|
onFirstFrame: (timing) => {
|
|
8754
8943
|
const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
|
|
8755
8944
|
log.info("session-decode: detection first frame", {
|
|
@@ -9698,6 +9887,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9698
9887
|
frame: result,
|
|
9699
9888
|
nodeId: this.nodeId,
|
|
9700
9889
|
frameHandle: handle,
|
|
9890
|
+
emittedAt: Date.now(),
|
|
9701
9891
|
...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
|
|
9702
9892
|
...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
|
|
9703
9893
|
};
|