@camstack/addon-pipeline 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -4
- package/dist/audio-analyzer/index.js.map +1 -1
- package/dist/audio-analyzer/index.mjs +2 -4
- package/dist/audio-analyzer/index.mjs.map +1 -1
- package/dist/audio-codec-nodeav/index.js +1 -1
- package/dist/audio-codec-nodeav/index.mjs +1 -1
- package/dist/decoder-nodeav/index.js +552 -18
- package/dist/decoder-nodeav/index.js.map +1 -1
- package/dist/decoder-nodeav/index.mjs +553 -19
- package/dist/decoder-nodeav/index.mjs.map +1 -1
- package/dist/detection-pipeline/index.js +2 -4
- package/dist/detection-pipeline/index.js.map +1 -1
- package/dist/detection-pipeline/index.mjs +2 -4
- package/dist/detection-pipeline/index.mjs.map +1 -1
- package/dist/{index-DKh0uEve.mjs → index-CVzLrojg.mjs} +539 -97
- package/dist/index-CVzLrojg.mjs.map +1 -0
- package/dist/{index-CFPKrb2Y.js → index-p-6GfKOg.js} +539 -97
- package/dist/index-p-6GfKOg.js.map +1 -0
- package/dist/motion-wasm/index.js +2 -4
- package/dist/motion-wasm/index.js.map +1 -1
- package/dist/motion-wasm/index.mjs +2 -4
- package/dist/motion-wasm/index.mjs.map +1 -1
- package/dist/pipeline-runner/index.js +133 -54
- package/dist/pipeline-runner/index.js.map +1 -1
- package/dist/pipeline-runner/index.mjs +133 -54
- package/dist/pipeline-runner/index.mjs.map +1 -1
- package/dist/stream-broker/@mf-types.zip +0 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +19 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-B4l8Nb2y.mjs +20 -0
- package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DePVYdid.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DAssX3h0.mjs} +4 -2
- package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CBlCGyx5.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-DFoJJhpt.mjs} +1 -1
- package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DZchZKbW.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-x7XMEeuJ.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CqeKw-Ig.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWHjxwIc.mjs} +6 -6
- package/dist/stream-broker/{client-BK73l2KT.mjs → client-CZXrddDR.mjs} +2990 -3217
- package/dist/stream-broker/{hostInit-DkjoXTMb.mjs → hostInit-B86vUcFC.mjs} +12 -12
- package/dist/stream-broker/{index-BP0-1QYT.mjs → index-BCEx31Mh.mjs} +3808 -3100
- package/dist/stream-broker/{index-lmXLeXy8.mjs → index-BvV3RVTZ.mjs} +1 -1
- package/dist/stream-broker/{index-IUYKHbxX.mjs → index-C0BzaWmB.mjs} +1 -1
- package/dist/stream-broker/index-CWkKuNLr.mjs +232 -0
- package/dist/stream-broker/{index-ns1fRD30.mjs → index-CZNxa0ad.mjs} +1 -1
- package/dist/stream-broker/index-Kb4xa8FX.mjs +36403 -0
- package/dist/stream-broker/{index-BxHaCH3N.mjs → index-KtR7Pp0O.mjs} +1 -1
- package/dist/stream-broker/{index-Ss9m7Jum.mjs → index-cYW01SNH.mjs} +1 -1
- package/dist/stream-broker/index.js +802 -541
- package/dist/stream-broker/index.js.map +1 -1
- package/dist/stream-broker/index.mjs +802 -519
- package/dist/stream-broker/index.mjs.map +1 -1
- package/dist/stream-broker/{jsx-runtime-ZdY5pIZz.mjs → jsx-runtime-B_evVsXl.mjs} +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +23 -31
- package/dist/index-CFPKrb2Y.js.map +0 -1
- package/dist/index-DKh0uEve.mjs.map +0 -1
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CpCK52pE.mjs +0 -19
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BN3K4dM8.mjs +0 -20
- package/dist/stream-broker/index-DKercbDS.mjs +0 -20855
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
|
@@ -1,5 +1,227 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { e as errMsg, B as BaseAddon, u as DEFAULT_DECODER_HWACCEL_CONFIG, H as HWACCEL_OPTIONS, v as decoderCapability, R as RingBuffer } from "../index-
|
|
2
|
+
import { e as errMsg, B as BaseAddon, u as DEFAULT_DECODER_HWACCEL_CONFIG, H as HWACCEL_OPTIONS, v as decoderCapability, R as RingBuffer } from "../index-CVzLrojg.mjs";
|
|
3
|
+
import { computeSlotByteLength, computeSegmentSize, deriveSlotCount, MIN_RING_SLOTS, createSegment, FrameRingWriter, FrameRingReaderCache } from "@camstack/shm-ring";
|
|
4
|
+
const RING_BUDGET_MB = (() => {
|
|
5
|
+
const raw = Number(process.env["CAMSTACK_SHM_RING_BUDGET_MB"]);
|
|
6
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 128;
|
|
7
|
+
})();
|
|
8
|
+
const RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
|
|
9
|
+
function makeSegmentName(seed, generation) {
|
|
10
|
+
let hash = 5381;
|
|
11
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
12
|
+
hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
|
|
13
|
+
}
|
|
14
|
+
const tag = (hash >>> 0).toString(36);
|
|
15
|
+
return `csf.${tag}.${generation}`;
|
|
16
|
+
}
|
|
17
|
+
class DecoderFrameRingSink {
|
|
18
|
+
seed;
|
|
19
|
+
logger;
|
|
20
|
+
nodeId;
|
|
21
|
+
segment = null;
|
|
22
|
+
writer = null;
|
|
23
|
+
segmentName = null;
|
|
24
|
+
slotByteLength = 0;
|
|
25
|
+
generation = 0;
|
|
26
|
+
destroyed = false;
|
|
27
|
+
/** Frames committed into the ring across this sink's lifetime (all generations). */
|
|
28
|
+
framesWritten = 0;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
const salt = Math.random().toString(36).slice(2, 8);
|
|
31
|
+
this.seed = `${options.seed}.${salt}`;
|
|
32
|
+
this.logger = options.logger;
|
|
33
|
+
this.nodeId = options.nodeId;
|
|
34
|
+
}
|
|
35
|
+
/** Whether a segment has been created (i.e. at least one frame written). */
|
|
36
|
+
get isArmed() {
|
|
37
|
+
return this.writer !== null;
|
|
38
|
+
}
|
|
39
|
+
/** The current segment name, or `null` before the first frame. */
|
|
40
|
+
get currentSegmentName() {
|
|
41
|
+
return this.segmentName;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write one decoded frame into the ring and return its `FrameHandle`.
|
|
45
|
+
*
|
|
46
|
+
* On the first call (or after a geometry change that overflows the current
|
|
47
|
+
* slot) the segment is created / re-created sized for this frame. Returns
|
|
48
|
+
* `null` only when the sink has been destroyed.
|
|
49
|
+
*
|
|
50
|
+
* This is the copy-in convenience form (it copies `pixels` into the slot).
|
|
51
|
+
* The decoder's hot path uses the zero-copy {@link beginFrame} /
|
|
52
|
+
* {@link commitFrame} scatter-write pair instead — the scaler produces its
|
|
53
|
+
* packed output directly into the slot, eliminating the write-side memcpy.
|
|
54
|
+
*/
|
|
55
|
+
writeFrame(pixels, meta) {
|
|
56
|
+
if (this.destroyed) return null;
|
|
57
|
+
if (this.writer === null || computeSlotByteLength(meta.width, meta.height, meta.format) > this.slotByteLength) {
|
|
58
|
+
this.recreateSegment(
|
|
59
|
+
computeSlotByteLength(meta.width, meta.height, meta.format)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const writer = this.writer;
|
|
63
|
+
if (writer === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const handle = writer.writeFrame(pixels, meta);
|
|
67
|
+
this.framesWritten += 1;
|
|
68
|
+
return handle;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reserve a ring slot for a frame of the given geometry — the **zero-copy**
|
|
72
|
+
* scatter-write entry point (Phase 5 / D9 Task 7c).
|
|
73
|
+
*
|
|
74
|
+
* The segment is created / re-created here if this is the first frame or the
|
|
75
|
+
* geometry overflows the current slot capacity, so the slot is correctly
|
|
76
|
+
* sized before the caller fills it. The returned `buffer` is a writable view
|
|
77
|
+
* **directly over the mapped segment** — the node-av scaler scatters its
|
|
78
|
+
* packed output straight into it, with no intermediate copy. The caller MUST
|
|
79
|
+
* call {@link commitFrame} with the returned `slot` once the slot is filled.
|
|
80
|
+
*
|
|
81
|
+
* Returns `null` when the sink is destroyed or the segment cannot be created.
|
|
82
|
+
*/
|
|
83
|
+
beginFrame(width, height, format) {
|
|
84
|
+
if (this.destroyed) return null;
|
|
85
|
+
const requiredSlotBytes = computeSlotByteLength(width, height, format);
|
|
86
|
+
if (this.writer === null || requiredSlotBytes > this.slotByteLength) {
|
|
87
|
+
this.recreateSegment(requiredSlotBytes);
|
|
88
|
+
}
|
|
89
|
+
const writer = this.writer;
|
|
90
|
+
if (writer === null) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const { slot, buffer } = writer.beginFrame();
|
|
94
|
+
return { slot, buffer };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Publish the frame whose slot was reserved by {@link beginFrame} and filled
|
|
98
|
+
* in place by the caller. `slot` MUST be the value from the matching
|
|
99
|
+
* `beginFrame`. Returns the published `FrameHandle`, or `null` if the sink
|
|
100
|
+
* was destroyed (or the segment lost) between begin and commit.
|
|
101
|
+
*/
|
|
102
|
+
commitFrame(slot, meta) {
|
|
103
|
+
if (this.destroyed) return null;
|
|
104
|
+
const writer = this.writer;
|
|
105
|
+
if (writer === null) return null;
|
|
106
|
+
const handle = writer.commitFrame(slot, meta);
|
|
107
|
+
this.framesWritten += 1;
|
|
108
|
+
return handle;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Current shm ring usage — `null` until the first frame arms the segment.
|
|
112
|
+
* Surfaced through `decoder.getShmStats` so a downstream consumer can
|
|
113
|
+
* observe ring pressure (slot depth, byte budget, frames written).
|
|
114
|
+
*/
|
|
115
|
+
getShmStats() {
|
|
116
|
+
if (this.writer === null) return null;
|
|
117
|
+
return {
|
|
118
|
+
slotCount: this.writer.slotCount,
|
|
119
|
+
slotByteLength: this.slotByteLength,
|
|
120
|
+
segmentBytes: computeSegmentSize(this.writer.slotCount, this.slotByteLength),
|
|
121
|
+
framesWritten: this.framesWritten
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Abandon a slot reserved by {@link beginFrame} **without publishing it** —
|
|
126
|
+
* the degenerate-path counterpart of {@link commitFrame}.
|
|
127
|
+
*
|
|
128
|
+
* A caller that reserved a slot but then could not produce valid pixels (no
|
|
129
|
+
* decoded source planes, or the scaler threw) MUST call this instead of
|
|
130
|
+
* `commitFrame`: it closes the open seqlock without advancing `writeIndex`,
|
|
131
|
+
* so no reader ever sees the slot's uninitialised bytes as a real frame, and
|
|
132
|
+
* no `FrameHandle` is handed downstream. `slot` MUST be the value from the
|
|
133
|
+
* matching `beginFrame`. A no-op if the sink was destroyed (or the segment
|
|
134
|
+
* lost) between begin and abort.
|
|
135
|
+
*/
|
|
136
|
+
abortFrame(slot) {
|
|
137
|
+
if (this.destroyed) return;
|
|
138
|
+
const writer = this.writer;
|
|
139
|
+
if (writer === null) return;
|
|
140
|
+
writer.abortFrame(slot);
|
|
141
|
+
}
|
|
142
|
+
/** Close + unlink the segment. Idempotent. */
|
|
143
|
+
destroy() {
|
|
144
|
+
if (this.destroyed) return;
|
|
145
|
+
this.destroyed = true;
|
|
146
|
+
this.releaseSegment();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a fresh segment sized for at least `slotByteLength` bytes per slot,
|
|
150
|
+
* replacing any prior one. A re-create bumps the generation so the new
|
|
151
|
+
* segment has a distinct name — a consumer holding the old mapping is never
|
|
152
|
+
* silently handed a resized segment.
|
|
153
|
+
*/
|
|
154
|
+
recreateSegment(slotByteLength) {
|
|
155
|
+
this.releaseSegment();
|
|
156
|
+
this.generation += 1;
|
|
157
|
+
const name = makeSegmentName(this.seed, this.generation);
|
|
158
|
+
const slotCount = deriveSlotCount(RING_BUDGET_BYTES, slotByteLength);
|
|
159
|
+
if (slotCount === MIN_RING_SLOTS && MIN_RING_SLOTS * slotByteLength > RING_BUDGET_BYTES) {
|
|
160
|
+
this.logger.warn(
|
|
161
|
+
"decoder shm ring: budget too small for resolution — using MIN slots",
|
|
162
|
+
{ meta: { slotByteLength, budgetMb: RING_BUDGET_MB } }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const totalBytes = computeSegmentSize(slotCount, slotByteLength);
|
|
166
|
+
try {
|
|
167
|
+
const segment = createSegment(name, totalBytes);
|
|
168
|
+
this.segment = segment;
|
|
169
|
+
this.segmentName = name;
|
|
170
|
+
this.slotByteLength = slotByteLength;
|
|
171
|
+
this.writer = new FrameRingWriter(
|
|
172
|
+
segment.buffer,
|
|
173
|
+
name,
|
|
174
|
+
slotCount,
|
|
175
|
+
slotByteLength,
|
|
176
|
+
this.nodeId
|
|
177
|
+
);
|
|
178
|
+
this.logger.info("decoder shm ring: segment created", {
|
|
179
|
+
meta: {
|
|
180
|
+
segment: name,
|
|
181
|
+
slotCount,
|
|
182
|
+
slotByteLength,
|
|
183
|
+
totalBytes,
|
|
184
|
+
generation: this.generation
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
this.segment = null;
|
|
189
|
+
this.writer = null;
|
|
190
|
+
this.segmentName = null;
|
|
191
|
+
this.slotByteLength = 0;
|
|
192
|
+
this.logger.error("decoder shm ring: segment create failed", {
|
|
193
|
+
meta: {
|
|
194
|
+
segment: name,
|
|
195
|
+
slotByteLength,
|
|
196
|
+
error: err instanceof Error ? err.message : String(err)
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Unmap + unlink the current segment, if any. */
|
|
202
|
+
releaseSegment() {
|
|
203
|
+
const segment = this.segment;
|
|
204
|
+
if (segment === null) return;
|
|
205
|
+
this.segment = null;
|
|
206
|
+
this.writer = null;
|
|
207
|
+
const name = this.segmentName;
|
|
208
|
+
this.segmentName = null;
|
|
209
|
+
try {
|
|
210
|
+
segment.close();
|
|
211
|
+
segment.unlink();
|
|
212
|
+
this.logger.info("decoder shm ring: segment released", {
|
|
213
|
+
meta: { segment: name }
|
|
214
|
+
});
|
|
215
|
+
} catch (err) {
|
|
216
|
+
this.logger.warn("decoder shm ring: segment release failed", {
|
|
217
|
+
meta: {
|
|
218
|
+
segment: name,
|
|
219
|
+
error: err instanceof Error ? err.message : String(err)
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
3
225
|
function backendToHwDeviceConst(backend, consts) {
|
|
4
226
|
switch (backend) {
|
|
5
227
|
case "videotoolbox":
|
|
@@ -91,7 +313,19 @@ class NodeAvDecoderSession {
|
|
|
91
313
|
config;
|
|
92
314
|
logger;
|
|
93
315
|
frameCallbacks = /* @__PURE__ */ new Set();
|
|
316
|
+
handleCallbacks = /* @__PURE__ */ new Set();
|
|
94
317
|
destroyed = false;
|
|
318
|
+
/**
|
|
319
|
+
* Frame delivery mode (see {@link DecoderFrameSink}). `'shm'` lazily
|
|
320
|
+
* constructs `frameRingSink` and routes every decoded frame through it.
|
|
321
|
+
*/
|
|
322
|
+
frameSink;
|
|
323
|
+
/**
|
|
324
|
+
* The shared-memory ring writer for this stream. Created lazily on the
|
|
325
|
+
* first decoded frame in `'shm'` mode (the segment cannot be sized until
|
|
326
|
+
* the output geometry is known) and torn down in `destroy`.
|
|
327
|
+
*/
|
|
328
|
+
frameRingSink = null;
|
|
95
329
|
// Low-level node-av objects (initialized on first keyframe)
|
|
96
330
|
parser = null;
|
|
97
331
|
codecCtx = null;
|
|
@@ -183,6 +417,8 @@ class NodeAvDecoderSession {
|
|
|
183
417
|
startTime = Date.now();
|
|
184
418
|
hwaccelPref;
|
|
185
419
|
hwaccelResolver;
|
|
420
|
+
/** Cluster node id stamped into every `FrameHandle` the `'shm'` sink emits. */
|
|
421
|
+
nodeId;
|
|
186
422
|
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
187
423
|
activeHwAccel = "none";
|
|
188
424
|
hwDevice = null;
|
|
@@ -197,6 +433,39 @@ class NodeAvDecoderSession {
|
|
|
197
433
|
this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
|
|
198
434
|
this.hwaccelPref = options?.hwaccel ?? "auto";
|
|
199
435
|
this.hwaccelResolver = options?.hwaccelResolver ?? null;
|
|
436
|
+
this.frameSink = options?.frameSink ?? "callback";
|
|
437
|
+
this.nodeId = options?.nodeId ?? "local";
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The shared-memory ring sink for this stream, or `null` before the first
|
|
441
|
+
* `'shm'`-mode frame lazily creates it. Exposed so the owning addon can
|
|
442
|
+
* surface ring stats via `decoder.getShmStats`.
|
|
443
|
+
*/
|
|
444
|
+
get frameRingSinkOrNull() {
|
|
445
|
+
return this.frameRingSink;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Lazily build the shared-memory ring sink for this stream. The segment
|
|
449
|
+
* itself is still created lazily by the sink on its first `writeFrame`,
|
|
450
|
+
* once the decoded geometry is known.
|
|
451
|
+
*/
|
|
452
|
+
ensureFrameRingSink() {
|
|
453
|
+
if (this.frameRingSink === null) {
|
|
454
|
+
const seedParts = [];
|
|
455
|
+
if (typeof this.config.deviceId === "number") {
|
|
456
|
+
seedParts.push(String(this.config.deviceId));
|
|
457
|
+
}
|
|
458
|
+
if (typeof this.config.tag === "string" && this.config.tag.length > 0) {
|
|
459
|
+
seedParts.push(this.config.tag);
|
|
460
|
+
}
|
|
461
|
+
const seed = seedParts.length > 0 ? seedParts.join(":") : "anon";
|
|
462
|
+
this.frameRingSink = new DecoderFrameRingSink({
|
|
463
|
+
seed,
|
|
464
|
+
logger: this.logger,
|
|
465
|
+
nodeId: this.nodeId
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return this.frameRingSink;
|
|
200
469
|
}
|
|
201
470
|
/**
|
|
202
471
|
* Resolve the backend preference list and try each one against
|
|
@@ -497,6 +766,10 @@ class NodeAvDecoderSession {
|
|
|
497
766
|
}
|
|
498
767
|
if (!this.dstFrame || !this.scaler) return;
|
|
499
768
|
const decodeStart = performance.now();
|
|
769
|
+
if (this.frameSink === "shm" && this.outputMode !== "jpeg") {
|
|
770
|
+
this.scaleIntoRingSlot(frame, decodeStart);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
500
773
|
try {
|
|
501
774
|
this.dstFrame.makeWritable();
|
|
502
775
|
this.scaler.scaleFrameSync(this.dstFrame, frame);
|
|
@@ -542,6 +815,104 @@ class NodeAvDecoderSession {
|
|
|
542
815
|
this.emitRawFrame(rawBuf, "gray", decodeStart);
|
|
543
816
|
}
|
|
544
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* Scale a decoded frame **directly into a shared-memory ring slot** — the
|
|
820
|
+
* zero write-side copy path (Phase 5 / D9 Task 7c).
|
|
821
|
+
*
|
|
822
|
+
* `beginFrame` reserves the slot (its seqlock open / odd — readers skip it);
|
|
823
|
+
* `SoftwareScaleContext.scaleSync` (the low-level `sws_scale` mapping) writes
|
|
824
|
+
* the packed pixels straight into the slot buffer with `linesize = width ×
|
|
825
|
+
* bpp` — no linesize padding, so there is nothing to strip and nothing to
|
|
826
|
+
* memcpy; `commitFrame` writes the metadata, closes the seqlock and publishes
|
|
827
|
+
* the slot. The decoded source planes feed `scaleSync` as `srcSlice` +
|
|
828
|
+
* `srcStride` (`frame.data` / `frame.linesize`).
|
|
829
|
+
*
|
|
830
|
+
* Used only for raw output formats (rgb/bgr/gray). The jpeg path keeps the
|
|
831
|
+
* `dstFrame` + sharp encode route because sharp must consume an RGB buffer
|
|
832
|
+
* before the final (variable-length) jpeg bytes exist.
|
|
833
|
+
*/
|
|
834
|
+
scaleIntoRingSlot(frame, decodeStart) {
|
|
835
|
+
if (!this.scaler) return;
|
|
836
|
+
const format = this.outputMode === "gray" ? "gray" : "rgb";
|
|
837
|
+
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
838
|
+
const sink = this.ensureFrameRingSink();
|
|
839
|
+
const reserved = sink.beginFrame(this.outWidth, this.outHeight, format);
|
|
840
|
+
if (reserved === null) {
|
|
841
|
+
this.droppedFrames++;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const srcPlanes = frame.data;
|
|
845
|
+
if (!srcPlanes || srcPlanes.length === 0) {
|
|
846
|
+
sink.abortFrame(reserved.slot);
|
|
847
|
+
this.droppedFrames++;
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const dstStride = this.outWidth * channels;
|
|
851
|
+
try {
|
|
852
|
+
const srcStrides = Array.from(frame.linesize).slice(0, srcPlanes.length);
|
|
853
|
+
this.scaler.scaleSync(
|
|
854
|
+
Array.from(srcPlanes),
|
|
855
|
+
srcStrides,
|
|
856
|
+
0,
|
|
857
|
+
frame.height,
|
|
858
|
+
[reserved.buffer],
|
|
859
|
+
[dstStride]
|
|
860
|
+
);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
this.logger.warn("node-av scale-into-slot error", {
|
|
863
|
+
meta: { error: errMsg(err) }
|
|
864
|
+
});
|
|
865
|
+
sink.abortFrame(reserved.slot);
|
|
866
|
+
this.droppedFrames++;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const decodeMs = performance.now() - decodeStart;
|
|
870
|
+
this.totalDecodeTimeMs += decodeMs;
|
|
871
|
+
this.outputFrames++;
|
|
872
|
+
this.lastEmitTime = performance.now();
|
|
873
|
+
if (!this.firstFrameLogged) {
|
|
874
|
+
this.firstFrameLogged = true;
|
|
875
|
+
this.logger.info("node-av: scaled directly into shm ring slot", {
|
|
876
|
+
meta: {
|
|
877
|
+
phase: "frame-debug",
|
|
878
|
+
width: this.outWidth,
|
|
879
|
+
height: this.outHeight,
|
|
880
|
+
dstStride,
|
|
881
|
+
format
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (this.outputFrames === 1 || this.outputFrames % 500 === 0) {
|
|
886
|
+
this.logger.info("node-av frame emitted", {
|
|
887
|
+
meta: {
|
|
888
|
+
frameNumber: this.outputFrames,
|
|
889
|
+
width: this.outWidth,
|
|
890
|
+
height: this.outHeight,
|
|
891
|
+
format,
|
|
892
|
+
decodeMs,
|
|
893
|
+
sink: "shm",
|
|
894
|
+
subs: this.handleCallbacks.size
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const handle = sink.commitFrame(reserved.slot, {
|
|
899
|
+
width: this.outWidth,
|
|
900
|
+
height: this.outHeight,
|
|
901
|
+
format,
|
|
902
|
+
// pts uses performance.now() (monotonic, process-relative) for
|
|
903
|
+
// ring-ordering; DecodedFrameHandle.timestamp carries the wall clock.
|
|
904
|
+
pts: performance.now(),
|
|
905
|
+
byteLength: dstStride * this.outHeight
|
|
906
|
+
});
|
|
907
|
+
if (handle === null) {
|
|
908
|
+
this.droppedFrames++;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const delivered = { handle, timestamp: Date.now() };
|
|
912
|
+
for (const cb of this.handleCallbacks) {
|
|
913
|
+
cb(delivered);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
545
916
|
/**
|
|
546
917
|
* Extract packed pixel buffer from a decoded frame.
|
|
547
918
|
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
@@ -604,10 +975,15 @@ class NodeAvDecoderSession {
|
|
|
604
975
|
height: this.outHeight,
|
|
605
976
|
format,
|
|
606
977
|
decodeMs,
|
|
607
|
-
|
|
978
|
+
sink: this.frameSink,
|
|
979
|
+
subs: this.frameSink === "shm" ? this.handleCallbacks.size : this.frameCallbacks.size
|
|
608
980
|
}
|
|
609
981
|
});
|
|
610
982
|
}
|
|
983
|
+
if (this.frameSink === "shm") {
|
|
984
|
+
this.emitShmFrame(data, format);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
611
987
|
const decodedFrame = {
|
|
612
988
|
data,
|
|
613
989
|
width: this.outWidth,
|
|
@@ -619,12 +995,53 @@ class NodeAvDecoderSession {
|
|
|
619
995
|
cb(decodedFrame);
|
|
620
996
|
}
|
|
621
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* Write a decoded frame into this stream's shared-memory ring and deliver
|
|
1000
|
+
* the resulting `FrameHandle` to `onFrameHandle` subscribers. No pixel
|
|
1001
|
+
* `Buffer` is handed to callbacks — consumers open the segment and read the
|
|
1002
|
+
* pixels zero-copy via a `FrameRingReader`.
|
|
1003
|
+
*/
|
|
1004
|
+
emitShmFrame(data, format) {
|
|
1005
|
+
const sink = this.ensureFrameRingSink();
|
|
1006
|
+
const handle = sink.writeFrame(data, {
|
|
1007
|
+
width: this.outWidth,
|
|
1008
|
+
height: this.outHeight,
|
|
1009
|
+
format,
|
|
1010
|
+
// The decoder does not carry a source PTS down to this point; use a
|
|
1011
|
+
// monotone-ish wall clock so a consumer can still order frames.
|
|
1012
|
+
// NOTE: pts uses performance.now() (monotonic, process-relative) while
|
|
1013
|
+
// DecodedFrameHandle.timestamp uses Date.now() (wall-clock epoch).
|
|
1014
|
+
// The two clocks are intentionally distinct — pts is for ring-ordering,
|
|
1015
|
+
// timestamp is for wall-clock correlation — and must not be compared.
|
|
1016
|
+
pts: performance.now(),
|
|
1017
|
+
byteLength: data.byteLength
|
|
1018
|
+
});
|
|
1019
|
+
if (handle === null) {
|
|
1020
|
+
this.droppedFrames++;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const frame = { handle, timestamp: Date.now() };
|
|
1024
|
+
for (const cb of this.handleCallbacks) {
|
|
1025
|
+
cb(frame);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
622
1028
|
onFrame(callback) {
|
|
623
1029
|
this.frameCallbacks.add(callback);
|
|
624
1030
|
return () => {
|
|
625
1031
|
this.frameCallbacks.delete(callback);
|
|
626
1032
|
};
|
|
627
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Subscribe to shared-memory frame handles. Fires only when the session was
|
|
1036
|
+
* created with `frameSink: 'shm'`; in the legacy `'callback'` mode no
|
|
1037
|
+
* handles are produced and this subscription never fires.
|
|
1038
|
+
*/
|
|
1039
|
+
onFrameHandle(callback) {
|
|
1040
|
+
this.handleCallbacks.add(callback);
|
|
1041
|
+
return () => {
|
|
1042
|
+
this.handleCallbacks.delete(callback);
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
628
1045
|
updateConfig(update) {
|
|
629
1046
|
const prevFormat = this.config.outputFormat;
|
|
630
1047
|
this.config = { ...this.config, ...update };
|
|
@@ -659,6 +1076,9 @@ class NodeAvDecoderSession {
|
|
|
659
1076
|
if (this.destroyed) return;
|
|
660
1077
|
this.destroyed = true;
|
|
661
1078
|
this.frameCallbacks.clear();
|
|
1079
|
+
this.handleCallbacks.clear();
|
|
1080
|
+
this.frameRingSink?.destroy();
|
|
1081
|
+
this.frameRingSink = null;
|
|
662
1082
|
this.dstFrame?.[Symbol.dispose]?.();
|
|
663
1083
|
this.avFrame?.[Symbol.dispose]?.();
|
|
664
1084
|
this.avPacket?.[Symbol.dispose]?.();
|
|
@@ -691,10 +1111,28 @@ class NodeAvDecoderSession {
|
|
|
691
1111
|
}
|
|
692
1112
|
const FRAME_BUFFER_CAPACITY = 32;
|
|
693
1113
|
class DecoderNodeAvAddon extends BaseAddon {
|
|
1114
|
+
/**
|
|
1115
|
+
* Sessions are stored as the concrete `NodeAvDecoderSession` (the only type
|
|
1116
|
+
* this addon ever creates) so `getShmStats` can reach the per-session shm
|
|
1117
|
+
* ring sink — `IDecoderSession` does not expose it.
|
|
1118
|
+
*/
|
|
694
1119
|
sessions = /* @__PURE__ */ new Map();
|
|
1120
|
+
/** Pixel-frame buffers — populated only for `frameSink: 'callback'` sessions. */
|
|
695
1121
|
frameBuffers = /* @__PURE__ */ new Map();
|
|
1122
|
+
/** `FrameHandle` buffers — populated only for `frameSink: 'shm'` sessions. */
|
|
1123
|
+
handleBuffers = /* @__PURE__ */ new Map();
|
|
696
1124
|
unsubscribers = /* @__PURE__ */ new Map();
|
|
697
1125
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
1126
|
+
/**
|
|
1127
|
+
* Per-shm-segment `FrameRingReader` cache backing `getFrame`. Opens each
|
|
1128
|
+
* named segment once and reuses the reader for every later handle on it;
|
|
1129
|
+
* built in `onInitialize` (it needs the scoped logger) and closed in
|
|
1130
|
+
* `onShutdown`.
|
|
1131
|
+
*/
|
|
1132
|
+
frameReaders = null;
|
|
1133
|
+
/** Running `getFrame` hit/miss counters surfaced via `getShmStats`. */
|
|
1134
|
+
getFrameHits = 0;
|
|
1135
|
+
getFrameMisses = 0;
|
|
698
1136
|
constructor() {
|
|
699
1137
|
super(DEFAULT_DECODER_HWACCEL_CONFIG);
|
|
700
1138
|
}
|
|
@@ -731,6 +1169,7 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
731
1169
|
}
|
|
732
1170
|
async onInitialize() {
|
|
733
1171
|
this.ctx.logger.info("node-av decoder addon initialized");
|
|
1172
|
+
this.frameReaders = new FrameRingReaderCache(this.ctx.logger);
|
|
734
1173
|
if (!this.config.probedBestHwaccel) {
|
|
735
1174
|
this.reprobeHwaccel().catch((err) => {
|
|
736
1175
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", {
|
|
@@ -789,15 +1228,48 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
789
1228
|
priority: 10
|
|
790
1229
|
};
|
|
791
1230
|
}
|
|
1231
|
+
/**
|
|
1232
|
+
* The cluster node id of this decoder — the Moleculer nodeID (`hub`,
|
|
1233
|
+
* `dev-agent-0`, …), not the addon id. Stamped into session-owned
|
|
1234
|
+
* `FrameHandle`s so a downstream consumer routes `getFrame` to the node
|
|
1235
|
+
* that holds the shm ring. A hierarchical broker nodeID (`hub/...`) is
|
|
1236
|
+
* collapsed to the cluster-visible parent; in-process boot is left as-is.
|
|
1237
|
+
* Mirrors the resolution `PipelineRunnerAddon.onInitialize` performs.
|
|
1238
|
+
*/
|
|
1239
|
+
resolveLocalNodeId() {
|
|
1240
|
+
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
1241
|
+
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
1242
|
+
}
|
|
792
1243
|
async createSession(config) {
|
|
793
1244
|
const sessionId = randomUUID();
|
|
794
1245
|
const hwaccel = this.resolveHwAccelPref();
|
|
1246
|
+
const { frameSink } = config;
|
|
1247
|
+
const nodeId = this.resolveLocalNodeId();
|
|
795
1248
|
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
796
1249
|
hwaccel,
|
|
797
|
-
hwaccelResolver: this.ctx.kernel.hwaccel
|
|
1250
|
+
hwaccelResolver: this.ctx.kernel.hwaccel,
|
|
1251
|
+
frameSink,
|
|
1252
|
+
nodeId
|
|
798
1253
|
});
|
|
1254
|
+
const unsub = frameSink === "shm" ? this.wireShmSink(sessionId, session) : this.wireCallbackSink(sessionId, session);
|
|
1255
|
+
this.sessions.set(sessionId, session);
|
|
1256
|
+
this.unsubscribers.set(sessionId, unsub);
|
|
1257
|
+
this.sessionMeta.set(sessionId, {
|
|
1258
|
+
codec: config.codec,
|
|
1259
|
+
outputFormat: config.outputFormat,
|
|
1260
|
+
createdAtMs: Date.now()
|
|
1261
|
+
});
|
|
1262
|
+
this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel, frameSink, nodeId } });
|
|
1263
|
+
return { sessionId, nodeId };
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Subscribe a `'callback'` session's pixel frames into a per-session
|
|
1267
|
+
* ring buffer drained by `pullFrames`.
|
|
1268
|
+
*/
|
|
1269
|
+
wireCallbackSink(sessionId, session) {
|
|
799
1270
|
const ringBuffer = new RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
800
|
-
|
|
1271
|
+
this.frameBuffers.set(sessionId, ringBuffer);
|
|
1272
|
+
return session.onFrame((frame) => {
|
|
801
1273
|
const { format } = frame;
|
|
802
1274
|
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
803
1275
|
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
@@ -812,16 +1284,18 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
812
1284
|
};
|
|
813
1285
|
ringBuffer.push(capFrame);
|
|
814
1286
|
});
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Subscribe a `'shm'` session's `FrameHandle`s into a per-session ring
|
|
1290
|
+
* buffer drained by `pullHandles`. No pixel bytes cross the cap boundary
|
|
1291
|
+
* — the broker opens the named segment and reads the pixels zero-copy.
|
|
1292
|
+
*/
|
|
1293
|
+
wireShmSink(sessionId, session) {
|
|
1294
|
+
const handleRing = new RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1295
|
+
this.handleBuffers.set(sessionId, handleRing);
|
|
1296
|
+
return session.onFrameHandle((frame) => {
|
|
1297
|
+
handleRing.push(frame.handle);
|
|
822
1298
|
});
|
|
823
|
-
this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } });
|
|
824
|
-
return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? "local" };
|
|
825
1299
|
}
|
|
826
1300
|
async destroySession(input) {
|
|
827
1301
|
const { sessionId } = input;
|
|
@@ -834,6 +1308,7 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
834
1308
|
await session.destroy();
|
|
835
1309
|
this.sessions.delete(sessionId);
|
|
836
1310
|
this.frameBuffers.delete(sessionId);
|
|
1311
|
+
this.handleBuffers.delete(sessionId);
|
|
837
1312
|
this.unsubscribers.delete(sessionId);
|
|
838
1313
|
this.sessionMeta.delete(sessionId);
|
|
839
1314
|
this.ctx.logger.info("node-av: destroyed session", { meta: { sessionId } });
|
|
@@ -859,17 +1334,24 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
859
1334
|
if (!session) {
|
|
860
1335
|
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
861
1336
|
}
|
|
862
|
-
|
|
863
|
-
await session.openStream(input.url);
|
|
864
|
-
}
|
|
1337
|
+
void input.url;
|
|
865
1338
|
}
|
|
866
1339
|
async pullFrames(input) {
|
|
867
|
-
|
|
868
|
-
if (!ringBuffer) {
|
|
1340
|
+
if (!this.sessions.has(input.sessionId)) {
|
|
869
1341
|
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
870
1342
|
}
|
|
1343
|
+
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
1344
|
+
if (!ringBuffer) return [];
|
|
871
1345
|
return ringBuffer.drain(input.maxCount);
|
|
872
1346
|
}
|
|
1347
|
+
async pullHandles(input) {
|
|
1348
|
+
if (!this.sessions.has(input.sessionId)) {
|
|
1349
|
+
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
1350
|
+
}
|
|
1351
|
+
const handleRing = this.handleBuffers.get(input.sessionId);
|
|
1352
|
+
if (!handleRing) return [];
|
|
1353
|
+
return handleRing.drain(input.maxCount);
|
|
1354
|
+
}
|
|
873
1355
|
async updateConfig(input) {
|
|
874
1356
|
const session = this.sessions.get(input.sessionId);
|
|
875
1357
|
if (!session) {
|
|
@@ -884,6 +1366,53 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
884
1366
|
}
|
|
885
1367
|
return session.getStats();
|
|
886
1368
|
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Read back the pixels a `FrameHandle` refers to from this node's shm ring
|
|
1371
|
+
* (Phase 5 / D9 downstream access). Returns `null` when the slot was
|
|
1372
|
+
* already recycled (latest-wins drop) or the segment could not be opened.
|
|
1373
|
+
* The reader cache opens each named segment once and reuses the reader.
|
|
1374
|
+
*
|
|
1375
|
+
* The reader hands back a `Buffer` view over its **reusable scratch
|
|
1376
|
+
* buffer**, only valid until the next `read` on the same reader; the
|
|
1377
|
+
* cap caller may keep the frame across reads, so the pixels are copied
|
|
1378
|
+
* into a fresh detached `ArrayBuffer` here. This matches the existing
|
|
1379
|
+
* callback-path copy in `wireCallbackSink`.
|
|
1380
|
+
*/
|
|
1381
|
+
async getFrame(input) {
|
|
1382
|
+
const frame = this.frameReaders?.read(input.handle) ?? null;
|
|
1383
|
+
if (!frame) {
|
|
1384
|
+
this.getFrameMisses += 1;
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
this.getFrameHits += 1;
|
|
1388
|
+
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
1389
|
+
new Uint8Array(arrayBuf).set(frame.data);
|
|
1390
|
+
return {
|
|
1391
|
+
data: new Uint8Array(arrayBuf),
|
|
1392
|
+
width: frame.width,
|
|
1393
|
+
height: frame.height,
|
|
1394
|
+
format: frame.format,
|
|
1395
|
+
timestamp: frame.timestamp
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* shm ring usage stats for a `frameSink: 'shm'` session — slot geometry,
|
|
1400
|
+
* frames written, byte budget, plus this addon's running `getFrame`
|
|
1401
|
+
* hit/miss counters. Returns `null` for an unknown session or one whose
|
|
1402
|
+
* shm ring has not yet been armed by a first decoded frame.
|
|
1403
|
+
*/
|
|
1404
|
+
async getShmStats(input) {
|
|
1405
|
+
const session = this.sessions.get(input.sessionId);
|
|
1406
|
+
const stats = session?.frameRingSinkOrNull?.getShmStats() ?? null;
|
|
1407
|
+
if (!stats) return null;
|
|
1408
|
+
return {
|
|
1409
|
+
sessionId: input.sessionId,
|
|
1410
|
+
...stats,
|
|
1411
|
+
budgetMb: RING_BUDGET_MB,
|
|
1412
|
+
getFrameHits: this.getFrameHits,
|
|
1413
|
+
getFrameMisses: this.getFrameMisses
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
887
1416
|
async onShutdown() {
|
|
888
1417
|
this.ctx.logger.info("node-av decoder addon shutdown — destroying all sessions");
|
|
889
1418
|
const destroyPromises = [];
|
|
@@ -895,13 +1424,18 @@ class DecoderNodeAvAddon extends BaseAddon {
|
|
|
895
1424
|
await Promise.all(destroyPromises);
|
|
896
1425
|
this.sessions.clear();
|
|
897
1426
|
this.frameBuffers.clear();
|
|
1427
|
+
this.handleBuffers.clear();
|
|
898
1428
|
this.unsubscribers.clear();
|
|
899
1429
|
this.sessionMeta.clear();
|
|
1430
|
+
this.frameReaders?.close();
|
|
1431
|
+
this.frameReaders = null;
|
|
900
1432
|
}
|
|
901
1433
|
}
|
|
902
1434
|
export {
|
|
1435
|
+
DecoderFrameRingSink,
|
|
903
1436
|
DecoderNodeAvAddon,
|
|
904
1437
|
NodeAvDecoderSession,
|
|
905
|
-
DecoderNodeAvAddon as default
|
|
1438
|
+
DecoderNodeAvAddon as default,
|
|
1439
|
+
makeSegmentName
|
|
906
1440
|
};
|
|
907
1441
|
//# sourceMappingURL=index.mjs.map
|