@camstack/addon-pipeline 1.2.106 → 1.2.115
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/{addon-utils-BEzVJ0xg.js → addon-utils-C0ht2KPO.js} +51 -15
- package/dist/{addon-utils-A2S9D7pu.mjs → addon-utils-CgBCF-mE.mjs} +50 -14
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/detection-pipeline/index.js +198 -38
- package/dist/detection-pipeline/index.mjs +197 -37
- package/dist/{dist-BSeR_hVv.js → dist-BqIbYW1A.js} +290 -17
- package/dist/{dist-lITI1Bn5.mjs → dist-DGQkkWc6.mjs} +273 -18
- package/dist/{event-loop-stall-monitor-0VvjTRDP.mjs → event-loop-stall-monitor-D5jK5v4Z.mjs} +223 -2
- package/dist/{event-loop-stall-monitor-BNsp9k8r.js → event-loop-stall-monitor-ylMGpz48.js} +234 -1
- package/dist/{lazy-sharp-CBohM8D8.js → lazy-sharp-cOsy7l_P.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +915 -107
- package/dist/pipeline-runner/index.mjs +914 -106
- package/dist/{process-memory-C33u3tQz.js → process-memory-45juuHpN.js} +1 -1
- package/dist/{process-memory-CEKVKfwg.mjs → process-memory-BtT7WfbJ.mjs} +1 -1
- package/dist/recorder/index.js +187 -47
- package/dist/recorder/index.mjs +187 -47
- package/dist/session-decode/decode-worker-child.js +30 -10
- package/dist/session-decode/decode-worker-child.mjs +29 -9
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-w2XLD3uI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_SFFNGT.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B1LDFHY9.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-DsIi4G5Q.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BAMyzCWw.mjs} +1 -1
- package/dist/stream-broker/{hostInit-B-h24cDy.mjs → hostInit-CG28NvlV.mjs} +3 -3
- package/dist/stream-broker/index.js +279 -198
- package/dist/stream-broker/index.mjs +279 -198
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CiKfal2P.js → worker-protocol-CIrqGbpG.js} +6 -3
- package/dist/{worker-protocol-BDiL8TBm.mjs → worker-protocol-DyocTIx_.mjs} +6 -3
- package/package.json +1 -1
- package/python/inference_pool.py +4 -1
- package/python/test_inference_pool_preprocess.py +87 -16
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DvZWLWk1.mjs +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { At as nodePin, Bt as literal, F as customAction, Ft as _enum, Ht as object, It as array, Kt as EventCategory, L as defineCustomActions, Lt as boolean, Tt as createEvent, Vt as number, Wt as string, et as pickDetailCropConvention, ht as errMsg, p as DEVICE_BACKEND_TO_FORMAT, rt as pipelineRunnerCapability, tt as pickNativeLeaseOverride, u as DEFAULT_DETAIL_CROP_CONVENTION, xt as BaseAddon, z as deriveDetailCropRect, zt as lazy } from "../dist-DGQkkWc6.mjs";
|
|
2
|
+
import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-D5jK5v4Z.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-DyocTIx_.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
6
6
|
import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
@@ -297,6 +297,48 @@ var NativeCropRegistry = class {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
};
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/session-decode/retained-content-fallback.ts
|
|
302
|
+
/**
|
|
303
|
+
* Honest ≤640 RAM fallback for `getNativeCrop` full-frame misses.
|
|
304
|
+
* Content-sized contain (16:9 → 640×360, 4:3 → 640×480, square → 640×640),
|
|
305
|
+
* never a detection default and never a cross-process ref. Task 3 owns
|
|
306
|
+
* whether this rung stays after telemetry.
|
|
307
|
+
*/
|
|
308
|
+
var RETAINED_FALLBACK_VIEW = {
|
|
309
|
+
content: {
|
|
310
|
+
width: 640,
|
|
311
|
+
height: 640
|
|
312
|
+
},
|
|
313
|
+
fit: "contain",
|
|
314
|
+
format: "rgb"
|
|
315
|
+
};
|
|
316
|
+
/** Resolve a content-sized RGB view from the process-local registry on demand. */
|
|
317
|
+
function createRetainedContentFallback(registry, ref) {
|
|
318
|
+
return { async materialize() {
|
|
319
|
+
const lease = registry.acquire(ref);
|
|
320
|
+
if (!lease) return null;
|
|
321
|
+
let reason = "error";
|
|
322
|
+
try {
|
|
323
|
+
const view = await lease.resolve(RETAINED_FALLBACK_VIEW);
|
|
324
|
+
if (view.kind !== "view" || view.format !== "rgb" || view.data.byteLength === 0) {
|
|
325
|
+
reason = "drop";
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
reason = "success";
|
|
329
|
+
return {
|
|
330
|
+
data: Buffer.from(view.data.buffer, view.data.byteOffset, view.data.byteLength),
|
|
331
|
+
width: view.width,
|
|
332
|
+
height: view.height,
|
|
333
|
+
format: "rgb",
|
|
334
|
+
timestamp: ref.timestamp,
|
|
335
|
+
...ref.capturedAt !== void 0 ? { capturedAt: ref.capturedAt } : {}
|
|
336
|
+
};
|
|
337
|
+
} finally {
|
|
338
|
+
lease.release(reason);
|
|
339
|
+
}
|
|
340
|
+
} };
|
|
341
|
+
}
|
|
300
342
|
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
301
343
|
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
302
344
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
@@ -344,12 +386,24 @@ var RETAINED_FRAME_KEY_PREFIX = "ram:";
|
|
|
344
386
|
function keyOf(handle) {
|
|
345
387
|
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
346
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Node-level RAM store of retained rgb detection frames, keyed by
|
|
391
|
+
* `FrameHandle` identity. Latest-wins with a byte budget.
|
|
392
|
+
*/
|
|
393
|
+
/** One retained frame plus the device it belongs to (for the per-device floor). */
|
|
394
|
+
/**
|
|
395
|
+
* Reserved footprint of a lazy contain ≤640 RGB view. Worst-case square
|
|
396
|
+
* 640×640×3 so 4:3 and portrait never exceed the reservation; 16:9 hydrates
|
|
397
|
+
* later settle to 640×360.
|
|
398
|
+
*/
|
|
399
|
+
var CONTENT_SIZED_RESERVE_BYTES = 640 * 640 * 3;
|
|
347
400
|
var RetainedFrameStore = class RetainedFrameStore {
|
|
348
401
|
nodeId;
|
|
349
402
|
budgetBytes;
|
|
350
403
|
perDeviceFloor;
|
|
351
404
|
timeFloorMs;
|
|
352
405
|
now;
|
|
406
|
+
onEvict;
|
|
353
407
|
/** Insertion-ordered (Map) → globally oldest is `keys().next()`. */
|
|
354
408
|
entries = /* @__PURE__ */ new Map();
|
|
355
409
|
/**
|
|
@@ -368,6 +422,7 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
368
422
|
this.perDeviceFloor = options.perDeviceFloor ?? RETAINED_FRAME_PER_DEVICE_FLOOR;
|
|
369
423
|
this.timeFloorMs = options.timeFloorMs ?? RETAINED_FRAME_TIME_FLOOR_MS;
|
|
370
424
|
this.now = options.now ?? Date.now;
|
|
425
|
+
this.onEvict = options.onEvict;
|
|
371
426
|
}
|
|
372
427
|
/** Number of retained frames (tests / metrics). */
|
|
373
428
|
get size() {
|
|
@@ -384,35 +439,76 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
384
439
|
* decode-worker IPC delivers a fresh Buffer per frame, so no defensive copy
|
|
385
440
|
* is needed.
|
|
386
441
|
*/
|
|
387
|
-
write(deviceId, frame) {
|
|
442
|
+
write(deviceId, frame, fallback) {
|
|
388
443
|
if (frame.format !== "rgb") return null;
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
slot: seq % RetainedFrameStore.SLOT_SPREAD,
|
|
394
|
-
seq,
|
|
395
|
-
width: frame.width,
|
|
396
|
-
height: frame.height,
|
|
397
|
-
format: "rgb",
|
|
398
|
-
pts: performance.now(),
|
|
399
|
-
byteLength,
|
|
400
|
-
nodeId: this.nodeId,
|
|
401
|
-
slotCount: RetainedFrameStore.SLOT_SPREAD
|
|
402
|
-
};
|
|
403
|
-
const key = keyOf(handle);
|
|
404
|
-
this.entries.set(key, {
|
|
444
|
+
const pixelFree = frame.data.byteLength === 0;
|
|
445
|
+
const handle = this.mintHandle(deviceId, frame);
|
|
446
|
+
const reservedBytes = pixelFree ? CONTENT_SIZED_RESERVE_BYTES : handle.byteLength;
|
|
447
|
+
this.insert(handle, {
|
|
405
448
|
frame,
|
|
406
449
|
deviceId,
|
|
407
|
-
writtenAt: this.now()
|
|
450
|
+
writtenAt: this.now(),
|
|
451
|
+
reservedBytes,
|
|
452
|
+
...pixelFree && fallback ? { fallback } : {}
|
|
408
453
|
});
|
|
409
|
-
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
410
|
-
if (deviceKeys) deviceKeys.push(key);
|
|
411
|
-
else this.perDeviceKeys.set(deviceId, [key]);
|
|
412
|
-
this.bytes += byteLength;
|
|
413
|
-
this.evictToBudget();
|
|
414
454
|
return handle;
|
|
415
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Identity-only handle for native hold/tile. Does not retain pixels or
|
|
458
|
+
* reserve the content-sized worst-case slot — Task 3 continuous path.
|
|
459
|
+
*/
|
|
460
|
+
allocateHandle(deviceId, frame) {
|
|
461
|
+
if (frame.format !== "rgb") return null;
|
|
462
|
+
return this.mintHandle(deviceId, frame);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Remember a detection-only on-demand fallback. Reserves the content-sized
|
|
466
|
+
* worst case until hydrate replaces it with the real JPEG/RGB bytes so the
|
|
467
|
+
* budget can evict unrequested detections. A no-detection frame must never
|
|
468
|
+
* call this.
|
|
469
|
+
*/
|
|
470
|
+
rememberOnDemand(deviceId, handle, fallback) {
|
|
471
|
+
const key = keyOf(handle);
|
|
472
|
+
if (this.entries.has(key)) return;
|
|
473
|
+
this.insert(handle, {
|
|
474
|
+
frame: {
|
|
475
|
+
data: Buffer.alloc(0),
|
|
476
|
+
width: handle.width,
|
|
477
|
+
height: handle.height,
|
|
478
|
+
format: "rgb",
|
|
479
|
+
timestamp: 0
|
|
480
|
+
},
|
|
481
|
+
deviceId,
|
|
482
|
+
writtenAt: this.now(),
|
|
483
|
+
reservedBytes: CONTENT_SIZED_RESERVE_BYTES,
|
|
484
|
+
fallback
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
has(handle) {
|
|
488
|
+
return this.entries.has(keyOf(handle));
|
|
489
|
+
}
|
|
490
|
+
getDeviceId(handle) {
|
|
491
|
+
return this.entries.get(keyOf(handle))?.deviceId;
|
|
492
|
+
}
|
|
493
|
+
statsByDevice(deviceId) {
|
|
494
|
+
const keys = this.perDeviceKeys.get(deviceId) ?? [];
|
|
495
|
+
let bytes = 0;
|
|
496
|
+
for (const key of keys) bytes += this.entries.get(key)?.reservedBytes ?? 0;
|
|
497
|
+
return {
|
|
498
|
+
entries: keys.length,
|
|
499
|
+
bytes
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
deviceStats() {
|
|
503
|
+
return [...this.perDeviceKeys.keys()].map((deviceId) => ({
|
|
504
|
+
deviceId,
|
|
505
|
+
...this.statsByDevice(deviceId)
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
/** Drop one handle (success/error/drop/detach). Idempotent. */
|
|
509
|
+
drop(handle) {
|
|
510
|
+
this.dropKey(keyOf(handle));
|
|
511
|
+
}
|
|
416
512
|
/** The retained frame for `handle`, or `null` if never retained / already evicted. */
|
|
417
513
|
getFrame(handle) {
|
|
418
514
|
return this.entries.get(keyOf(handle))?.frame ?? null;
|
|
@@ -427,11 +523,46 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
427
523
|
*/
|
|
428
524
|
crop(handle, bbox, maxWidth) {
|
|
429
525
|
const entry = this.entries.get(keyOf(handle));
|
|
430
|
-
if (!entry || entry.frame.
|
|
526
|
+
if (!entry || entry.frame.data.byteLength === 0) return null;
|
|
527
|
+
if (entry.frame.format === "jpeg") {
|
|
528
|
+
if (!(bbox.x <= .001 && bbox.y <= .001 && bbox.w >= .999 && bbox.h >= .999)) return null;
|
|
529
|
+
return {
|
|
530
|
+
bytes: entry.frame.data,
|
|
531
|
+
width: entry.frame.width,
|
|
532
|
+
height: entry.frame.height
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (entry.frame.format !== "rgb") return null;
|
|
431
536
|
return cropRgb(entry.frame, bbox, maxWidth);
|
|
432
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Full-frame / ROI crop that hydrates a pixel-free handle on demand.
|
|
540
|
+
* A missing fallback or failed materialize is an explicit miss — never a
|
|
541
|
+
* 0-byte raster.
|
|
542
|
+
*/
|
|
543
|
+
async cropAsync(handle, bbox, maxWidth) {
|
|
544
|
+
await this.ensureContent(handle);
|
|
545
|
+
return this.crop(handle, bbox, maxWidth);
|
|
546
|
+
}
|
|
547
|
+
/** Materialize a content-sized RGB view once, if the handle still has a fallback. */
|
|
548
|
+
async ensureContent(handle) {
|
|
549
|
+
const key = keyOf(handle);
|
|
550
|
+
const entry = this.entries.get(key);
|
|
551
|
+
if (!entry) return null;
|
|
552
|
+
if (entry.frame.data.byteLength > 0) return entry.frame;
|
|
553
|
+
if (entry.hydrating) return entry.hydrating;
|
|
554
|
+
if (!entry.fallback) return null;
|
|
555
|
+
entry.hydrating = this.hydrate(key, entry);
|
|
556
|
+
try {
|
|
557
|
+
return await entry.hydrating;
|
|
558
|
+
} finally {
|
|
559
|
+
const current = this.entries.get(key);
|
|
560
|
+
if (current) current.hydrating = void 0;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
433
563
|
/** Drop every retained frame (node/addon shutdown). */
|
|
434
564
|
clear() {
|
|
565
|
+
for (const entry of this.entries.values()) entry.fallback?.release?.();
|
|
435
566
|
this.entries.clear();
|
|
436
567
|
this.perDeviceKeys.clear();
|
|
437
568
|
this.bytes = 0;
|
|
@@ -456,7 +587,7 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
456
587
|
if (!entry) continue;
|
|
457
588
|
if (this.timeFloorMs > 0 && nowMs - entry.writtenAt < this.timeFloorMs) continue;
|
|
458
589
|
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
459
|
-
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key);
|
|
590
|
+
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key, "evict");
|
|
460
591
|
}
|
|
461
592
|
if (this.bytes <= this.budgetBytes) return;
|
|
462
593
|
for (const key of oldestFirst) {
|
|
@@ -464,27 +595,68 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
464
595
|
const entry = this.entries.get(key);
|
|
465
596
|
if (!entry) continue;
|
|
466
597
|
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
467
|
-
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key);
|
|
598
|
+
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key, "evict");
|
|
468
599
|
}
|
|
469
600
|
if (this.bytes <= this.budgetBytes) return;
|
|
470
601
|
for (const key of oldestFirst) {
|
|
471
602
|
if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
|
|
472
|
-
this.dropKey(key);
|
|
603
|
+
this.dropKey(key, "evict");
|
|
473
604
|
}
|
|
474
605
|
if (this.bytes < 0) this.bytes = 0;
|
|
475
606
|
}
|
|
607
|
+
async hydrate(key, entry) {
|
|
608
|
+
const view = await entry.fallback?.materialize();
|
|
609
|
+
const current = this.entries.get(key);
|
|
610
|
+
if (!current) return null;
|
|
611
|
+
if (!view || view.data.byteLength === 0) return null;
|
|
612
|
+
if (view.format !== "rgb" && view.format !== "jpeg") return null;
|
|
613
|
+
current.frame = view;
|
|
614
|
+
current.fallback = void 0;
|
|
615
|
+
this.bytes -= current.reservedBytes;
|
|
616
|
+
current.reservedBytes = view.data.byteLength;
|
|
617
|
+
this.bytes += current.reservedBytes;
|
|
618
|
+
this.evictToBudget();
|
|
619
|
+
return view;
|
|
620
|
+
}
|
|
621
|
+
mintHandle(deviceId, frame) {
|
|
622
|
+
const seq = ++this.seq;
|
|
623
|
+
const pixelFree = frame.data.byteLength === 0;
|
|
624
|
+
return {
|
|
625
|
+
shmId: `${RETAINED_FRAME_KEY_PREFIX}${this.nodeId}:${deviceId}`,
|
|
626
|
+
slot: seq % RetainedFrameStore.SLOT_SPREAD,
|
|
627
|
+
seq,
|
|
628
|
+
width: frame.width,
|
|
629
|
+
height: frame.height,
|
|
630
|
+
format: "rgb",
|
|
631
|
+
pts: performance.now(),
|
|
632
|
+
byteLength: pixelFree ? 0 : frame.data.byteLength,
|
|
633
|
+
nodeId: this.nodeId,
|
|
634
|
+
slotCount: RetainedFrameStore.SLOT_SPREAD
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
insert(handle, entry) {
|
|
638
|
+
const key = keyOf(handle);
|
|
639
|
+
this.entries.set(key, entry);
|
|
640
|
+
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
641
|
+
if (deviceKeys) deviceKeys.push(key);
|
|
642
|
+
else this.perDeviceKeys.set(entry.deviceId, [key]);
|
|
643
|
+
this.bytes += entry.reservedBytes;
|
|
644
|
+
this.evictToBudget();
|
|
645
|
+
}
|
|
476
646
|
/** Remove one frame by key, keeping the byte tally + per-device index exact. */
|
|
477
|
-
dropKey(key) {
|
|
647
|
+
dropKey(key, reason = "drop") {
|
|
478
648
|
const entry = this.entries.get(key);
|
|
479
649
|
if (!entry) return;
|
|
480
650
|
this.entries.delete(key);
|
|
481
|
-
|
|
651
|
+
entry.fallback?.release?.();
|
|
652
|
+
this.bytes -= entry.reservedBytes;
|
|
482
653
|
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
483
654
|
if (deviceKeys) {
|
|
484
655
|
const idx = deviceKeys.indexOf(key);
|
|
485
656
|
if (idx >= 0) deviceKeys.splice(idx, 1);
|
|
486
657
|
if (deviceKeys.length === 0) this.perDeviceKeys.delete(entry.deviceId);
|
|
487
658
|
}
|
|
659
|
+
if (reason === "evict") this.onEvict?.(entry.deviceId);
|
|
488
660
|
}
|
|
489
661
|
};
|
|
490
662
|
/**
|
|
@@ -761,7 +933,8 @@ var SessionDecodeSession = class {
|
|
|
761
933
|
this.settleNativeCrop(raw.requestId, {
|
|
762
934
|
bytes: raw.bytes,
|
|
763
935
|
width: raw.width,
|
|
764
|
-
height: raw.height
|
|
936
|
+
height: raw.height,
|
|
937
|
+
...raw.source ? { source: raw.source } : {}
|
|
765
938
|
});
|
|
766
939
|
return;
|
|
767
940
|
case "nativeCropMiss":
|
|
@@ -1021,26 +1194,6 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1021
1194
|
//#endregion
|
|
1022
1195
|
//#region src/session-decode/session-decode-pump.ts
|
|
1023
1196
|
/**
|
|
1024
|
-
* Detection working width cap — mirrors the shm decoder's
|
|
1025
|
-
* `initScaler` (`nodeav-decoder-session.ts`: `maxW = floor(640 / scale)`,
|
|
1026
|
-
* `scale = 1` default). The worker decodes full source resolution (e.g. a 4K
|
|
1027
|
-
* `native:main` stream); without this downscale `toBuffer` would ship a full
|
|
1028
|
-
* 3840×2160 rgb frame (~24 MB) over IPC per pull, which the detection path
|
|
1029
|
-
* cannot keep up with. Aspect ratio is preserved. A source already ≤ this
|
|
1030
|
-
* width (e.g. the small `native:sub` motion stream) is passed through
|
|
1031
|
-
* unscaled, exactly as the shm scaler's `min(srcW, maxW)` no-ops.
|
|
1032
|
-
*/
|
|
1033
|
-
var WORKING_MAX_WIDTH = 640;
|
|
1034
|
-
/** Aspect-preserving cap to {@link WORKING_MAX_WIDTH}; `undefined` when no downscale is needed. */
|
|
1035
|
-
function resolveWorkingResize(width, height) {
|
|
1036
|
-
if (width <= WORKING_MAX_WIDTH || width <= 0) return void 0;
|
|
1037
|
-
const outWidth = WORKING_MAX_WIDTH;
|
|
1038
|
-
return {
|
|
1039
|
-
width: outWidth,
|
|
1040
|
-
height: Math.round(outWidth * height / width)
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
1197
|
* Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
|
|
1045
1198
|
* the detection working width (see {@link resolveWorkingResize}) so the ROI
|
|
1046
1199
|
* scaler ships a small frame instead of the full source resolution.
|
|
@@ -1049,17 +1202,18 @@ function resolveWorkingResize(width, height) {
|
|
|
1049
1202
|
* time) — this is what lets Step 5 MEASURE session-decode frameAge vs the
|
|
1050
1203
|
* shm-ring cameras.
|
|
1051
1204
|
*/
|
|
1052
|
-
async function toDecodedFrame(frame, format) {
|
|
1053
|
-
const
|
|
1054
|
-
const bytes = await frame.image.toBuffer({
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1205
|
+
async function toDecodedFrame(frame, format, viewSpec) {
|
|
1206
|
+
const geometry = viewSpec ? resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
|
|
1207
|
+
const bytes = await frame.image.toBuffer(geometry ? {
|
|
1208
|
+
crop: geometry.sourceCrop,
|
|
1209
|
+
resize: geometry.output,
|
|
1210
|
+
format: viewSpec?.format ?? format
|
|
1211
|
+
} : { format });
|
|
1058
1212
|
return {
|
|
1059
1213
|
data: Buffer.from(bytes),
|
|
1060
|
-
width:
|
|
1061
|
-
height:
|
|
1062
|
-
format,
|
|
1214
|
+
width: geometry?.output.width ?? frame.image.width,
|
|
1215
|
+
height: geometry?.output.height ?? frame.image.height,
|
|
1216
|
+
format: viewSpec?.format ?? format,
|
|
1063
1217
|
timestamp: frame.timestamp,
|
|
1064
1218
|
capturedAt: Date.now()
|
|
1065
1219
|
};
|
|
@@ -1116,9 +1270,9 @@ function startSessionDecodePump(deps) {
|
|
|
1116
1270
|
frame.image.close();
|
|
1117
1271
|
break;
|
|
1118
1272
|
}
|
|
1273
|
+
let imageOwnershipTransferred = false;
|
|
1119
1274
|
try {
|
|
1120
1275
|
if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame()) continue;
|
|
1121
|
-
const decoded = await toDecodedFrame(frame, format);
|
|
1122
1276
|
const image = frame.image;
|
|
1123
1277
|
const cropFn = image.nativeCrop;
|
|
1124
1278
|
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
@@ -1126,7 +1280,33 @@ function startSessionDecodePump(deps) {
|
|
|
1126
1280
|
release: () => image.releaseNativeLease?.(),
|
|
1127
1281
|
markInferred: () => image.markInferred?.()
|
|
1128
1282
|
}) : void 0;
|
|
1129
|
-
deps.
|
|
1283
|
+
if (deps.frameRegistry && deps.onFrameRef) {
|
|
1284
|
+
const capturedAt = Date.now();
|
|
1285
|
+
const ref = deps.frameRegistry.register(image, {
|
|
1286
|
+
timestamp: frame.timestamp,
|
|
1287
|
+
capturedAt
|
|
1288
|
+
});
|
|
1289
|
+
imageOwnershipTransferred = true;
|
|
1290
|
+
if (!ref) continue;
|
|
1291
|
+
const lazyFrame = {
|
|
1292
|
+
data: Buffer.alloc(0),
|
|
1293
|
+
width: ref.width,
|
|
1294
|
+
height: ref.height,
|
|
1295
|
+
format: ref.format,
|
|
1296
|
+
timestamp: ref.timestamp,
|
|
1297
|
+
capturedAt,
|
|
1298
|
+
frameRef: ref
|
|
1299
|
+
};
|
|
1300
|
+
try {
|
|
1301
|
+
if (!await deps.onFrameRef(lazyFrame, nativeCrop)) deps.frameRegistry.release(ref, "drop");
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
deps.frameRegistry.release(ref, "error");
|
|
1304
|
+
throw err;
|
|
1305
|
+
}
|
|
1306
|
+
} else {
|
|
1307
|
+
const decoded = await toDecodedFrame(frame, format, deps.inlineViewSpec);
|
|
1308
|
+
deps.onDecodedFrame(decoded, nativeCrop);
|
|
1309
|
+
}
|
|
1130
1310
|
} catch (err) {
|
|
1131
1311
|
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
1132
1312
|
tags: { deviceId: deps.source.deviceId },
|
|
@@ -1136,7 +1316,7 @@ function startSessionDecodePump(deps) {
|
|
|
1136
1316
|
}
|
|
1137
1317
|
});
|
|
1138
1318
|
} finally {
|
|
1139
|
-
frame.image.close();
|
|
1319
|
+
if (!imageOwnershipTransferred) frame.image.close();
|
|
1140
1320
|
}
|
|
1141
1321
|
}
|
|
1142
1322
|
} catch (err) {
|
|
@@ -1526,6 +1706,424 @@ function buildBenchRunPipelineInput(req) {
|
|
|
1526
1706
|
};
|
|
1527
1707
|
}
|
|
1528
1708
|
//#endregion
|
|
1709
|
+
//#region src/pipeline-runner/frame-lazy-metrics.ts
|
|
1710
|
+
function emptyFrameLazyCounters() {
|
|
1711
|
+
return {
|
|
1712
|
+
framesDecoded: 0,
|
|
1713
|
+
framesAdmitted: 0,
|
|
1714
|
+
framesDroppedPixelFree: 0,
|
|
1715
|
+
viewsMaterialized: 0,
|
|
1716
|
+
viewsSkipped: 0,
|
|
1717
|
+
workerToRunnerBytes: 0,
|
|
1718
|
+
runnerToPoolRawBytes: 0,
|
|
1719
|
+
runnerToPoolJpegBytes: 0,
|
|
1720
|
+
onDemandFullFrameRequests: 0,
|
|
1721
|
+
onDemandCropRequests: 0,
|
|
1722
|
+
nativeHits: 0,
|
|
1723
|
+
nativeMisses: 0,
|
|
1724
|
+
tileHits: 0,
|
|
1725
|
+
tileMisses: 0,
|
|
1726
|
+
fallbackHits: 0,
|
|
1727
|
+
fallbackMisses: 0,
|
|
1728
|
+
retainedWritesAvoided: 0,
|
|
1729
|
+
residentRefs: 0,
|
|
1730
|
+
residentBytes: 0,
|
|
1731
|
+
releases: 0,
|
|
1732
|
+
evictions: 0,
|
|
1733
|
+
staleMisses: 0
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
var FrameLazyTelemetry = class {
|
|
1737
|
+
node = emptyFrameLazyCounters();
|
|
1738
|
+
cameras = /* @__PURE__ */ new Map();
|
|
1739
|
+
noteDecoded(deviceId) {
|
|
1740
|
+
this.node.framesDecoded += 1;
|
|
1741
|
+
this.camera(deviceId).framesDecoded += 1;
|
|
1742
|
+
}
|
|
1743
|
+
noteAdmitted(deviceId) {
|
|
1744
|
+
this.node.framesAdmitted += 1;
|
|
1745
|
+
this.camera(deviceId).framesAdmitted += 1;
|
|
1746
|
+
}
|
|
1747
|
+
noteDroppedPixelFree(deviceId) {
|
|
1748
|
+
this.node.framesDroppedPixelFree += 1;
|
|
1749
|
+
this.camera(deviceId).framesDroppedPixelFree += 1;
|
|
1750
|
+
}
|
|
1751
|
+
noteView(input) {
|
|
1752
|
+
if (input.materialized) {
|
|
1753
|
+
this.node.viewsMaterialized += 1;
|
|
1754
|
+
this.camera(input.deviceId).viewsMaterialized += 1;
|
|
1755
|
+
} else {
|
|
1756
|
+
this.node.viewsSkipped += 1;
|
|
1757
|
+
this.camera(input.deviceId).viewsSkipped += 1;
|
|
1758
|
+
}
|
|
1759
|
+
this.node.workerToRunnerBytes += input.workerToRunnerBytes;
|
|
1760
|
+
this.camera(input.deviceId).workerToRunnerBytes += input.workerToRunnerBytes;
|
|
1761
|
+
}
|
|
1762
|
+
notePoolBytes(input) {
|
|
1763
|
+
this.node.runnerToPoolRawBytes += input.raw;
|
|
1764
|
+
this.node.runnerToPoolJpegBytes += input.jpeg;
|
|
1765
|
+
this.camera(input.deviceId).runnerToPoolRawBytes += input.raw;
|
|
1766
|
+
this.camera(input.deviceId).runnerToPoolJpegBytes += input.jpeg;
|
|
1767
|
+
}
|
|
1768
|
+
noteOnDemandRequest(input) {
|
|
1769
|
+
if (input.kind === "full-frame") {
|
|
1770
|
+
this.node.onDemandFullFrameRequests += 1;
|
|
1771
|
+
this.camera(input.deviceId).onDemandFullFrameRequests += 1;
|
|
1772
|
+
} else {
|
|
1773
|
+
this.node.onDemandCropRequests += 1;
|
|
1774
|
+
this.camera(input.deviceId).onDemandCropRequests += 1;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
noteHit(kind, deviceId) {
|
|
1778
|
+
this.addHit(this.node, kind, 1);
|
|
1779
|
+
this.addHit(this.camera(deviceId), kind, 1);
|
|
1780
|
+
}
|
|
1781
|
+
noteMiss(kind, deviceId) {
|
|
1782
|
+
this.addMiss(this.node, kind, 1);
|
|
1783
|
+
this.addMiss(this.camera(deviceId), kind, 1);
|
|
1784
|
+
}
|
|
1785
|
+
noteAvoidedWrite(deviceId) {
|
|
1786
|
+
this.node.retainedWritesAvoided += 1;
|
|
1787
|
+
this.camera(deviceId).retainedWritesAvoided += 1;
|
|
1788
|
+
}
|
|
1789
|
+
noteRelease(deviceId) {
|
|
1790
|
+
this.node.releases += 1;
|
|
1791
|
+
if (deviceId !== void 0) this.camera(deviceId).releases += 1;
|
|
1792
|
+
}
|
|
1793
|
+
noteEviction(deviceId) {
|
|
1794
|
+
this.node.evictions += 1;
|
|
1795
|
+
if (deviceId !== void 0) this.camera(deviceId).evictions += 1;
|
|
1796
|
+
}
|
|
1797
|
+
noteStale(deviceId) {
|
|
1798
|
+
this.node.staleMisses += 1;
|
|
1799
|
+
if (deviceId !== void 0) this.camera(deviceId).staleMisses += 1;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Node occupancy. `residentRefs` is entry count (pending + store), not
|
|
1803
|
+
* outstanding registry leases.
|
|
1804
|
+
*/
|
|
1805
|
+
setResident(input) {
|
|
1806
|
+
this.node.residentRefs = input.refs;
|
|
1807
|
+
this.node.residentBytes = input.bytes;
|
|
1808
|
+
}
|
|
1809
|
+
setCameraResident(deviceId, input) {
|
|
1810
|
+
const counters = this.camera(deviceId);
|
|
1811
|
+
counters.residentRefs = input.refs;
|
|
1812
|
+
counters.residentBytes = input.bytes;
|
|
1813
|
+
}
|
|
1814
|
+
forgetCamera(deviceId) {
|
|
1815
|
+
this.cameras.delete(deviceId);
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Zero occupancy for already-tracked cameras absent from `present`.
|
|
1819
|
+
* Does not create or delete entries — snapshot still does not prune,
|
|
1820
|
+
* and detach still goes through {@link forgetCamera}.
|
|
1821
|
+
*/
|
|
1822
|
+
zeroAbsentResidents(present) {
|
|
1823
|
+
for (const [deviceId, counters] of this.cameras) {
|
|
1824
|
+
if (present.has(deviceId)) continue;
|
|
1825
|
+
counters.residentRefs = 0;
|
|
1826
|
+
counters.residentBytes = 0;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
snapshot(deviceIds) {
|
|
1830
|
+
return {
|
|
1831
|
+
node: { ...this.node },
|
|
1832
|
+
cameras: deviceIds.map((deviceId) => ({
|
|
1833
|
+
deviceId,
|
|
1834
|
+
...this.camera(deviceId)
|
|
1835
|
+
}))
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
trackedCameraCount() {
|
|
1839
|
+
return this.cameras.size;
|
|
1840
|
+
}
|
|
1841
|
+
camera(deviceId) {
|
|
1842
|
+
const existing = this.cameras.get(deviceId);
|
|
1843
|
+
if (existing) return existing;
|
|
1844
|
+
const created = emptyFrameLazyCounters();
|
|
1845
|
+
this.cameras.set(deviceId, created);
|
|
1846
|
+
return created;
|
|
1847
|
+
}
|
|
1848
|
+
addHit(counters, kind, n) {
|
|
1849
|
+
if (kind === "native") counters.nativeHits += n;
|
|
1850
|
+
else if (kind === "tile") counters.tileHits += n;
|
|
1851
|
+
else counters.fallbackHits += n;
|
|
1852
|
+
}
|
|
1853
|
+
addMiss(counters, kind, n) {
|
|
1854
|
+
if (kind === "native") counters.nativeMisses += n;
|
|
1855
|
+
else if (kind === "tile") counters.tileMisses += n;
|
|
1856
|
+
else counters.fallbackMisses += n;
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/pipeline-runner/frame-ref-transport.ts
|
|
1861
|
+
/** A local ref never leaves the grouped process; inline remains rollback/split compatibility. */
|
|
1862
|
+
function selectExecutorFrameSource(coLocated, frameRef, inlineFrame) {
|
|
1863
|
+
return coLocated && frameRef ? { frameRef } : { frame: inlineFrame };
|
|
1864
|
+
}
|
|
1865
|
+
//#endregion
|
|
1866
|
+
//#region src/session-decode/on-demand-jpeg-fallback.ts
|
|
1867
|
+
/** Single content-sized JPEG requested after a detection, never a continuous raw. */
|
|
1868
|
+
var ON_DEMAND_JPEG_VIEW = {
|
|
1869
|
+
content: {
|
|
1870
|
+
width: 640,
|
|
1871
|
+
height: 640
|
|
1872
|
+
},
|
|
1873
|
+
fit: "contain",
|
|
1874
|
+
format: "jpeg"
|
|
1875
|
+
};
|
|
1876
|
+
function createOnDemandJpegFallback(registry, ref) {
|
|
1877
|
+
const lease = registry.acquire(ref);
|
|
1878
|
+
let cached;
|
|
1879
|
+
let released = false;
|
|
1880
|
+
const releaseLease = (reason) => {
|
|
1881
|
+
if (released) return;
|
|
1882
|
+
released = true;
|
|
1883
|
+
lease?.release(reason);
|
|
1884
|
+
};
|
|
1885
|
+
return {
|
|
1886
|
+
async materialize() {
|
|
1887
|
+
if (cached !== void 0) return cached;
|
|
1888
|
+
if (!lease) {
|
|
1889
|
+
cached = null;
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
try {
|
|
1893
|
+
const view = await lease.resolve(ON_DEMAND_JPEG_VIEW);
|
|
1894
|
+
if (view.kind !== "view" || view.format !== "jpeg" || view.data.byteLength === 0) {
|
|
1895
|
+
cached = null;
|
|
1896
|
+
releaseLease("drop");
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
cached = {
|
|
1900
|
+
data: Buffer.from(view.data.buffer, view.data.byteOffset, view.data.byteLength),
|
|
1901
|
+
width: view.width,
|
|
1902
|
+
height: view.height,
|
|
1903
|
+
format: "jpeg",
|
|
1904
|
+
timestamp: ref.timestamp,
|
|
1905
|
+
...ref.capturedAt !== void 0 ? { capturedAt: ref.capturedAt } : {}
|
|
1906
|
+
};
|
|
1907
|
+
releaseLease("success");
|
|
1908
|
+
return cached;
|
|
1909
|
+
} catch {
|
|
1910
|
+
cached = null;
|
|
1911
|
+
releaseLease("error");
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
release() {
|
|
1916
|
+
releaseLease("eviction");
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
//#endregion
|
|
1921
|
+
//#region src/pipeline-runner/on-demand-retention.ts
|
|
1922
|
+
var CAMSTACK_CONTINUOUS_RETAIN = "CAMSTACK_CONTINUOUS_RETAIN";
|
|
1923
|
+
function isContinuousRetainEnabled(env = process.env) {
|
|
1924
|
+
return env[CAMSTACK_CONTINUOUS_RETAIN] === "1";
|
|
1925
|
+
}
|
|
1926
|
+
function shouldRetainDetectionFrame(input) {
|
|
1927
|
+
if (!input.admitted) return false;
|
|
1928
|
+
if (input.continuousRetain) return true;
|
|
1929
|
+
return input.hasDetection;
|
|
1930
|
+
}
|
|
1931
|
+
function encodeCrossNodeCrop(input) {
|
|
1932
|
+
if (!input.jpeg) return null;
|
|
1933
|
+
return { jpeg: input.jpeg };
|
|
1934
|
+
}
|
|
1935
|
+
function handleKey(handle) {
|
|
1936
|
+
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Pending occupancy exists only admission → inference result. A detection
|
|
1940
|
+
* transfers to {@link RetainedFrameStore} (authoritative late-media owner).
|
|
1941
|
+
* No-detection leaves zero state. Store eviction owns fallback cleanup.
|
|
1942
|
+
*/
|
|
1943
|
+
var OnDemandRetentionSession = class {
|
|
1944
|
+
store;
|
|
1945
|
+
registry;
|
|
1946
|
+
telemetry;
|
|
1947
|
+
continuousRetain;
|
|
1948
|
+
pending = /* @__PURE__ */ new Map();
|
|
1949
|
+
constructor(options) {
|
|
1950
|
+
this.store = options.store;
|
|
1951
|
+
this.registry = options.registry;
|
|
1952
|
+
this.telemetry = options.telemetry;
|
|
1953
|
+
this.continuousRetain = options.continuousRetain;
|
|
1954
|
+
}
|
|
1955
|
+
get pendingSize() {
|
|
1956
|
+
return this.pending.size;
|
|
1957
|
+
}
|
|
1958
|
+
hasPending(handle) {
|
|
1959
|
+
return this.pending.has(handleKey(handle));
|
|
1960
|
+
}
|
|
1961
|
+
admitFrame(deviceId, frame) {
|
|
1962
|
+
if (this.continuousRetain) {
|
|
1963
|
+
const fallback = frame.frameRef ? createRetainedContentFallback(this.registry, frame.frameRef) : void 0;
|
|
1964
|
+
const handle = this.store.write(deviceId, frame, fallback) ?? void 0;
|
|
1965
|
+
if (handle) this.pending.set(handleKey(handle), {
|
|
1966
|
+
deviceId,
|
|
1967
|
+
ref: frame.frameRef
|
|
1968
|
+
});
|
|
1969
|
+
this.refreshResident();
|
|
1970
|
+
return handle;
|
|
1971
|
+
}
|
|
1972
|
+
const handle = this.store.allocateHandle(deviceId, frame) ?? void 0;
|
|
1973
|
+
if (!handle) return void 0;
|
|
1974
|
+
this.pending.set(handleKey(handle), {
|
|
1975
|
+
deviceId,
|
|
1976
|
+
ref: frame.frameRef
|
|
1977
|
+
});
|
|
1978
|
+
this.telemetry.noteAvoidedWrite(deviceId);
|
|
1979
|
+
this.refreshResident();
|
|
1980
|
+
return handle;
|
|
1981
|
+
}
|
|
1982
|
+
noteFrame(input) {
|
|
1983
|
+
if (input.decoded) this.telemetry.noteDecoded(input.deviceId);
|
|
1984
|
+
if (input.admitted) this.telemetry.noteAdmitted(input.deviceId);
|
|
1985
|
+
if (input.droppedPixelFree) this.telemetry.noteDroppedPixelFree(input.deviceId);
|
|
1986
|
+
}
|
|
1987
|
+
noteInferenceResult(deviceId, handle, result) {
|
|
1988
|
+
if (!handle) return;
|
|
1989
|
+
const key = handleKey(handle);
|
|
1990
|
+
const pending = this.pending.get(key);
|
|
1991
|
+
this.forgetPending(key);
|
|
1992
|
+
if (!shouldRetainDetectionFrame({
|
|
1993
|
+
admitted: true,
|
|
1994
|
+
hasDetection: (result.detections?.length ?? 0) > 0,
|
|
1995
|
+
continuousRetain: this.continuousRetain
|
|
1996
|
+
})) {
|
|
1997
|
+
this.refreshResident();
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (this.continuousRetain) {
|
|
2001
|
+
this.refreshResident();
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (!pending?.ref) {
|
|
2005
|
+
this.refreshResident();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const fallback = createOnDemandJpegFallback(this.registry, pending.ref);
|
|
2009
|
+
this.store.rememberOnDemand(deviceId, handle, fallback);
|
|
2010
|
+
this.refreshResident();
|
|
2011
|
+
}
|
|
2012
|
+
async requestMedia(handle, request) {
|
|
2013
|
+
const deviceId = this.store.getDeviceId(handle) ?? this.pending.get(handleKey(handle))?.deviceId ?? 0;
|
|
2014
|
+
const kind = request.kind === "full-frame" ? "full-frame" : "crop";
|
|
2015
|
+
this.telemetry.noteOnDemandRequest({
|
|
2016
|
+
deviceId,
|
|
2017
|
+
kind
|
|
2018
|
+
});
|
|
2019
|
+
if (request.nativeHit) {
|
|
2020
|
+
this.telemetry.noteHit("native", deviceId);
|
|
2021
|
+
return {
|
|
2022
|
+
tier: "native",
|
|
2023
|
+
bytes: request.nativeHit.bytes,
|
|
2024
|
+
width: request.nativeHit.width,
|
|
2025
|
+
height: request.nativeHit.height
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
this.telemetry.noteMiss("native", deviceId);
|
|
2029
|
+
if (request.tileHit) {
|
|
2030
|
+
this.telemetry.noteHit("tile", deviceId);
|
|
2031
|
+
return {
|
|
2032
|
+
tier: "tile",
|
|
2033
|
+
bytes: request.tileHit.bytes,
|
|
2034
|
+
jpeg: request.tileHit.jpeg ? request.tileHit.bytes : void 0,
|
|
2035
|
+
width: request.tileHit.width,
|
|
2036
|
+
height: request.tileHit.height
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
this.telemetry.noteMiss("tile", deviceId);
|
|
2040
|
+
if (request.recordedStillHit) return {
|
|
2041
|
+
tier: "recorded-still",
|
|
2042
|
+
jpeg: request.recordedStillHit.jpeg,
|
|
2043
|
+
width: request.recordedStillHit.width,
|
|
2044
|
+
height: request.recordedStillHit.height
|
|
2045
|
+
};
|
|
2046
|
+
if (!this.continuousRetain && !this.store.has(handle)) {
|
|
2047
|
+
this.telemetry.noteMiss("fallback", deviceId);
|
|
2048
|
+
return { tier: "miss" };
|
|
2049
|
+
}
|
|
2050
|
+
const view = await this.store.ensureContent(handle);
|
|
2051
|
+
if (view && view.data.byteLength > 0) {
|
|
2052
|
+
this.telemetry.noteHit("fallback", deviceId);
|
|
2053
|
+
this.refreshResident();
|
|
2054
|
+
if (view.format === "jpeg") return {
|
|
2055
|
+
tier: "fallback-jpeg",
|
|
2056
|
+
jpeg: view.data,
|
|
2057
|
+
width: view.width,
|
|
2058
|
+
height: view.height
|
|
2059
|
+
};
|
|
2060
|
+
return {
|
|
2061
|
+
tier: "fallback-jpeg",
|
|
2062
|
+
bytes: view.data,
|
|
2063
|
+
width: view.width,
|
|
2064
|
+
height: view.height
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
this.telemetry.noteMiss("fallback", deviceId);
|
|
2068
|
+
this.telemetry.noteStale(deviceId);
|
|
2069
|
+
return { tier: "miss" };
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Pending cleanup. Success never invalidates a store entry already
|
|
2073
|
+
* transferred for late pull. Drop/error/detach drop the store only when the
|
|
2074
|
+
* handle was still pending (not yet transferred). Eviction owns transferred
|
|
2075
|
+
* fallback cleanup.
|
|
2076
|
+
*/
|
|
2077
|
+
release(handle, reason) {
|
|
2078
|
+
const key = handleKey(handle);
|
|
2079
|
+
const pending = this.pending.get(key);
|
|
2080
|
+
this.forgetPending(key);
|
|
2081
|
+
if (reason !== "success" && pending) this.store.drop(handle);
|
|
2082
|
+
this.telemetry.noteRelease(pending?.deviceId ?? this.store.getDeviceId(handle));
|
|
2083
|
+
this.refreshResident();
|
|
2084
|
+
}
|
|
2085
|
+
forgetDevice(deviceId) {
|
|
2086
|
+
for (const [key, pending] of [...this.pending]) {
|
|
2087
|
+
if (pending.deviceId !== deviceId) continue;
|
|
2088
|
+
this.pending.delete(key);
|
|
2089
|
+
}
|
|
2090
|
+
this.telemetry.forgetCamera(deviceId);
|
|
2091
|
+
this.refreshResident();
|
|
2092
|
+
}
|
|
2093
|
+
syncResident() {
|
|
2094
|
+
this.refreshResident();
|
|
2095
|
+
}
|
|
2096
|
+
forgetPending(key) {
|
|
2097
|
+
this.pending.delete(key);
|
|
2098
|
+
}
|
|
2099
|
+
refreshResident() {
|
|
2100
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
2101
|
+
for (const pending of this.pending.values()) {
|
|
2102
|
+
const cur = byDevice.get(pending.deviceId) ?? {
|
|
2103
|
+
refs: 0,
|
|
2104
|
+
bytes: 0
|
|
2105
|
+
};
|
|
2106
|
+
cur.refs += 1;
|
|
2107
|
+
byDevice.set(pending.deviceId, cur);
|
|
2108
|
+
}
|
|
2109
|
+
for (const stats of this.store.deviceStats()) {
|
|
2110
|
+
const cur = byDevice.get(stats.deviceId) ?? {
|
|
2111
|
+
refs: 0,
|
|
2112
|
+
bytes: 0
|
|
2113
|
+
};
|
|
2114
|
+
cur.refs += stats.entries;
|
|
2115
|
+
cur.bytes += stats.bytes;
|
|
2116
|
+
byDevice.set(stats.deviceId, cur);
|
|
2117
|
+
}
|
|
2118
|
+
this.telemetry.setResident({
|
|
2119
|
+
refs: this.pending.size + this.store.size,
|
|
2120
|
+
bytes: this.registry.estimatedBytes + this.store.retainedBytes
|
|
2121
|
+
});
|
|
2122
|
+
this.telemetry.zeroAbsentResidents(new Set(byDevice.keys()));
|
|
2123
|
+
for (const [deviceId, stats] of byDevice) this.telemetry.setCameraResident(deviceId, stats);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
//#endregion
|
|
1529
2127
|
//#region src/pipeline-runner/capacity/capacity-runner.ts
|
|
1530
2128
|
/**
|
|
1531
2129
|
* Run ONE ramp step: `cameras` independent open-loop pacers for
|
|
@@ -2435,6 +3033,35 @@ function summarizeRunnerHardware(hardware, cpuCores = os.cpus().length) {
|
|
|
2435
3033
|
};
|
|
2436
3034
|
}
|
|
2437
3035
|
//#endregion
|
|
3036
|
+
//#region src/pipeline-runner/motion-frame-view.ts
|
|
3037
|
+
var MOTION_FRAME_VIEW = {
|
|
3038
|
+
content: {
|
|
3039
|
+
width: 320,
|
|
3040
|
+
height: 320
|
|
3041
|
+
},
|
|
3042
|
+
fit: "contain",
|
|
3043
|
+
format: "gray"
|
|
3044
|
+
};
|
|
3045
|
+
/**
|
|
3046
|
+
* Materialize the motion gray view under a short-lived registry lease.
|
|
3047
|
+
* Release is idempotent and always runs, including miss/error.
|
|
3048
|
+
*/
|
|
3049
|
+
async function resolveMotionFrameView(registry, ref) {
|
|
3050
|
+
const lease = registry.acquire(ref);
|
|
3051
|
+
if (!lease) return {
|
|
3052
|
+
kind: "miss",
|
|
3053
|
+
reason: ref.registryId === registry.registryId ? "stale-ref" : "foreign-registry"
|
|
3054
|
+
};
|
|
3055
|
+
let reason = "error";
|
|
3056
|
+
try {
|
|
3057
|
+
const view = await lease.resolve(MOTION_FRAME_VIEW);
|
|
3058
|
+
reason = view.kind === "view" ? "success" : "drop";
|
|
3059
|
+
return view;
|
|
3060
|
+
} finally {
|
|
3061
|
+
lease.release(reason);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
//#endregion
|
|
2438
3065
|
//#region src/pipeline-runner/motion-settle-governor.ts
|
|
2439
3066
|
/**
|
|
2440
3067
|
* MotionSettleGovernor — decides WHEN the root-plane `package-detection` step
|
|
@@ -2697,6 +3324,13 @@ function resolveNativeCropSource(isFullFrame, nativeHit) {
|
|
|
2697
3324
|
if (nativeHit) return "native";
|
|
2698
3325
|
return isFullFrame ? "ram-fullframe" : "miss";
|
|
2699
3326
|
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Runtime hit kind for telemetry. A worker tile is native-resolution quality
|
|
3329
|
+
* (public wire `tier` stays `native`) but must not increment `nativeHits`.
|
|
3330
|
+
*/
|
|
3331
|
+
function resolveNativeCropHitKind(source) {
|
|
3332
|
+
return source === "tile" ? "tile" : "native";
|
|
3333
|
+
}
|
|
2700
3334
|
//#endregion
|
|
2701
3335
|
//#region src/pipeline-runner/native-lease-settings-source.ts
|
|
2702
3336
|
/**
|
|
@@ -2879,18 +3513,23 @@ var FrameQueue = class {
|
|
|
2879
3513
|
latest = null;
|
|
2880
3514
|
_droppedFrames = 0;
|
|
2881
3515
|
clone;
|
|
3516
|
+
dispose;
|
|
2882
3517
|
/**
|
|
2883
3518
|
* `clone` runs on every {@link enqueue} to detach a queue-owned copy from
|
|
2884
3519
|
* the (possibly borrowed) source — required by the D9 Task 7b ownership
|
|
2885
3520
|
* contract above. Motion call sites pass {@link ownFrame}; the detection
|
|
2886
3521
|
* path passes a clone that copies its inner frame's pixel buffer.
|
|
2887
3522
|
*/
|
|
2888
|
-
constructor(maxSize, clone) {
|
|
3523
|
+
constructor(maxSize, clone, dispose) {
|
|
2889
3524
|
this.maxSize = maxSize;
|
|
2890
3525
|
this.clone = clone;
|
|
3526
|
+
this.dispose = dispose;
|
|
2891
3527
|
}
|
|
2892
3528
|
enqueue(item) {
|
|
2893
|
-
if (this.latest !== null)
|
|
3529
|
+
if (this.latest !== null) {
|
|
3530
|
+
this._droppedFrames++;
|
|
3531
|
+
this.dispose?.(this.latest);
|
|
3532
|
+
}
|
|
2894
3533
|
this.latest = this.clone(item);
|
|
2895
3534
|
}
|
|
2896
3535
|
dequeue() {
|
|
@@ -2905,6 +3544,7 @@ var FrameQueue = class {
|
|
|
2905
3544
|
return this._droppedFrames;
|
|
2906
3545
|
}
|
|
2907
3546
|
clear() {
|
|
3547
|
+
if (this.latest !== null) this.dispose?.(this.latest);
|
|
2908
3548
|
this.latest = null;
|
|
2909
3549
|
}
|
|
2910
3550
|
};
|
|
@@ -3307,7 +3947,8 @@ function toFrameInput$1(frame) {
|
|
|
3307
3947
|
width: frame.width,
|
|
3308
3948
|
height: frame.height,
|
|
3309
3949
|
format: frame.format,
|
|
3310
|
-
timestamp: frame.timestamp
|
|
3950
|
+
timestamp: frame.timestamp,
|
|
3951
|
+
...frame.frameRef ? { frameRef: frame.frameRef } : {}
|
|
3311
3952
|
};
|
|
3312
3953
|
}
|
|
3313
3954
|
/**
|
|
@@ -3406,7 +4047,10 @@ var PipelineRunner = class {
|
|
|
3406
4047
|
}
|
|
3407
4048
|
registerCamera(deviceId, registration) {
|
|
3408
4049
|
const motionQueue = new FrameQueue(this.config.maxQueueDepth, ownMotionEntry);
|
|
3409
|
-
const detectionQueue = new FrameQueue(this.config.maxQueueDepth, ownDetectionEntry)
|
|
4050
|
+
const detectionQueue = new FrameQueue(this.config.maxQueueDepth, ownDetectionEntry, (entry) => {
|
|
4051
|
+
if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
|
|
4052
|
+
if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
|
|
4053
|
+
});
|
|
3410
4054
|
const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
|
|
3411
4055
|
const state = {
|
|
3412
4056
|
registration,
|
|
@@ -3804,9 +4448,13 @@ var PipelineRunner = class {
|
|
|
3804
4448
|
};
|
|
3805
4449
|
const semAcquiredAt = Date.now();
|
|
3806
4450
|
const executorHandle = state.detectionQueue.size <= HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH && this.semaphore.available > 0 ? handle : void 0;
|
|
4451
|
+
let frameReleaseReason = "error";
|
|
3807
4452
|
try {
|
|
3808
4453
|
const result = await this.awaitInferenceBounded(deviceId, frameInput, executorHandle, release);
|
|
3809
|
-
if (result === TIMED_OUT)
|
|
4454
|
+
if (result === TIMED_OUT) {
|
|
4455
|
+
frameReleaseReason = "drop";
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
3810
4458
|
const inferDoneAt = Date.now();
|
|
3811
4459
|
const inferenceMs = inferDoneAt - semAcquiredAt;
|
|
3812
4460
|
state.inferenceTimes.push(inferenceMs);
|
|
@@ -3815,6 +4463,7 @@ var PipelineRunner = class {
|
|
|
3815
4463
|
if (result) {
|
|
3816
4464
|
if (result.shed === true) state.shedFrames++;
|
|
3817
4465
|
await this.notifyCallbacks(deviceId, frame, result, streamType, handle);
|
|
4466
|
+
frameReleaseReason = "success";
|
|
3818
4467
|
const emittedAt = Date.now();
|
|
3819
4468
|
const capturedAt = frame.capturedAt;
|
|
3820
4469
|
this.timingSampler.addSample(deviceId, {
|
|
@@ -3826,9 +4475,16 @@ var PipelineRunner = class {
|
|
|
3826
4475
|
endToEnd: emittedAt - enqueuedAt,
|
|
3827
4476
|
detections: result.detections?.length ?? 0
|
|
3828
4477
|
});
|
|
3829
|
-
}
|
|
4478
|
+
} else frameReleaseReason = "drop";
|
|
4479
|
+
} catch {
|
|
4480
|
+
frameReleaseReason = "error";
|
|
3830
4481
|
} finally {
|
|
3831
4482
|
release();
|
|
4483
|
+
try {
|
|
4484
|
+
if (frameReleaseReason === "success" && handle && isContinuousRetainEnabled()) await this.config.hydrateRetainedFrame?.(handle);
|
|
4485
|
+
} catch {}
|
|
4486
|
+
if (handle) this.config.releaseRetention?.(handle, frameReleaseReason);
|
|
4487
|
+
if (frame.frameRef) this.config.releaseFrameRef?.(frame.frameRef, frameReleaseReason);
|
|
3832
4488
|
}
|
|
3833
4489
|
}
|
|
3834
4490
|
/**
|
|
@@ -4178,6 +4834,20 @@ var DEFAULT_CONFIG = {
|
|
|
4178
4834
|
* than raw RGB — the hop transits the hub main process (UDS child↔child
|
|
4179
4835
|
* routing), which OOM'd under raw frames. */
|
|
4180
4836
|
var DETECTION_HOP_JPEG_QUALITY = 82;
|
|
4837
|
+
var SPLIT_DETECTION_VIEW = {
|
|
4838
|
+
content: {
|
|
4839
|
+
width: 640,
|
|
4840
|
+
height: 640
|
|
4841
|
+
},
|
|
4842
|
+
fit: "contain",
|
|
4843
|
+
format: "rgb"
|
|
4844
|
+
};
|
|
4845
|
+
function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
|
|
4846
|
+
if (retention) return retention.admitFrame(deviceId, frame);
|
|
4847
|
+
if (!store) return void 0;
|
|
4848
|
+
if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
|
|
4849
|
+
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
|
|
4850
|
+
}
|
|
4181
4851
|
var DETECTION_KEEPWARM_MS = 15e3;
|
|
4182
4852
|
/**
|
|
4183
4853
|
* Occupancy-burst safety timeout (both the dial path and the analyzer-tap
|
|
@@ -4498,6 +5168,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4498
5168
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
4499
5169
|
*/
|
|
4500
5170
|
retainedFrames = null;
|
|
5171
|
+
frameLazy = new FrameLazyTelemetry();
|
|
5172
|
+
retention = null;
|
|
4501
5173
|
attached = /* @__PURE__ */ new Map();
|
|
4502
5174
|
/**
|
|
4503
5175
|
* Detection decode workers parked across a detach, keyed by deviceId — see
|
|
@@ -4676,7 +5348,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4676
5348
|
this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
|
|
4677
5349
|
this.retainedFrames = new RetainedFrameStore({
|
|
4678
5350
|
nodeId: this.nodeId,
|
|
4679
|
-
logger: this.ctx.logger.child("retained-frames")
|
|
5351
|
+
logger: this.ctx.logger.child("retained-frames"),
|
|
5352
|
+
onEvict: (deviceId) => this.frameLazy.noteEviction(deviceId)
|
|
5353
|
+
});
|
|
5354
|
+
this.retention = new OnDemandRetentionSession({
|
|
5355
|
+
store: this.retainedFrames,
|
|
5356
|
+
registry: localFrameRegistry,
|
|
5357
|
+
telemetry: this.frameLazy,
|
|
5358
|
+
nodeId: this.nodeId,
|
|
5359
|
+
continuousRetain: isContinuousRetainEnabled()
|
|
4680
5360
|
});
|
|
4681
5361
|
this.runner = new PipelineRunner({
|
|
4682
5362
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
@@ -4685,6 +5365,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4685
5365
|
minThrottledFps: this.config.minThrottledFps,
|
|
4686
5366
|
processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
|
|
4687
5367
|
analyzeMotion: (deviceId, frame, handle) => this.runMotionAnalysis(deviceId, frame, handle),
|
|
5368
|
+
releaseFrameRef: (ref, reason) => localFrameRegistry.release(ref, reason),
|
|
5369
|
+
releaseRetention: (handle, reason) => this.retention?.release(handle, reason),
|
|
5370
|
+
hydrateRetainedFrame: (handle) => this.retainedFrames?.ensureContent(handle) ?? Promise.resolve(null),
|
|
4688
5371
|
onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
|
|
4689
5372
|
logger: this.ctx.logger,
|
|
4690
5373
|
onOccupancyRecheck: (deviceId, frames) => {
|
|
@@ -4701,6 +5384,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4701
5384
|
this.handleDetectionStreamChange(deviceId, action);
|
|
4702
5385
|
});
|
|
4703
5386
|
this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
|
|
5387
|
+
this.retention?.noteInferenceResult(deviceId, handle, result);
|
|
4704
5388
|
this.cutSubjectTiles(deviceId, result, handle);
|
|
4705
5389
|
this.emitInferenceResult(deviceId, frame, result, handle);
|
|
4706
5390
|
const detectionCount = result.detections?.length ?? 0;
|
|
@@ -4796,6 +5480,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4796
5480
|
this.nativeCropRegistry.clear();
|
|
4797
5481
|
this.retainedFrames?.clear();
|
|
4798
5482
|
this.retainedFrames = null;
|
|
5483
|
+
this.retention = null;
|
|
4799
5484
|
}
|
|
4800
5485
|
async cacheBenchFrame(input) {
|
|
4801
5486
|
const { data, info } = await (await getSharp())(Buffer.from(input.imageBase64, "base64")).raw().toBuffer({ resolveWithObject: true });
|
|
@@ -5548,11 +6233,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5548
6233
|
* resolve legitimately want the lower-RES honest frame).
|
|
5549
6234
|
*/
|
|
5550
6235
|
async getNativeCrop(input) {
|
|
6236
|
+
const deviceId = this.nativeCropRegistry.deviceIdFor(input.handle) ?? 0;
|
|
6237
|
+
const isFullFrame = input.bbox.x <= .001 && input.bbox.y <= .001 && input.bbox.w >= .999 && input.bbox.h >= .999;
|
|
6238
|
+
this.frameLazy.noteOnDemandRequest({
|
|
6239
|
+
deviceId,
|
|
6240
|
+
kind: isFullFrame && input.maxWidth === void 0 ? "full-frame" : "crop"
|
|
6241
|
+
});
|
|
5551
6242
|
const resolved = await this.resolveNativeCropPixels(input);
|
|
5552
|
-
if (resolved.tier === "native" && resolved.pixels !== null)
|
|
6243
|
+
if (resolved.tier === "native" && resolved.pixels !== null) {
|
|
6244
|
+
this.frameLazy.noteHit(resolveNativeCropHitKind(resolved.pixels.source), deviceId);
|
|
6245
|
+
return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
|
|
6246
|
+
}
|
|
6247
|
+
this.frameLazy.noteMiss("native", deviceId);
|
|
5553
6248
|
if (resolved.tier !== "ram-fullframe") return null;
|
|
5554
|
-
const crop = this.retainedFrames?.
|
|
6249
|
+
const crop = await this.retainedFrames?.cropAsync(input.handle, input.bbox, input.maxWidth);
|
|
5555
6250
|
if (!crop) {
|
|
6251
|
+
this.frameLazy.noteMiss("fallback", deviceId);
|
|
5556
6252
|
this.ctx?.logger.warn("native full-frame miss — the retained store no longer holds this frame", {
|
|
5557
6253
|
tags: { deviceId: this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
5558
6254
|
meta: {
|
|
@@ -5566,6 +6262,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5566
6262
|
});
|
|
5567
6263
|
return null;
|
|
5568
6264
|
}
|
|
6265
|
+
this.frameLazy.noteHit("fallback", deviceId);
|
|
5569
6266
|
return this.encodeNativeCropResult({
|
|
5570
6267
|
bytes: crop.bytes,
|
|
5571
6268
|
width: crop.width,
|
|
@@ -5588,6 +6285,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5588
6285
|
*/
|
|
5589
6286
|
cutSubjectTiles(deviceId, result, handle) {
|
|
5590
6287
|
const outcome = this.dispatchSubjectTiles(result, handle);
|
|
6288
|
+
if ((result.detections?.length ?? 0) > 0) if (outcome === "sent") this.frameLazy.noteHit("tile", deviceId);
|
|
6289
|
+
else this.frameLazy.noteMiss("tile", deviceId);
|
|
5591
6290
|
if (outcome === "sent") return;
|
|
5592
6291
|
const tally = this.tileCutDrops.get(deviceId) ?? {
|
|
5593
6292
|
handleless: 0,
|
|
@@ -5689,7 +6388,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5689
6388
|
if (result) nativeResult = {
|
|
5690
6389
|
bytes: result.bytes,
|
|
5691
6390
|
width: result.width,
|
|
5692
|
-
height: result.height
|
|
6391
|
+
height: result.height,
|
|
6392
|
+
...result.source ? { source: result.source } : {}
|
|
5693
6393
|
};
|
|
5694
6394
|
else missReason = "worker-lease-gone";
|
|
5695
6395
|
} else missReason = "no-registry-entry";
|
|
@@ -5728,6 +6428,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5728
6428
|
* (`null`) — the caller retries — never a silent raw-RGB cross-node fallback.
|
|
5729
6429
|
*/
|
|
5730
6430
|
async encodeNativeCropResult(crop, tier, encodeJpeg) {
|
|
6431
|
+
if (crop.bytes.byteLength >= 2 && crop.bytes[0] === 255 && crop.bytes[1] === 216) {
|
|
6432
|
+
const encoded = encodeCrossNodeCrop({ jpeg: Buffer.from(crop.bytes).toString("base64") });
|
|
6433
|
+
if (!encoded) return null;
|
|
6434
|
+
return {
|
|
6435
|
+
jpeg: encoded.jpeg,
|
|
6436
|
+
width: crop.width,
|
|
6437
|
+
height: crop.height,
|
|
6438
|
+
tier
|
|
6439
|
+
};
|
|
6440
|
+
}
|
|
5731
6441
|
if (encodeJpeg !== true) return {
|
|
5732
6442
|
bytes: crop.bytes,
|
|
5733
6443
|
width: crop.width,
|
|
@@ -5735,12 +6445,17 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5735
6445
|
tier
|
|
5736
6446
|
};
|
|
5737
6447
|
try {
|
|
5738
|
-
|
|
6448
|
+
const encoded = encodeCrossNodeCrop({
|
|
5739
6449
|
jpeg: (await (await getSharp())(Buffer.from(crop.bytes), { raw: {
|
|
5740
6450
|
width: crop.width,
|
|
5741
6451
|
height: crop.height,
|
|
5742
6452
|
channels: 3
|
|
5743
6453
|
} }).jpeg({ quality: 90 }).toBuffer()).toString("base64"),
|
|
6454
|
+
bytes: crop.bytes
|
|
6455
|
+
});
|
|
6456
|
+
if (!encoded) return null;
|
|
6457
|
+
return {
|
|
6458
|
+
jpeg: encoded.jpeg,
|
|
5744
6459
|
width: crop.width,
|
|
5745
6460
|
height: crop.height,
|
|
5746
6461
|
tier
|
|
@@ -6081,6 +6796,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6081
6796
|
this.attached.delete(deviceId);
|
|
6082
6797
|
this.lastMotionAt.delete(deviceId);
|
|
6083
6798
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
6799
|
+
this.retention?.forgetDevice(deviceId);
|
|
6084
6800
|
this.runner?.unregisterCamera(deviceId);
|
|
6085
6801
|
this.ctx?.logger.info("detachCamera", {
|
|
6086
6802
|
tags: { deviceId },
|
|
@@ -6275,9 +6991,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6275
6991
|
avgInferenceTimeMs: 0,
|
|
6276
6992
|
queueDepth: 0
|
|
6277
6993
|
};
|
|
6994
|
+
this.retention?.syncResident();
|
|
6278
6995
|
return {
|
|
6279
6996
|
nodeId: this.nodeId,
|
|
6280
|
-
...m
|
|
6997
|
+
...m,
|
|
6998
|
+
frameLazy: this.frameLazy.snapshot(this.getLocalCameras())
|
|
6281
6999
|
};
|
|
6282
7000
|
}
|
|
6283
7001
|
async getCameraMetrics(input) {
|
|
@@ -6437,9 +7155,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6437
7155
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config), true),
|
|
6438
7156
|
startPump: (source) => {
|
|
6439
7157
|
const dialMs = Date.now() - requestedAt;
|
|
7158
|
+
const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
|
|
6440
7159
|
log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
|
|
6441
7160
|
streamId: config.detectionStreamId,
|
|
6442
|
-
codec: source.codec
|
|
7161
|
+
codec: source.codec,
|
|
7162
|
+
transport: useLocalFrameRef ? "local-frame-ref" : "inline-compat"
|
|
6443
7163
|
} });
|
|
6444
7164
|
let reservation = null;
|
|
6445
7165
|
return startSessionDecodePump({
|
|
@@ -6452,8 +7172,52 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6452
7172
|
},
|
|
6453
7173
|
shouldMaterializeFrame: () => {
|
|
6454
7174
|
reservation = this.reserveSharedDetectionFrame(config.deviceId);
|
|
6455
|
-
|
|
7175
|
+
const materialize = reservation.detection || reservation.motion;
|
|
7176
|
+
this.retention?.noteFrame({
|
|
7177
|
+
deviceId: config.deviceId,
|
|
7178
|
+
decoded: true,
|
|
7179
|
+
admitted: reservation.detection,
|
|
7180
|
+
droppedPixelFree: !materialize
|
|
7181
|
+
});
|
|
7182
|
+
if (!materialize) this.frameLazy.noteView({
|
|
7183
|
+
deviceId: config.deviceId,
|
|
7184
|
+
materialized: false,
|
|
7185
|
+
workerToRunnerBytes: 0
|
|
7186
|
+
});
|
|
7187
|
+
return materialize;
|
|
6456
7188
|
},
|
|
7189
|
+
...useLocalFrameRef ? {
|
|
7190
|
+
frameRegistry: localFrameRegistry,
|
|
7191
|
+
onFrameRef: async (frame, nativeCrop) => {
|
|
7192
|
+
const current = reservation;
|
|
7193
|
+
reservation = null;
|
|
7194
|
+
if (!current || !frame.frameRef) return false;
|
|
7195
|
+
const handle = current.detection ? writeRetainedDetectionFrame(retainedFrames, config.deviceId, frame, this.retention) : void 0;
|
|
7196
|
+
if (handle && nativeCrop) {
|
|
7197
|
+
this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
7198
|
+
this.bindFrameCutter(handle, nativeCrop);
|
|
7199
|
+
}
|
|
7200
|
+
this.frameLazy.noteView({
|
|
7201
|
+
deviceId: config.deviceId,
|
|
7202
|
+
materialized: false,
|
|
7203
|
+
workerToRunnerBytes: 0
|
|
7204
|
+
});
|
|
7205
|
+
const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
|
|
7206
|
+
if (current.motion) {
|
|
7207
|
+
const motion = await resolveMotionFrameView(localFrameRegistry, frame.frameRef);
|
|
7208
|
+
if (motion.kind === "view") runner.enqueueMotionFrame(config.deviceId, {
|
|
7209
|
+
data: Buffer.from(motion.data),
|
|
7210
|
+
width: motion.width,
|
|
7211
|
+
height: motion.height,
|
|
7212
|
+
format: "gray",
|
|
7213
|
+
timestamp: frame.timestamp,
|
|
7214
|
+
capturedAt: frame.capturedAt
|
|
7215
|
+
}, void 0);
|
|
7216
|
+
}
|
|
7217
|
+
if (admitted) nativeCrop?.markInferred();
|
|
7218
|
+
return admitted;
|
|
7219
|
+
}
|
|
7220
|
+
} : { inlineViewSpec: SPLIT_DETECTION_VIEW },
|
|
6457
7221
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
6458
7222
|
onFirstFrame: (timing) => {
|
|
6459
7223
|
const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
|
|
@@ -6475,7 +7239,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6475
7239
|
const current = reservation;
|
|
6476
7240
|
reservation = null;
|
|
6477
7241
|
if (!current) return;
|
|
6478
|
-
|
|
7242
|
+
this.frameLazy.noteView({
|
|
7243
|
+
deviceId: config.deviceId,
|
|
7244
|
+
materialized: frame.data.byteLength > 0,
|
|
7245
|
+
workerToRunnerBytes: frame.data.byteLength
|
|
7246
|
+
});
|
|
7247
|
+
const handle = current.detection ? writeRetainedDetectionFrame(retainedFrames, config.deviceId, frame, this.retention) : void 0;
|
|
6479
7248
|
if (handle && nativeCrop) {
|
|
6480
7249
|
this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
6481
7250
|
this.bindFrameCutter(handle, nativeCrop);
|
|
@@ -6521,6 +7290,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6521
7290
|
fps: config.motionFps,
|
|
6522
7291
|
format: "gray"
|
|
6523
7292
|
},
|
|
7293
|
+
inlineViewSpec: MOTION_FRAME_VIEW,
|
|
6524
7294
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "motion"),
|
|
6525
7295
|
onDecodedFrame: (frame) => {
|
|
6526
7296
|
runner.enqueueMotionFrame(config.deviceId, frame, void 0);
|
|
@@ -6694,7 +7464,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6694
7464
|
}
|
|
6695
7465
|
}
|
|
6696
7466
|
let image;
|
|
6697
|
-
|
|
7467
|
+
const executorCoLocated = this.executorCoLocated();
|
|
7468
|
+
if (!executorCoLocated) {
|
|
6698
7469
|
if (frame.format === "rgb") try {
|
|
6699
7470
|
const encoded = await sharp(frame.data, { raw: {
|
|
6700
7471
|
width: frame.width,
|
|
@@ -6706,10 +7477,25 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6706
7477
|
log.debug("runInference: frame JPEG encode failed — falling back to raw pixels", { meta: { error: errMsg(err) } });
|
|
6707
7478
|
}
|
|
6708
7479
|
}
|
|
7480
|
+
if (image !== void 0) this.frameLazy.notePoolBytes({
|
|
7481
|
+
deviceId,
|
|
7482
|
+
raw: 0,
|
|
7483
|
+
jpeg: image.byteLength
|
|
7484
|
+
});
|
|
7485
|
+
else if (executorCoLocated && frame.frameRef) this.frameLazy.notePoolBytes({
|
|
7486
|
+
deviceId,
|
|
7487
|
+
raw: 0,
|
|
7488
|
+
jpeg: 0
|
|
7489
|
+
});
|
|
7490
|
+
else this.frameLazy.notePoolBytes({
|
|
7491
|
+
deviceId,
|
|
7492
|
+
raw: frame.data.byteLength,
|
|
7493
|
+
jpeg: 0
|
|
7494
|
+
});
|
|
6709
7495
|
try {
|
|
6710
7496
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
6711
7497
|
steps: [...dispatchSteps],
|
|
6712
|
-
...image !== void 0 ? { image } :
|
|
7498
|
+
...image !== void 0 ? { image } : selectExecutorFrameSource(executorCoLocated, frame.frameRef, frame),
|
|
6713
7499
|
deviceId,
|
|
6714
7500
|
...camConfig?.deviceKey ? { deviceKey: camConfig.deviceKey } : {},
|
|
6715
7501
|
plane: "frame"
|
|
@@ -7065,31 +7851,53 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7065
7851
|
logger: log,
|
|
7066
7852
|
admitWorker: this.buildLiveWorkerAdmit(deviceId, "occupancy"),
|
|
7067
7853
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
7068
|
-
startPump: (source) =>
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7854
|
+
startPump: (source) => {
|
|
7855
|
+
const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
|
|
7856
|
+
return startSessionDecodePump({
|
|
7857
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
7858
|
+
logger: log,
|
|
7859
|
+
source,
|
|
7860
|
+
opts: {
|
|
7861
|
+
fps: config.detectionFps,
|
|
7862
|
+
format: "rgb"
|
|
7863
|
+
},
|
|
7864
|
+
...useLocalFrameRef ? {
|
|
7865
|
+
frameRegistry: localFrameRegistry,
|
|
7866
|
+
onFrameRef: (frame, nativeCrop) => {
|
|
7867
|
+
if (settled) return false;
|
|
7868
|
+
const handle = writeRetainedDetectionFrame(retainedFrames, config.deviceId, frame, this.retention);
|
|
7869
|
+
if (handle && nativeCrop) {
|
|
7870
|
+
this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
7871
|
+
this.bindFrameCutter(handle, nativeCrop);
|
|
7872
|
+
}
|
|
7873
|
+
const admitted = runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
7874
|
+
if (admitted) nativeCrop?.markInferred();
|
|
7875
|
+
count++;
|
|
7876
|
+
if (count >= frames) {
|
|
7877
|
+
clearTimeout(safetyTimer);
|
|
7878
|
+
cleanup();
|
|
7879
|
+
}
|
|
7880
|
+
return admitted;
|
|
7881
|
+
}
|
|
7882
|
+
} : { inlineViewSpec: SPLIT_DETECTION_VIEW },
|
|
7883
|
+
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "occupancy"),
|
|
7884
|
+
onEnded: () => cleanup(),
|
|
7885
|
+
onDecodedFrame: (frame, nativeCrop) => {
|
|
7886
|
+
if (settled) return;
|
|
7887
|
+
const handle = writeRetainedDetectionFrame(retainedFrames, config.deviceId, frame, this.retention);
|
|
7888
|
+
if (handle && nativeCrop) {
|
|
7889
|
+
this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
7890
|
+
this.bindFrameCutter(handle, nativeCrop);
|
|
7891
|
+
}
|
|
7892
|
+
if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
7893
|
+
count++;
|
|
7894
|
+
if (count >= frames) {
|
|
7895
|
+
clearTimeout(safetyTimer);
|
|
7896
|
+
cleanup();
|
|
7897
|
+
}
|
|
7090
7898
|
}
|
|
7091
|
-
}
|
|
7092
|
-
}
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7093
7901
|
});
|
|
7094
7902
|
if (settled) {
|
|
7095
7903
|
clearTimeout(safetyTimer);
|