@camstack/addon-pipeline 1.2.295 → 1.2.296
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/THIRD_PARTY_MODELS.md +8 -0
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-CAUBVbgK.mjs → default-detection-model-Co578D8C.mjs} +75 -46
- package/dist/{default-detection-model-wNEISVA9.js → default-detection-model-D1daTtqT.js} +75 -46
- package/dist/detection-pipeline/index.js +1023 -80
- package/dist/detection-pipeline/index.mjs +1023 -80
- package/dist/{dist-BsVcf5wO.js → dist-8up-f2TX.js} +877 -365
- package/dist/{dist-CuxSNLKW.mjs → dist-CCd0Q3nr.mjs} +872 -366
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-Cqb1QiXk.mjs → node-DgMSXSWP.mjs} +1 -1
- package/dist/{node-UFk6I2f6.js → node-lpQgHes9.js} +1 -1
- package/dist/pipeline-runner/index.js +789 -203
- package/dist/pipeline-runner/index.mjs +789 -203
- package/dist/{process-memory-CJV29sPf.js → process-memory-CX_92V_r.js} +1 -1
- package/dist/{process-memory-D0Nvs9rr.mjs → process-memory-DFC_O5zE.mjs} +1 -1
- package/dist/recorder/index.js +4 -6
- package/dist/recorder/index.mjs +4 -6
- package/dist/{segment-demux-js-DGwmf5hu.js → segment-demux-js-DzBx6NN2.js} +1 -1
- package/dist/{segment-demux-js-DIDWw1iE.mjs → segment-demux-js-FZbBuk3F.mjs} +1 -1
- package/dist/session-decode/{decode-worker-child.js → decode-worker-main.js} +481 -72
- package/dist/session-decode/{decode-worker-child.mjs → decode-worker-main.mjs} +482 -71
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B7nBFqva.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_i7oFBl.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DUGQKsKL.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-CkbplMHA.mjs +26 -0
- 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-BKlb3qac.mjs → hostInit-BPtppL3W.mjs} +2 -2
- package/dist/stream-broker/index.js +4 -4
- package/dist/stream-broker/index.mjs +4 -4
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-B2MfQLlu.js → worker-protocol-C-G8qmye.js} +3 -1
- package/dist/{worker-protocol-C_W-P_g-.mjs → worker-protocol-D_NzPcnh.mjs} +3 -1
- package/package.json +1 -1
- package/python/inference_pool.py +422 -64
- package/python/test_inference_pool_compile_off_loop.py +414 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCIyBvRa.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DeD_UFdb.mjs +0 -26
|
@@ -1,5 +1,73 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, r as logLevelForLine } from "../worker-protocol-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, r as logLevelForLine } from "../worker-protocol-D_NzPcnh.mjs";
|
|
2
2
|
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
3
|
+
var OutboundSendTracker = class {
|
|
4
|
+
inFlight = 0;
|
|
5
|
+
idleWaiters = [];
|
|
6
|
+
/** A send left for the channel; call the returned function from its callback. */
|
|
7
|
+
begin() {
|
|
8
|
+
this.inFlight += 1;
|
|
9
|
+
let done = false;
|
|
10
|
+
return () => {
|
|
11
|
+
if (done) return;
|
|
12
|
+
done = true;
|
|
13
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
14
|
+
if (this.inFlight > 0) return;
|
|
15
|
+
const waiters = this.idleWaiters;
|
|
16
|
+
this.idleWaiters = [];
|
|
17
|
+
for (const resolve of waiters) resolve();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
get pending() {
|
|
21
|
+
return this.inFlight;
|
|
22
|
+
}
|
|
23
|
+
/** Resolves when nothing is in flight, or after `boundMs` — whichever is first. */
|
|
24
|
+
whenIdle(boundMs) {
|
|
25
|
+
if (this.inFlight === 0) return Promise.resolve("idle");
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const timer = setTimeout(() => resolve("bound"), boundMs);
|
|
28
|
+
this.idleWaiters.push(() => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
resolve("idle");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createWorkerExit(deps) {
|
|
36
|
+
let exiting = false;
|
|
37
|
+
return (code) => {
|
|
38
|
+
if (exiting) return;
|
|
39
|
+
exiting = true;
|
|
40
|
+
let exitCode = code;
|
|
41
|
+
try {
|
|
42
|
+
deps.steps.quiesce();
|
|
43
|
+
} catch {
|
|
44
|
+
exitCode = 1;
|
|
45
|
+
} finally {
|
|
46
|
+
try {
|
|
47
|
+
deps.steps.flushFinalLines();
|
|
48
|
+
} catch {
|
|
49
|
+
exitCode = 1;
|
|
50
|
+
} finally {
|
|
51
|
+
deps.sends.whenIdle(deps.boundMs ?? 250).then(() => deps.exit(exitCode));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Route every event that ends the worker through the one bounded exit:
|
|
58
|
+
* SIGTERM (the parent's `kill()`) and a lost channel exit 0, an uncaught
|
|
59
|
+
* error is reported and exits 1. Before D652 there was no SIGTERM handler and
|
|
60
|
+
* a `kill()` ended the process with its last lines still queued.
|
|
61
|
+
*/
|
|
62
|
+
function wireWorkerExit(deps) {
|
|
63
|
+
deps.on("SIGTERM", () => deps.exitWorker(0));
|
|
64
|
+
deps.on("disconnect", () => deps.exitWorker(0));
|
|
65
|
+
deps.on("uncaughtException", (err) => {
|
|
66
|
+
deps.reportUncaught(err);
|
|
67
|
+
deps.exitWorker(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
3
71
|
//#region src/session-decode/color-conversion.ts
|
|
4
72
|
/**
|
|
5
73
|
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
@@ -301,6 +369,18 @@ var FrameSlot = class {
|
|
|
301
369
|
if (!this.reserved || this.reserved.frameId !== frameId) return null;
|
|
302
370
|
return this.reserved.frame;
|
|
303
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Free only the UNDELIVERED slot frame, keeping the reserved one (D652 drain):
|
|
374
|
+
* after a drain no `pull` will ever take the slot frame, while the reserved
|
|
375
|
+
* frame is one the parent may still read. Returns whether a frame was freed.
|
|
376
|
+
*/
|
|
377
|
+
freeUndelivered() {
|
|
378
|
+
const held = this.slot;
|
|
379
|
+
if (!held) return false;
|
|
380
|
+
held.frame.free();
|
|
381
|
+
this.slot = null;
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
304
384
|
/** Free both the slot and the reserved frame (teardown). */
|
|
305
385
|
free() {
|
|
306
386
|
this.slot?.frame.free();
|
|
@@ -614,6 +694,7 @@ var NativeLeaseStore = class {
|
|
|
614
694
|
budgetBytes;
|
|
615
695
|
maxFrames;
|
|
616
696
|
now;
|
|
697
|
+
onEvict;
|
|
617
698
|
bytes = 0;
|
|
618
699
|
/**
|
|
619
700
|
* Frames dropped because the hold bound or the byte ceiling was reached
|
|
@@ -626,6 +707,7 @@ var NativeLeaseStore = class {
|
|
|
626
707
|
this.budgetBytes = options.budgetBytes;
|
|
627
708
|
this.maxFrames = Math.max(1, options.maxFrames);
|
|
628
709
|
this.now = options.now ?? Date.now;
|
|
710
|
+
this.onEvict = options.onEvict;
|
|
629
711
|
}
|
|
630
712
|
/** Frames dropped by a bound rather than released (tests / metrics). */
|
|
631
713
|
get overflowCount() {
|
|
@@ -667,6 +749,7 @@ var NativeLeaseStore = class {
|
|
|
667
749
|
if (oldest === frameId) break;
|
|
668
750
|
this.overflowed += 1;
|
|
669
751
|
this.drop(oldest);
|
|
752
|
+
this.onEvict?.(oldest);
|
|
670
753
|
}
|
|
671
754
|
return true;
|
|
672
755
|
}
|
|
@@ -1350,6 +1433,40 @@ function planPreRollBurst(input) {
|
|
|
1350
1433
|
};
|
|
1351
1434
|
}
|
|
1352
1435
|
/**
|
|
1436
|
+
* Fit a burst inside the hold its frames live in (D652), without shrinking the
|
|
1437
|
+
* history it covers.
|
|
1438
|
+
*
|
|
1439
|
+
* Every pre-roll frame the runner admits is a lazy ref: its pixels are a frame
|
|
1440
|
+
* the worker HOLDS until inference reads it, and the hold keeps
|
|
1441
|
+
* `holdFrames`. A burst of 12 into a hold of 8, read oldest-first by the lane,
|
|
1442
|
+
* evicts its own oldest frames before inference reaches them — 54 of 55 live
|
|
1443
|
+
* `toBuffer miss … released/hold-full` lines named a frameId ≤ 12. So the burst
|
|
1444
|
+
* sends at most `holdFrames - 1` frames (the ceiling D367 already gives the
|
|
1445
|
+
* runner's in-flight frames, leaving the newest slot to the first live frame).
|
|
1446
|
+
*
|
|
1447
|
+
* The REACH is kept: the window handed to the planner is still
|
|
1448
|
+
* `requestedFrames × cadence`, so the capped frames are spread across the same
|
|
1449
|
+
* span of media the operator asked for (D382 — a consumer is served the history
|
|
1450
|
+
* it asked for), sampled less densely. The alternative, raising the hold for the
|
|
1451
|
+
* burst, would pin up to `requestedFrames - holdFrames + 1` extra native rasters
|
|
1452
|
+
* (~24 MB each at 4K) on every session open.
|
|
1453
|
+
*/
|
|
1454
|
+
function planPreRollWithinHold(input) {
|
|
1455
|
+
const requested = Number.isFinite(input.requestedFrames) && input.requestedFrames > 0 ? Math.floor(input.requestedFrames) : 0;
|
|
1456
|
+
const holdCeiling = Math.max(1, Math.floor(input.holdFrames) - 1);
|
|
1457
|
+
const budgetFrames = input.holdEnabled ? Math.min(requested, holdCeiling) : requested;
|
|
1458
|
+
const cadenceMs = Number.isFinite(input.cadenceMs) && input.cadenceMs > 0 ? input.cadenceMs : 1e3;
|
|
1459
|
+
return {
|
|
1460
|
+
budgetFrames,
|
|
1461
|
+
holdCapped: budgetFrames < requested,
|
|
1462
|
+
plan: planPreRollBurst({
|
|
1463
|
+
spanMs: requested * cadenceMs,
|
|
1464
|
+
detectionFps: 1e3 / cadenceMs,
|
|
1465
|
+
maxFrames: budgetFrames
|
|
1466
|
+
})
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1353
1470
|
* The live, per-session budget. Stateful by necessity — a stride is a decision
|
|
1354
1471
|
* about the PREVIOUS admission — and deliberately free of any clock of its own:
|
|
1355
1472
|
* both times are passed in, so a test can drive a catch-up burst without
|
|
@@ -1443,6 +1560,121 @@ var PreRollBurst = class {
|
|
|
1443
1560
|
}
|
|
1444
1561
|
};
|
|
1445
1562
|
//#endregion
|
|
1563
|
+
//#region src/session-decode/pre-roll-hold-ledger.ts
|
|
1564
|
+
var PreRollSettleTracker = class {
|
|
1565
|
+
pending = /* @__PURE__ */ new Set();
|
|
1566
|
+
read = 0;
|
|
1567
|
+
released = 0;
|
|
1568
|
+
lost = {
|
|
1569
|
+
evicted: 0,
|
|
1570
|
+
cleared: 0,
|
|
1571
|
+
"download-failed": 0,
|
|
1572
|
+
miss: 0
|
|
1573
|
+
};
|
|
1574
|
+
burstEnded = false;
|
|
1575
|
+
emitted = false;
|
|
1576
|
+
/** A pre-roll frame was delivered to the runner; inference has not read it yet. */
|
|
1577
|
+
admit(frameId) {
|
|
1578
|
+
if (this.emitted) return;
|
|
1579
|
+
this.pending.add(frameId);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* One frame's fate. Only the FIRST outcome of a delivered pre-roll frame
|
|
1583
|
+
* counts: a miss that follows an eviction is the same loss, not two. Returns
|
|
1584
|
+
* the summary to send when this settle was the last one owed after the burst
|
|
1585
|
+
* ended, else `null`.
|
|
1586
|
+
*/
|
|
1587
|
+
settle(frameId, outcome) {
|
|
1588
|
+
if (!this.pending.delete(frameId)) return null;
|
|
1589
|
+
if (outcome === "read") this.read += 1;
|
|
1590
|
+
else if (outcome === "released") this.released += 1;
|
|
1591
|
+
else this.lost[outcome] += 1;
|
|
1592
|
+
return this.summaryIfSettled();
|
|
1593
|
+
}
|
|
1594
|
+
/** Every frame still pending is lost the same way at once (a re-dial clearing the hold). */
|
|
1595
|
+
settleAll(outcome) {
|
|
1596
|
+
let summary = null;
|
|
1597
|
+
for (const frameId of [...this.pending]) summary = this.settle(frameId, outcome) ?? summary;
|
|
1598
|
+
return summary;
|
|
1599
|
+
}
|
|
1600
|
+
/** The burst is over: from now on the summary goes out when nothing is owed. */
|
|
1601
|
+
endBurst() {
|
|
1602
|
+
this.burstEnded = true;
|
|
1603
|
+
return this.summaryIfSettled();
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Teardown: send what is known now, `unsettled` included. `null` when there
|
|
1607
|
+
* was no burst or the summary already went out.
|
|
1608
|
+
*/
|
|
1609
|
+
flush() {
|
|
1610
|
+
if (this.emitted || !this.burstEnded) return null;
|
|
1611
|
+
return this.emit();
|
|
1612
|
+
}
|
|
1613
|
+
summaryIfSettled() {
|
|
1614
|
+
if (this.emitted || !this.burstEnded || this.pending.size > 0) return null;
|
|
1615
|
+
return this.emit();
|
|
1616
|
+
}
|
|
1617
|
+
emit() {
|
|
1618
|
+
this.emitted = true;
|
|
1619
|
+
return {
|
|
1620
|
+
read: this.read,
|
|
1621
|
+
released: this.released,
|
|
1622
|
+
lostByReason: { ...this.lost },
|
|
1623
|
+
unsettled: this.pending.size
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/session-decode/native-disposer.ts
|
|
1629
|
+
var NativeDisposer = class {
|
|
1630
|
+
onRefused;
|
|
1631
|
+
refusing = false;
|
|
1632
|
+
refused = 0;
|
|
1633
|
+
constructor(onRefused) {
|
|
1634
|
+
this.onRefused = onRefused;
|
|
1635
|
+
}
|
|
1636
|
+
/** From now on nothing native is disposed (the worker is exiting). Idempotent. */
|
|
1637
|
+
refuseFromNowOn() {
|
|
1638
|
+
this.refusing = true;
|
|
1639
|
+
}
|
|
1640
|
+
get isRefusing() {
|
|
1641
|
+
return this.refusing;
|
|
1642
|
+
}
|
|
1643
|
+
/** Disposes refused since {@link refuseFromNowOn} — diagnostics and tests. */
|
|
1644
|
+
get refusedCount() {
|
|
1645
|
+
return this.refused;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Run `dispose` unless the worker is exiting. `what` names the object for the
|
|
1649
|
+
* refusal line. Returns whether it ran — a caller that dropped its reference
|
|
1650
|
+
* either way loses nothing: an unrun dispose is reclaimed by process exit.
|
|
1651
|
+
*/
|
|
1652
|
+
run(what, dispose) {
|
|
1653
|
+
if (this.refusing) {
|
|
1654
|
+
this.refused += 1;
|
|
1655
|
+
this.onRefused?.(what);
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
dispose();
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region src/session-decode/decode-loop-policy.ts
|
|
1664
|
+
function inputCloseAfterLoop(state) {
|
|
1665
|
+
if (state.exiting) return "none";
|
|
1666
|
+
if (state.draining) return "demuxer-only";
|
|
1667
|
+
return "all";
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* The frame decoded after a drain/stop is nobody's: free it — unless the
|
|
1671
|
+
* worker is exiting, where exit reclaims it and no native call is made.
|
|
1672
|
+
*/
|
|
1673
|
+
function releaseFrameAfterStop(frame, state) {
|
|
1674
|
+
if (state.exiting || frame === null) return;
|
|
1675
|
+
frame.free();
|
|
1676
|
+
}
|
|
1677
|
+
//#endregion
|
|
1446
1678
|
//#region src/session-decode/pull-demuxer-open.ts
|
|
1447
1679
|
/**
|
|
1448
1680
|
* Open the pull input, skipping `avformat_find_stream_info` when the opened
|
|
@@ -2013,10 +2245,11 @@ async function getConstants() {
|
|
|
2013
2245
|
}
|
|
2014
2246
|
}
|
|
2015
2247
|
/** The HOLD store, sized from one session's knobs. */
|
|
2016
|
-
function buildLeaseStore(lease) {
|
|
2248
|
+
function buildLeaseStore(lease, onEvict) {
|
|
2017
2249
|
return new NativeLeaseStore({
|
|
2018
2250
|
budgetBytes: lease.values.budgetMb * MB,
|
|
2019
|
-
maxFrames: lease.values.holdFrames
|
|
2251
|
+
maxFrames: lease.values.holdFrames,
|
|
2252
|
+
...onEvict ? { onEvict } : {}
|
|
2020
2253
|
});
|
|
2021
2254
|
}
|
|
2022
2255
|
/** A TILE store (subject or scene) under `budgetMb`, on the shared tile TTL. */
|
|
@@ -2136,11 +2369,17 @@ function packSinglePlane(plane, linesize, width, height, channels) {
|
|
|
2136
2369
|
else for (let row = 0; row < height; row++) plane.copy(out, row * rowBytes, row * stride, row * stride + rowBytes);
|
|
2137
2370
|
return out;
|
|
2138
2371
|
}
|
|
2139
|
-
/**
|
|
2140
|
-
* The forked child's whole runtime: decode-loop lifecycle, the latest-wins
|
|
2141
|
-
* frame slot, and the ROI scaler for `toBuffer`. One instance per process.
|
|
2142
|
-
*/
|
|
2143
2372
|
var DecodeWorkerChild = class {
|
|
2373
|
+
/**
|
|
2374
|
+
* Every native dispose / close goes through here, and it refuses once the
|
|
2375
|
+
* exit has begun (D652) — see `native-disposer.ts`.
|
|
2376
|
+
*/
|
|
2377
|
+
native;
|
|
2378
|
+
requestExit;
|
|
2379
|
+
constructor(hooks = {}) {
|
|
2380
|
+
this.requestExit = hooks.requestExit ?? (() => void 0);
|
|
2381
|
+
this.native = hooks.disposer ?? new NativeDisposer((what) => this.emitStderr(`decode-worker-child: native dispose left to process exit — ${what}\n`, "debug"));
|
|
2382
|
+
}
|
|
2144
2383
|
nav = null;
|
|
2145
2384
|
consts = null;
|
|
2146
2385
|
hwContext = null;
|
|
@@ -2151,7 +2390,7 @@ var DecodeWorkerChild = class {
|
|
|
2151
2390
|
* ~10GB/h). Detection uses a stable ROI + resize target, so the scaler is
|
|
2152
2391
|
* built once and reused, rebuilt only when the key changes — mirroring the
|
|
2153
2392
|
* shared decoder session (`nodeav-decoder-session.ts`, which reuses one
|
|
2154
|
-
* `this.scaler` for the same reason).
|
|
2393
|
+
* `this.scaler` for the same reason). Reclaimed by process exit (D652).
|
|
2155
2394
|
*/
|
|
2156
2395
|
scaler = null;
|
|
2157
2396
|
scalerKey = "";
|
|
@@ -2176,7 +2415,7 @@ var DecodeWorkerChild = class {
|
|
|
2176
2415
|
* for — the HW analogue of {@link scaler}/{@link scalerKey}. Rebuilt (old
|
|
2177
2416
|
* CLOSED) only when the key changes. Its internal `scale_<be>` OUTPUT hw
|
|
2178
2417
|
* frames pool is the leak-prone VAAPI surface pool, so it is closed on EVERY
|
|
2179
|
-
* re-dial ({@link closeInput}) and
|
|
2418
|
+
* re-dial ({@link closeInput}) and by process exit — this discipline is what stops
|
|
2180
2419
|
* the prior "keeping decoded frames on the GPU leaked the VAAPI surface pool
|
|
2181
2420
|
* on every re-dial" incident from recurring.
|
|
2182
2421
|
*/
|
|
@@ -2188,7 +2427,7 @@ var DecodeWorkerChild = class {
|
|
|
2188
2427
|
* evicts (rebuilds) the hot-path detection filter cache — the crop geometry
|
|
2189
2428
|
* differs from the stable detection geometry, so sharing one cache would
|
|
2190
2429
|
* thrash it. Same dispose discipline: both are closed on every re-dial
|
|
2191
|
-
* ({@link closeInput}) and
|
|
2430
|
+
* ({@link closeInput}) and by process exit. Rebuilt lazily per crop geometry.
|
|
2192
2431
|
*/
|
|
2193
2432
|
nativeCropHwFilter = null;
|
|
2194
2433
|
nativeCropHwFilterKey = "";
|
|
@@ -2200,7 +2439,7 @@ var DecodeWorkerChild = class {
|
|
|
2200
2439
|
* request that follows. Hard-capped at {@link NATIVE_RING_CAP}; single
|
|
2201
2440
|
* ownership via the {@link FrameSlot} `retainSuperseded` hook (a frame enters
|
|
2202
2441
|
* the ring exactly when the slot would otherwise free it). Cleared on every
|
|
2203
|
-
* re-dial
|
|
2442
|
+
* re-dial, reclaimed by process exit (see {@link closeInput}).
|
|
2204
2443
|
*/
|
|
2205
2444
|
nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
|
|
2206
2445
|
/**
|
|
@@ -2209,7 +2448,7 @@ var DecodeWorkerChild = class {
|
|
|
2209
2448
|
* eviction. Replaces the tiny GPU {@link nativeRing} as the default (the ring
|
|
2210
2449
|
* is used only when the lease is disabled via a 0 budget). Because it holds
|
|
2211
2450
|
* DOWNLOADED (system-memory) frames, retaining a large window has no VAAPI
|
|
2212
|
-
* surface-pool starvation risk. Cleared on every re-dial
|
|
2451
|
+
* surface-pool starvation risk. Cleared on every re-dial; reclaimed by process exit.
|
|
2213
2452
|
*/
|
|
2214
2453
|
leaseStore = buildLeaseStore(UNSTARTED_LEASE_KNOBS);
|
|
2215
2454
|
/**
|
|
@@ -2295,7 +2534,7 @@ var DecodeWorkerChild = class {
|
|
|
2295
2534
|
* materialize a HW surface into a leasable software frame at NATIVE
|
|
2296
2535
|
* resolution (no scale). Kept separate from the detection/native-crop filters
|
|
2297
2536
|
* (different geometry). Same dispose discipline: closed on every re-dial
|
|
2298
|
-
* ({@link closeInput})
|
|
2537
|
+
* ({@link closeInput}), since it references the decoder's per-dial
|
|
2299
2538
|
* `hw_frames_ctx`.
|
|
2300
2539
|
*/
|
|
2301
2540
|
nativeLeaseDownloadFilter = null;
|
|
@@ -2389,6 +2628,19 @@ var DecodeWorkerChild = class {
|
|
|
2389
2628
|
started = false;
|
|
2390
2629
|
stopped = false;
|
|
2391
2630
|
/**
|
|
2631
|
+
* Set by `drain` (D652): decoding has stopped but every frame already handed
|
|
2632
|
+
* out is kept, so the parent can still read what it admitted. The decode
|
|
2633
|
+
* loop's per-dial `closeInput` is skipped while this is set — that is what
|
|
2634
|
+
* would free the hold — and `stop` ends the process, whose exit reclaims everything.
|
|
2635
|
+
*/
|
|
2636
|
+
draining = false;
|
|
2637
|
+
/**
|
|
2638
|
+
* Set by the EXIT path (SIGTERM / `stop` / a lost channel / an uncaught
|
|
2639
|
+
* error): nothing native is disposed from here on — the decode loop may
|
|
2640
|
+
* still be inside libav, and process exit reclaims it all (D652).
|
|
2641
|
+
*/
|
|
2642
|
+
exiting = false;
|
|
2643
|
+
/**
|
|
2392
2644
|
* The numeric device id this worker decodes for — learned from the `start`
|
|
2393
2645
|
* message's {@link VideoFrameSource}. The child's stderr is INHERITED by the
|
|
2394
2646
|
* pipeline-runner (silent:false fork), so these lines land in the runner's
|
|
@@ -2422,6 +2674,16 @@ var DecodeWorkerChild = class {
|
|
|
2422
2674
|
preRollBurst = null;
|
|
2423
2675
|
/** Whether the burst's report has already been sent — it is sent exactly once. */
|
|
2424
2676
|
preRollReported = false;
|
|
2677
|
+
/**
|
|
2678
|
+
* What became of the pre-roll frames the burst delivered — read, let go, or
|
|
2679
|
+
* lost after the burst's report went out (D652). Its summary is a second,
|
|
2680
|
+
* later line (`preRollSettled`), never a delay on the report itself.
|
|
2681
|
+
*/
|
|
2682
|
+
preRollSettle = new PreRollSettleTracker();
|
|
2683
|
+
/** Every IPC send until its callback fires, so an exit can wait for them (D652). */
|
|
2684
|
+
outbound = new OutboundSendTracker();
|
|
2685
|
+
/** Frames this burst may send to inference, after the hold cap (D652). */
|
|
2686
|
+
preRollBudgetFrames = 0;
|
|
2425
2687
|
/** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
|
|
2426
2688
|
preRollFirstMediaWall = null;
|
|
2427
2689
|
preRollLastMediaWall = null;
|
|
@@ -2434,7 +2696,7 @@ var DecodeWorkerChild = class {
|
|
|
2434
2696
|
frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
|
|
2435
2697
|
/**
|
|
2436
2698
|
* Mirrors whether `this.frames` currently has a pull waiting, so
|
|
2437
|
-
* `
|
|
2699
|
+
* `quiesceForExit` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
|
|
2438
2700
|
* intentionally exposes no pending-pull query, only `markPendingPull()`,
|
|
2439
2701
|
* so the child tracks this in lockstep (set in `handlePull`, cleared
|
|
2440
2702
|
* whenever `consumeDecodedFrame`'s `publish` resolves it).
|
|
@@ -2447,6 +2709,10 @@ var DecodeWorkerChild = class {
|
|
|
2447
2709
|
this.sendError("decode-worker-child: malformed request");
|
|
2448
2710
|
return;
|
|
2449
2711
|
}
|
|
2712
|
+
if (this.exiting) {
|
|
2713
|
+
this.refuseWhileExiting(message);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2450
2716
|
switch (message.kind) {
|
|
2451
2717
|
case "start":
|
|
2452
2718
|
this.handleStart(message.source, message.opts, message.lease);
|
|
@@ -2469,13 +2735,73 @@ var DecodeWorkerChild = class {
|
|
|
2469
2735
|
case "markInferred":
|
|
2470
2736
|
this.admission.mark(message.frameId);
|
|
2471
2737
|
return;
|
|
2738
|
+
case "drain":
|
|
2739
|
+
this.handleDrain();
|
|
2740
|
+
return;
|
|
2472
2741
|
case "stop":
|
|
2473
2742
|
this.handleStop();
|
|
2474
2743
|
return;
|
|
2475
2744
|
}
|
|
2476
2745
|
}
|
|
2477
|
-
/**
|
|
2478
|
-
|
|
2746
|
+
/** The answer to a request that arrives after the exit began (D652). */
|
|
2747
|
+
refuseWhileExiting(message) {
|
|
2748
|
+
switch (message.kind) {
|
|
2749
|
+
case "toBuffer":
|
|
2750
|
+
this.send({
|
|
2751
|
+
kind: "error",
|
|
2752
|
+
message: "decode-worker-child: exiting — toBuffer refused",
|
|
2753
|
+
frameId: message.frameId
|
|
2754
|
+
});
|
|
2755
|
+
return;
|
|
2756
|
+
case "nativeCrop":
|
|
2757
|
+
this.send({
|
|
2758
|
+
kind: "nativeCropMiss",
|
|
2759
|
+
requestId: message.requestId,
|
|
2760
|
+
reason: "worker exiting"
|
|
2761
|
+
});
|
|
2762
|
+
return;
|
|
2763
|
+
case "pull":
|
|
2764
|
+
this.send({ kind: "ended" });
|
|
2765
|
+
return;
|
|
2766
|
+
case "stop":
|
|
2767
|
+
this.requestExit(0);
|
|
2768
|
+
return;
|
|
2769
|
+
default: return;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Stop decoding without giving up a single frame the parent holds (D652).
|
|
2774
|
+
*
|
|
2775
|
+
* The loop ends at its next frame and never re-dials, a dial still opening is
|
|
2776
|
+
* aborted, and a later `pull` answers `ended`. The reserved slot, the hold and
|
|
2777
|
+
* the tile/scene stores are left exactly as they are: those are the frames the
|
|
2778
|
+
* runner already admitted, and the whole point is that they stay readable
|
|
2779
|
+
* until the parent has read them and sends `stop`.
|
|
2780
|
+
*/
|
|
2781
|
+
handleDrain() {
|
|
2782
|
+
if (this.stopped) return;
|
|
2783
|
+
this.draining = true;
|
|
2784
|
+
this.stopped = true;
|
|
2785
|
+
this.ended = true;
|
|
2786
|
+
if (this.redialTimer) {
|
|
2787
|
+
clearTimeout(this.redialTimer);
|
|
2788
|
+
this.redialTimer = null;
|
|
2789
|
+
}
|
|
2790
|
+
this.abortController?.abort();
|
|
2791
|
+
this.native.run("undeliveredSlotFrame", () => this.frames.freeUndelivered());
|
|
2792
|
+
this.endPreRollBurst();
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Exit step 1 (`createWorkerExit`): stop everything that runs, WITHOUT a
|
|
2796
|
+
* native call. Before D652 this also freed the frames and disposed the
|
|
2797
|
+
* demuxer, decoder, scalers and GPU context — while the decode loop could
|
|
2798
|
+
* still sit in native I/O, and the process now lives on up to
|
|
2799
|
+
* `WORKER_EXIT_FLUSH_BOUND_MS` after it. Process exit reclaims every native
|
|
2800
|
+
* resource, the GPU pool included, so the exit path disposes nothing.
|
|
2801
|
+
*/
|
|
2802
|
+
quiesceForExit() {
|
|
2803
|
+
this.native.refuseFromNowOn();
|
|
2804
|
+
this.exiting = true;
|
|
2479
2805
|
this.stopped = true;
|
|
2480
2806
|
this.ended = true;
|
|
2481
2807
|
if (this.redialTimer) {
|
|
@@ -2489,21 +2815,19 @@ var DecodeWorkerChild = class {
|
|
|
2489
2815
|
if (this.started) this.logMetrics(true);
|
|
2490
2816
|
this.endPreRollBurst();
|
|
2491
2817
|
this.abortController?.abort();
|
|
2492
|
-
this.frames.free();
|
|
2493
|
-
this.closeInput();
|
|
2494
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
2495
|
-
this.scaler = null;
|
|
2496
|
-
this.scalerKey = "";
|
|
2497
|
-
this.nativeCropScaler?.[Symbol.dispose]?.();
|
|
2498
|
-
this.nativeCropScaler = null;
|
|
2499
|
-
this.nativeCropScalerKey = "";
|
|
2500
|
-
this.hwContext?.[Symbol.dispose]?.();
|
|
2501
|
-
this.hwContext = null;
|
|
2502
2818
|
if (this.pendingPull) {
|
|
2503
2819
|
this.pendingPull = false;
|
|
2504
2820
|
this.send({ kind: "ended" });
|
|
2505
2821
|
}
|
|
2506
2822
|
}
|
|
2823
|
+
/** Exit step 2: the last report lines — `unsettled` included. */
|
|
2824
|
+
flushFinalLines() {
|
|
2825
|
+
const settled = this.preRollSettle.flush();
|
|
2826
|
+
if (settled) this.send({
|
|
2827
|
+
kind: "preRollSettled",
|
|
2828
|
+
...settled
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2507
2831
|
/**
|
|
2508
2832
|
* Emit the compact throughput line (window `deliveredFps`); cheap,
|
|
2509
2833
|
* integer-only.
|
|
@@ -2545,7 +2869,7 @@ var DecodeWorkerChild = class {
|
|
|
2545
2869
|
this.started = true;
|
|
2546
2870
|
this.deviceId = source.deviceId;
|
|
2547
2871
|
this.lease = lease;
|
|
2548
|
-
this.leaseStore = buildLeaseStore(lease);
|
|
2872
|
+
this.leaseStore = buildLeaseStore(lease, (frameId) => this.settlePreRoll(frameId, "evicted"));
|
|
2549
2873
|
this.tileStore = buildTileStore(lease.values.tileBudgetMb);
|
|
2550
2874
|
this.sceneStore = buildTileStore(lease.values.sceneBudgetMb);
|
|
2551
2875
|
this.leaseGate = new LeaseActivityGate(lease.values.activityMs);
|
|
@@ -2581,6 +2905,7 @@ var DecodeWorkerChild = class {
|
|
|
2581
2905
|
* supersede declines it (`LeaseAdmissionLedger.unmark`), and count it.
|
|
2582
2906
|
*/
|
|
2583
2907
|
noteLeaseRelease(frameId, held, by) {
|
|
2908
|
+
this.settlePreRoll(frameId, "released");
|
|
2584
2909
|
if (held) {
|
|
2585
2910
|
this.leaseReleased += 1;
|
|
2586
2911
|
return;
|
|
@@ -2598,6 +2923,7 @@ var DecodeWorkerChild = class {
|
|
|
2598
2923
|
}
|
|
2599
2924
|
async handleToBuffer(frameId, opts) {
|
|
2600
2925
|
const frame = this.leaseStore.get(frameId)?.frame ?? this.frames.toBuffer(frameId);
|
|
2926
|
+
this.settlePreRoll(frameId, frame ? "read" : "miss");
|
|
2601
2927
|
if (!frame) {
|
|
2602
2928
|
this.sendError(describeToBufferMiss({
|
|
2603
2929
|
frameId,
|
|
@@ -3037,13 +3363,25 @@ var DecodeWorkerChild = class {
|
|
|
3037
3363
|
* `docs/design/decode-path.md` → "Lease admission".
|
|
3038
3364
|
*/
|
|
3039
3365
|
captureNativeFrame(frameId, frame) {
|
|
3040
|
-
if (!this.leaseStore.enabled)
|
|
3041
|
-
|
|
3042
|
-
|
|
3366
|
+
if (!this.leaseStore.enabled) {
|
|
3367
|
+
this.settlePreRoll(frameId, "released");
|
|
3368
|
+
return this.nativeRing.retain(frameId, frame);
|
|
3369
|
+
}
|
|
3370
|
+
if (!this.leaseGate.active) {
|
|
3371
|
+
this.settlePreRoll(frameId, "released");
|
|
3372
|
+
return false;
|
|
3373
|
+
}
|
|
3374
|
+
if (!this.admission.admit(frameId)) {
|
|
3375
|
+
this.settlePreRoll(frameId, "released");
|
|
3376
|
+
return false;
|
|
3377
|
+
}
|
|
3043
3378
|
if (frame.isHwFrame()) {
|
|
3044
3379
|
const software = this.downloadToSoftware(frame);
|
|
3045
3380
|
frame.free();
|
|
3046
|
-
if (!software)
|
|
3381
|
+
if (!software) {
|
|
3382
|
+
this.settlePreRoll(frameId, "download-failed");
|
|
3383
|
+
return true;
|
|
3384
|
+
}
|
|
3047
3385
|
this.leaseStore.put(frameId, PINNED_RGB_CROP ? toLeasedRgbNavFrame(software) : toLeasedNavFrame(software));
|
|
3048
3386
|
return true;
|
|
3049
3387
|
}
|
|
@@ -3069,7 +3407,7 @@ var DecodeWorkerChild = class {
|
|
|
3069
3407
|
return first;
|
|
3070
3408
|
} catch (err) {
|
|
3071
3409
|
this.emitStderr(`decode-worker-child: native lease download failed — ${errMessage(err)} [forensics: frame=${frame.width}x${frame.height} fmt=${frame.format} hw=${frame.isHwFrame()} filterUses=${this.leaseDownloadFilterUses} filterAgeMs=${Date.now() - this.leaseDownloadFilterBuiltAt} dialAgeMs=${Date.now() - this.dialStartedAt} decoded=${this.framesDecoded}]\n`);
|
|
3072
|
-
this.nativeLeaseDownloadFilter?.close();
|
|
3410
|
+
this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
|
|
3073
3411
|
this.nativeLeaseDownloadFilter = null;
|
|
3074
3412
|
this.nativeLeaseDownloadFilterKey = "";
|
|
3075
3413
|
this.leaseDownloadFailuresThisDial++;
|
|
@@ -3097,7 +3435,7 @@ var DecodeWorkerChild = class {
|
|
|
3097
3435
|
this.leaseDownloadFilterUses++;
|
|
3098
3436
|
return cached;
|
|
3099
3437
|
}
|
|
3100
|
-
this.nativeLeaseDownloadFilter?.close();
|
|
3438
|
+
this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
|
|
3101
3439
|
this.nativeLeaseDownloadFilter = null;
|
|
3102
3440
|
this.nativeLeaseDownloadFilterKey = "";
|
|
3103
3441
|
const description = buildLeaseDownloadDescription(scaleFilter, color);
|
|
@@ -3206,14 +3544,14 @@ var DecodeWorkerChild = class {
|
|
|
3206
3544
|
const key = nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFormat);
|
|
3207
3545
|
const cached = this.nativeCropScaler;
|
|
3208
3546
|
if (cached && key === this.nativeCropScalerKey) return cached;
|
|
3209
|
-
this.nativeCropScaler?.[Symbol.dispose]?.();
|
|
3547
|
+
this.native.run("nativeCropScaler", () => this.nativeCropScaler?.[Symbol.dispose]?.());
|
|
3210
3548
|
this.nativeCropScaler = null;
|
|
3211
3549
|
this.nativeCropScalerKey = "";
|
|
3212
3550
|
const scaler = new nav.SoftwareScaleContext();
|
|
3213
3551
|
scaler.getContext(srcW, srcH, srcFmt, dstW, dstH, C.AV_PIX_FMT_RGB24, C.SWS_FAST_BILINEAR);
|
|
3214
3552
|
const initRet = scaler.initContext();
|
|
3215
3553
|
if (initRet < 0) {
|
|
3216
|
-
scaler[Symbol.dispose]?.();
|
|
3554
|
+
this.native.run("nativeCropScaler", () => scaler[Symbol.dispose]?.());
|
|
3217
3555
|
throw new Error(`native crop sws_init_context failed: ${initRet}`);
|
|
3218
3556
|
}
|
|
3219
3557
|
this.nativeCropScaler = scaler;
|
|
@@ -3228,7 +3566,7 @@ var DecodeWorkerChild = class {
|
|
|
3228
3566
|
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${sourceColorKey(color)}`;
|
|
3229
3567
|
const cached = this.nativeCropHwFilter;
|
|
3230
3568
|
if (cached && key === this.nativeCropHwFilterKey) return cached;
|
|
3231
|
-
this.nativeCropHwFilter?.close();
|
|
3569
|
+
this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
|
|
3232
3570
|
this.nativeCropHwFilter = null;
|
|
3233
3571
|
this.nativeCropHwFilterKey = "";
|
|
3234
3572
|
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24", color);
|
|
@@ -3241,8 +3579,7 @@ var DecodeWorkerChild = class {
|
|
|
3241
3579
|
return filter;
|
|
3242
3580
|
}
|
|
3243
3581
|
handleStop() {
|
|
3244
|
-
this.
|
|
3245
|
-
process.exit(0);
|
|
3582
|
+
this.requestExit(0);
|
|
3246
3583
|
}
|
|
3247
3584
|
async runDecodeLoop(url) {
|
|
3248
3585
|
const nav = await getNodeAv();
|
|
@@ -3261,7 +3598,15 @@ var DecodeWorkerChild = class {
|
|
|
3261
3598
|
if (this.stopped) break;
|
|
3262
3599
|
this.emitStderr(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
|
|
3263
3600
|
} finally {
|
|
3264
|
-
|
|
3601
|
+
const close = inputCloseAfterLoop({
|
|
3602
|
+
draining: this.draining,
|
|
3603
|
+
exiting: this.exiting
|
|
3604
|
+
});
|
|
3605
|
+
if (close === "all") this.closeInput();
|
|
3606
|
+
else if (close === "demuxer-only") {
|
|
3607
|
+
this.native.run("demuxer", () => this.demuxer?.[Symbol.dispose]?.());
|
|
3608
|
+
this.demuxer = null;
|
|
3609
|
+
}
|
|
3265
3610
|
}
|
|
3266
3611
|
if (this.stopped) break;
|
|
3267
3612
|
const skipBackoff = this.redialAsSoftware;
|
|
@@ -3305,7 +3650,7 @@ var DecodeWorkerChild = class {
|
|
|
3305
3650
|
});
|
|
3306
3651
|
const demuxer = opened.demuxer;
|
|
3307
3652
|
if (this.stopped) {
|
|
3308
|
-
demuxer[Symbol.dispose]?.();
|
|
3653
|
+
this.native.run("demuxer", () => demuxer[Symbol.dispose]?.());
|
|
3309
3654
|
return;
|
|
3310
3655
|
}
|
|
3311
3656
|
this.demuxer = demuxer;
|
|
@@ -3321,13 +3666,19 @@ var DecodeWorkerChild = class {
|
|
|
3321
3666
|
});
|
|
3322
3667
|
const decoderCreateMs = Date.now() - decoderStartedAt;
|
|
3323
3668
|
if (this.stopped) {
|
|
3324
|
-
decoder[Symbol.dispose]?.();
|
|
3669
|
+
this.native.run("decoder", () => decoder[Symbol.dispose]?.());
|
|
3325
3670
|
return;
|
|
3326
3671
|
}
|
|
3327
3672
|
this.decoder = decoder;
|
|
3328
3673
|
const decodeStartedAt = Date.now();
|
|
3329
3674
|
for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
|
|
3330
|
-
if (this.stopped)
|
|
3675
|
+
if (this.stopped) {
|
|
3676
|
+
releaseFrameAfterStop(frame, {
|
|
3677
|
+
draining: this.draining,
|
|
3678
|
+
exiting: this.exiting
|
|
3679
|
+
});
|
|
3680
|
+
break;
|
|
3681
|
+
}
|
|
3331
3682
|
if (!frame) continue;
|
|
3332
3683
|
this.reportDialTiming({
|
|
3333
3684
|
hwContextMs: this.hwContextMs,
|
|
@@ -3457,16 +3808,19 @@ var DecodeWorkerChild = class {
|
|
|
3457
3808
|
});
|
|
3458
3809
|
if (this.preRollMs > 0 && this.preRollMaxFrames > 0) {
|
|
3459
3810
|
const preRollCadenceMs = this.minIntervalMs > 0 ? this.minIntervalMs : 1e3;
|
|
3811
|
+
const withinHold = planPreRollWithinHold({
|
|
3812
|
+
requestedFrames: this.preRollMaxFrames,
|
|
3813
|
+
cadenceMs: preRollCadenceMs,
|
|
3814
|
+
holdFrames: this.lease?.values.holdFrames ?? UNSTARTED_LEASE_KNOBS.values.holdFrames,
|
|
3815
|
+
holdEnabled: this.leaseStore.enabled
|
|
3816
|
+
});
|
|
3817
|
+
this.preRollBudgetFrames = withinHold.budgetFrames;
|
|
3460
3818
|
this.preRollBurst = new PreRollBurst({
|
|
3461
3819
|
budget: {
|
|
3462
|
-
maxFrames:
|
|
3820
|
+
maxFrames: withinHold.budgetFrames,
|
|
3463
3821
|
maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
|
|
3464
3822
|
},
|
|
3465
|
-
plan:
|
|
3466
|
-
spanMs: this.preRollMaxFrames * preRollCadenceMs,
|
|
3467
|
-
detectionFps: 1e3 / preRollCadenceMs,
|
|
3468
|
-
maxFrames: this.preRollMaxFrames
|
|
3469
|
-
}),
|
|
3823
|
+
plan: withinHold.plan,
|
|
3470
3824
|
startedAtWall: now
|
|
3471
3825
|
});
|
|
3472
3826
|
}
|
|
@@ -3527,12 +3881,27 @@ var DecodeWorkerChild = class {
|
|
|
3527
3881
|
droppedByReason: report.droppedByReason,
|
|
3528
3882
|
strideMs: report.strideMs,
|
|
3529
3883
|
subsampled: report.subsampled,
|
|
3530
|
-
spanMs: first !== null && last !== null ? Math.round(last - first) : 0
|
|
3884
|
+
spanMs: first !== null && last !== null ? Math.round(last - first) : 0,
|
|
3885
|
+
budgetFrames: this.preRollBudgetFrames
|
|
3886
|
+
});
|
|
3887
|
+
const settled = this.preRollSettle.endBurst();
|
|
3888
|
+
if (settled) this.send({
|
|
3889
|
+
kind: "preRollSettled",
|
|
3890
|
+
...settled
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
/** One pre-roll frame's fate; sends the settle summary when it was the last owed. */
|
|
3894
|
+
settlePreRoll(frameId, outcome) {
|
|
3895
|
+
const settled = this.preRollSettle.settle(frameId, outcome);
|
|
3896
|
+
if (settled) this.send({
|
|
3897
|
+
kind: "preRollSettled",
|
|
3898
|
+
...settled
|
|
3531
3899
|
});
|
|
3532
3900
|
}
|
|
3533
3901
|
/** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
|
|
3534
3902
|
deliverFrame(reply) {
|
|
3535
3903
|
this.framesDelivered++;
|
|
3904
|
+
if (reply.preRoll) this.preRollSettle.admit(reply.frameId);
|
|
3536
3905
|
this.send(this.toFrameReply(reply));
|
|
3537
3906
|
}
|
|
3538
3907
|
/**
|
|
@@ -3562,7 +3931,7 @@ var DecodeWorkerChild = class {
|
|
|
3562
3931
|
this.emitStderr(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
|
|
3563
3932
|
return false;
|
|
3564
3933
|
} finally {
|
|
3565
|
-
filter?.close();
|
|
3934
|
+
this.native.run("hwFilterProbe", () => filter?.close());
|
|
3566
3935
|
}
|
|
3567
3936
|
}
|
|
3568
3937
|
/**
|
|
@@ -3577,7 +3946,7 @@ var DecodeWorkerChild = class {
|
|
|
3577
3946
|
if (this.hwFilterDecision === "software") return;
|
|
3578
3947
|
this.hwFilterDecision = "software";
|
|
3579
3948
|
this.redialAsSoftware = true;
|
|
3580
|
-
this.hwFilter?.close();
|
|
3949
|
+
this.native.run("hwFilter", () => this.hwFilter?.close());
|
|
3581
3950
|
this.hwFilter = null;
|
|
3582
3951
|
this.hwFilterKey = "";
|
|
3583
3952
|
this.abortController?.abort();
|
|
@@ -3651,14 +4020,14 @@ var DecodeWorkerChild = class {
|
|
|
3651
4020
|
const key = `${srcKind}:${srcW}x${srcH}->${dstW}x${dstH}:${dstFmt}`;
|
|
3652
4021
|
const cached = this.scaler;
|
|
3653
4022
|
if (cached && key === this.scalerKey) return cached;
|
|
3654
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
4023
|
+
this.native.run("scaler", () => this.scaler?.[Symbol.dispose]?.());
|
|
3655
4024
|
this.scaler = null;
|
|
3656
4025
|
this.scalerKey = "";
|
|
3657
4026
|
const scaler = new nav.SoftwareScaleContext();
|
|
3658
4027
|
scaler.getContext(srcW, srcH, navPixelFormatFor(srcKind, C), dstW, dstH, dstFmt, C.SWS_FAST_BILINEAR);
|
|
3659
4028
|
const initRet = scaler.initContext();
|
|
3660
4029
|
if (initRet < 0) {
|
|
3661
|
-
scaler[Symbol.dispose]?.();
|
|
4030
|
+
this.native.run("scaler", () => scaler[Symbol.dispose]?.());
|
|
3662
4031
|
throw new Error(`sws_init_context failed: ${initRet}`);
|
|
3663
4032
|
}
|
|
3664
4033
|
this.scaler = scaler;
|
|
@@ -3716,7 +4085,7 @@ var DecodeWorkerChild = class {
|
|
|
3716
4085
|
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}:${sourceColorKey(color)}`;
|
|
3717
4086
|
const cached = this.hwFilter;
|
|
3718
4087
|
if (cached && key === this.hwFilterKey) return cached;
|
|
3719
|
-
this.hwFilter?.close();
|
|
4088
|
+
this.native.run("hwFilter", () => this.hwFilter?.close());
|
|
3720
4089
|
this.hwFilter = null;
|
|
3721
4090
|
this.hwFilterKey = "";
|
|
3722
4091
|
const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel, color);
|
|
@@ -3771,25 +4140,30 @@ var DecodeWorkerChild = class {
|
|
|
3771
4140
|
* the next dial's first `toBuffer`.
|
|
3772
4141
|
*/
|
|
3773
4142
|
closeInput() {
|
|
3774
|
-
this.nativeRing.clear();
|
|
3775
|
-
this.leaseStore.clear();
|
|
4143
|
+
this.native.run("nativeRing", () => this.nativeRing.clear());
|
|
4144
|
+
this.native.run("leaseStore", () => this.leaseStore.clear());
|
|
4145
|
+
const cleared = this.preRollSettle.settleAll("cleared");
|
|
4146
|
+
if (cleared) this.send({
|
|
4147
|
+
kind: "preRollSettled",
|
|
4148
|
+
...cleared
|
|
4149
|
+
});
|
|
3776
4150
|
this.tileStore.clear();
|
|
3777
4151
|
this.sceneStore.clear();
|
|
3778
4152
|
this.pendingCuts.clear();
|
|
3779
4153
|
this.tileFrameWidth = 0;
|
|
3780
4154
|
this.tileFrameHeight = 0;
|
|
3781
|
-
this.nativeLeaseDownloadFilter?.close();
|
|
4155
|
+
this.native.run("nativeLeaseDownloadFilter", () => this.nativeLeaseDownloadFilter?.close());
|
|
3782
4156
|
this.nativeLeaseDownloadFilter = null;
|
|
3783
4157
|
this.nativeLeaseDownloadFilterKey = "";
|
|
3784
|
-
this.nativeCropHwFilter?.close();
|
|
4158
|
+
this.native.run("nativeCropHwFilter", () => this.nativeCropHwFilter?.close());
|
|
3785
4159
|
this.nativeCropHwFilter = null;
|
|
3786
4160
|
this.nativeCropHwFilterKey = "";
|
|
3787
|
-
this.hwFilter?.close();
|
|
4161
|
+
this.native.run("hwFilter", () => this.hwFilter?.close());
|
|
3788
4162
|
this.hwFilter = null;
|
|
3789
4163
|
this.hwFilterKey = "";
|
|
3790
|
-
this.decoder?.[Symbol.dispose]?.();
|
|
4164
|
+
this.native.run("decoder", () => this.decoder?.[Symbol.dispose]?.());
|
|
3791
4165
|
this.decoder = null;
|
|
3792
|
-
this.demuxer?.[Symbol.dispose]?.();
|
|
4166
|
+
this.native.run("demuxer", () => this.demuxer?.[Symbol.dispose]?.());
|
|
3793
4167
|
this.demuxer = null;
|
|
3794
4168
|
}
|
|
3795
4169
|
/**
|
|
@@ -3822,7 +4196,13 @@ var DecodeWorkerChild = class {
|
|
|
3822
4196
|
}
|
|
3823
4197
|
}
|
|
3824
4198
|
send(reply) {
|
|
3825
|
-
process.send
|
|
4199
|
+
if (!process.send) return;
|
|
4200
|
+
const done = this.outbound.begin();
|
|
4201
|
+
try {
|
|
4202
|
+
process.send(reply, void 0, void 0, () => done());
|
|
4203
|
+
} catch {
|
|
4204
|
+
done();
|
|
4205
|
+
}
|
|
3826
4206
|
}
|
|
3827
4207
|
/**
|
|
3828
4208
|
* The `User-Agent` this worker's RTSP dial carries — its identity inside the
|
|
@@ -3871,7 +4251,20 @@ var DecodeWorkerChild = class {
|
|
|
3871
4251
|
});
|
|
3872
4252
|
}
|
|
3873
4253
|
};
|
|
3874
|
-
|
|
4254
|
+
//#endregion
|
|
4255
|
+
//#region src/session-decode/decode-worker-main.ts
|
|
4256
|
+
/**
|
|
4257
|
+
* Decode-worker — the process ENTRY (its own vite build entry, the path
|
|
4258
|
+
* `fork-decode-worker.ts` forks). The worker itself is `DecodeWorkerChild` in
|
|
4259
|
+
* `decode-worker-child.ts`; this file only instantiates it and wires the process:
|
|
4260
|
+
* IPC messages, the native preload, the sharp warn sink, and the ONE bounded
|
|
4261
|
+
* exit every ending goes through (D652).
|
|
4262
|
+
*
|
|
4263
|
+
* ‼ SANCTIONED RAW IPC ‼ — `process.on('message')` / `process.send` are the
|
|
4264
|
+
* correct mechanism for this private, directly-forked worker only; see the
|
|
4265
|
+
* note at the top of `decode-worker-child.ts`.
|
|
4266
|
+
*/
|
|
4267
|
+
var worker = new DecodeWorkerChild({ requestExit: (code) => exitWorker(code) });
|
|
3875
4268
|
setSharpWarnSink((message) => {
|
|
3876
4269
|
worker.emitStderr(`decode-worker-child: ${message}\n`, "warn");
|
|
3877
4270
|
});
|
|
@@ -3879,17 +4272,35 @@ process.on("message", (message) => {
|
|
|
3879
4272
|
worker.handleMessage(message);
|
|
3880
4273
|
});
|
|
3881
4274
|
worker.warm();
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
4275
|
+
/**
|
|
4276
|
+
* The ONE way this worker exits: quiesce (the disposer starts refusing, so
|
|
4277
|
+
* nothing native is freed under a live read), queue the last lines, wait —
|
|
4278
|
+
* bounded — for them to leave, exit. `stop`, SIGTERM (the parent's `kill()`),
|
|
4279
|
+
* a lost channel and an uncaught error all land here, and a step that throws
|
|
4280
|
+
* still exits (with code 1). Before D652 there was no SIGTERM handler and a
|
|
4281
|
+
* `kill()` ended the process with those lines still queued.
|
|
4282
|
+
*/
|
|
4283
|
+
var exitWorker = createWorkerExit({
|
|
4284
|
+
sends: worker.outbound,
|
|
4285
|
+
steps: {
|
|
4286
|
+
quiesce: () => worker.quiesceForExit(),
|
|
4287
|
+
flushFinalLines: () => worker.flushFinalLines()
|
|
4288
|
+
},
|
|
4289
|
+
exit: (code) => process.exit(code)
|
|
3885
4290
|
});
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4291
|
+
wireWorkerExit({
|
|
4292
|
+
on: (event, listener) => {
|
|
4293
|
+
if (event === "SIGTERM") process.on("SIGTERM", () => listener(void 0));
|
|
4294
|
+
else if (event === "disconnect") process.on("disconnect", () => listener(void 0));
|
|
4295
|
+
else process.on("uncaughtException", (err) => listener(err));
|
|
4296
|
+
},
|
|
4297
|
+
exitWorker,
|
|
4298
|
+
reportUncaught: (err) => {
|
|
4299
|
+
worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
|
|
4300
|
+
}
|
|
3890
4301
|
});
|
|
3891
4302
|
process.on("unhandledRejection", (reason) => {
|
|
3892
4303
|
worker.emitStderr(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
|
|
3893
4304
|
});
|
|
3894
4305
|
//#endregion
|
|
3895
|
-
export {
|
|
4306
|
+
export {};
|