@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
|
@@ -23,7 +23,229 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
25
25
|
const crypto = require("node:crypto");
|
|
26
|
-
const index = require("../index-
|
|
26
|
+
const index = require("../index-p-6GfKOg.js");
|
|
27
|
+
const shmRing = require("@camstack/shm-ring");
|
|
28
|
+
const RING_BUDGET_MB = (() => {
|
|
29
|
+
const raw = Number(process.env["CAMSTACK_SHM_RING_BUDGET_MB"]);
|
|
30
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 128;
|
|
31
|
+
})();
|
|
32
|
+
const RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
|
|
33
|
+
function makeSegmentName(seed, generation) {
|
|
34
|
+
let hash = 5381;
|
|
35
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
36
|
+
hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
|
|
37
|
+
}
|
|
38
|
+
const tag = (hash >>> 0).toString(36);
|
|
39
|
+
return `csf.${tag}.${generation}`;
|
|
40
|
+
}
|
|
41
|
+
class DecoderFrameRingSink {
|
|
42
|
+
seed;
|
|
43
|
+
logger;
|
|
44
|
+
nodeId;
|
|
45
|
+
segment = null;
|
|
46
|
+
writer = null;
|
|
47
|
+
segmentName = null;
|
|
48
|
+
slotByteLength = 0;
|
|
49
|
+
generation = 0;
|
|
50
|
+
destroyed = false;
|
|
51
|
+
/** Frames committed into the ring across this sink's lifetime (all generations). */
|
|
52
|
+
framesWritten = 0;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
const salt = Math.random().toString(36).slice(2, 8);
|
|
55
|
+
this.seed = `${options.seed}.${salt}`;
|
|
56
|
+
this.logger = options.logger;
|
|
57
|
+
this.nodeId = options.nodeId;
|
|
58
|
+
}
|
|
59
|
+
/** Whether a segment has been created (i.e. at least one frame written). */
|
|
60
|
+
get isArmed() {
|
|
61
|
+
return this.writer !== null;
|
|
62
|
+
}
|
|
63
|
+
/** The current segment name, or `null` before the first frame. */
|
|
64
|
+
get currentSegmentName() {
|
|
65
|
+
return this.segmentName;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write one decoded frame into the ring and return its `FrameHandle`.
|
|
69
|
+
*
|
|
70
|
+
* On the first call (or after a geometry change that overflows the current
|
|
71
|
+
* slot) the segment is created / re-created sized for this frame. Returns
|
|
72
|
+
* `null` only when the sink has been destroyed.
|
|
73
|
+
*
|
|
74
|
+
* This is the copy-in convenience form (it copies `pixels` into the slot).
|
|
75
|
+
* The decoder's hot path uses the zero-copy {@link beginFrame} /
|
|
76
|
+
* {@link commitFrame} scatter-write pair instead — the scaler produces its
|
|
77
|
+
* packed output directly into the slot, eliminating the write-side memcpy.
|
|
78
|
+
*/
|
|
79
|
+
writeFrame(pixels, meta) {
|
|
80
|
+
if (this.destroyed) return null;
|
|
81
|
+
if (this.writer === null || shmRing.computeSlotByteLength(meta.width, meta.height, meta.format) > this.slotByteLength) {
|
|
82
|
+
this.recreateSegment(
|
|
83
|
+
shmRing.computeSlotByteLength(meta.width, meta.height, meta.format)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const writer = this.writer;
|
|
87
|
+
if (writer === null) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const handle = writer.writeFrame(pixels, meta);
|
|
91
|
+
this.framesWritten += 1;
|
|
92
|
+
return handle;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reserve a ring slot for a frame of the given geometry — the **zero-copy**
|
|
96
|
+
* scatter-write entry point (Phase 5 / D9 Task 7c).
|
|
97
|
+
*
|
|
98
|
+
* The segment is created / re-created here if this is the first frame or the
|
|
99
|
+
* geometry overflows the current slot capacity, so the slot is correctly
|
|
100
|
+
* sized before the caller fills it. The returned `buffer` is a writable view
|
|
101
|
+
* **directly over the mapped segment** — the node-av scaler scatters its
|
|
102
|
+
* packed output straight into it, with no intermediate copy. The caller MUST
|
|
103
|
+
* call {@link commitFrame} with the returned `slot` once the slot is filled.
|
|
104
|
+
*
|
|
105
|
+
* Returns `null` when the sink is destroyed or the segment cannot be created.
|
|
106
|
+
*/
|
|
107
|
+
beginFrame(width, height, format) {
|
|
108
|
+
if (this.destroyed) return null;
|
|
109
|
+
const requiredSlotBytes = shmRing.computeSlotByteLength(width, height, format);
|
|
110
|
+
if (this.writer === null || requiredSlotBytes > this.slotByteLength) {
|
|
111
|
+
this.recreateSegment(requiredSlotBytes);
|
|
112
|
+
}
|
|
113
|
+
const writer = this.writer;
|
|
114
|
+
if (writer === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const { slot, buffer } = writer.beginFrame();
|
|
118
|
+
return { slot, buffer };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Publish the frame whose slot was reserved by {@link beginFrame} and filled
|
|
122
|
+
* in place by the caller. `slot` MUST be the value from the matching
|
|
123
|
+
* `beginFrame`. Returns the published `FrameHandle`, or `null` if the sink
|
|
124
|
+
* was destroyed (or the segment lost) between begin and commit.
|
|
125
|
+
*/
|
|
126
|
+
commitFrame(slot, meta) {
|
|
127
|
+
if (this.destroyed) return null;
|
|
128
|
+
const writer = this.writer;
|
|
129
|
+
if (writer === null) return null;
|
|
130
|
+
const handle = writer.commitFrame(slot, meta);
|
|
131
|
+
this.framesWritten += 1;
|
|
132
|
+
return handle;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Current shm ring usage — `null` until the first frame arms the segment.
|
|
136
|
+
* Surfaced through `decoder.getShmStats` so a downstream consumer can
|
|
137
|
+
* observe ring pressure (slot depth, byte budget, frames written).
|
|
138
|
+
*/
|
|
139
|
+
getShmStats() {
|
|
140
|
+
if (this.writer === null) return null;
|
|
141
|
+
return {
|
|
142
|
+
slotCount: this.writer.slotCount,
|
|
143
|
+
slotByteLength: this.slotByteLength,
|
|
144
|
+
segmentBytes: shmRing.computeSegmentSize(this.writer.slotCount, this.slotByteLength),
|
|
145
|
+
framesWritten: this.framesWritten
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Abandon a slot reserved by {@link beginFrame} **without publishing it** —
|
|
150
|
+
* the degenerate-path counterpart of {@link commitFrame}.
|
|
151
|
+
*
|
|
152
|
+
* A caller that reserved a slot but then could not produce valid pixels (no
|
|
153
|
+
* decoded source planes, or the scaler threw) MUST call this instead of
|
|
154
|
+
* `commitFrame`: it closes the open seqlock without advancing `writeIndex`,
|
|
155
|
+
* so no reader ever sees the slot's uninitialised bytes as a real frame, and
|
|
156
|
+
* no `FrameHandle` is handed downstream. `slot` MUST be the value from the
|
|
157
|
+
* matching `beginFrame`. A no-op if the sink was destroyed (or the segment
|
|
158
|
+
* lost) between begin and abort.
|
|
159
|
+
*/
|
|
160
|
+
abortFrame(slot) {
|
|
161
|
+
if (this.destroyed) return;
|
|
162
|
+
const writer = this.writer;
|
|
163
|
+
if (writer === null) return;
|
|
164
|
+
writer.abortFrame(slot);
|
|
165
|
+
}
|
|
166
|
+
/** Close + unlink the segment. Idempotent. */
|
|
167
|
+
destroy() {
|
|
168
|
+
if (this.destroyed) return;
|
|
169
|
+
this.destroyed = true;
|
|
170
|
+
this.releaseSegment();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Create a fresh segment sized for at least `slotByteLength` bytes per slot,
|
|
174
|
+
* replacing any prior one. A re-create bumps the generation so the new
|
|
175
|
+
* segment has a distinct name — a consumer holding the old mapping is never
|
|
176
|
+
* silently handed a resized segment.
|
|
177
|
+
*/
|
|
178
|
+
recreateSegment(slotByteLength) {
|
|
179
|
+
this.releaseSegment();
|
|
180
|
+
this.generation += 1;
|
|
181
|
+
const name = makeSegmentName(this.seed, this.generation);
|
|
182
|
+
const slotCount = shmRing.deriveSlotCount(RING_BUDGET_BYTES, slotByteLength);
|
|
183
|
+
if (slotCount === shmRing.MIN_RING_SLOTS && shmRing.MIN_RING_SLOTS * slotByteLength > RING_BUDGET_BYTES) {
|
|
184
|
+
this.logger.warn(
|
|
185
|
+
"decoder shm ring: budget too small for resolution — using MIN slots",
|
|
186
|
+
{ meta: { slotByteLength, budgetMb: RING_BUDGET_MB } }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const totalBytes = shmRing.computeSegmentSize(slotCount, slotByteLength);
|
|
190
|
+
try {
|
|
191
|
+
const segment = shmRing.createSegment(name, totalBytes);
|
|
192
|
+
this.segment = segment;
|
|
193
|
+
this.segmentName = name;
|
|
194
|
+
this.slotByteLength = slotByteLength;
|
|
195
|
+
this.writer = new shmRing.FrameRingWriter(
|
|
196
|
+
segment.buffer,
|
|
197
|
+
name,
|
|
198
|
+
slotCount,
|
|
199
|
+
slotByteLength,
|
|
200
|
+
this.nodeId
|
|
201
|
+
);
|
|
202
|
+
this.logger.info("decoder shm ring: segment created", {
|
|
203
|
+
meta: {
|
|
204
|
+
segment: name,
|
|
205
|
+
slotCount,
|
|
206
|
+
slotByteLength,
|
|
207
|
+
totalBytes,
|
|
208
|
+
generation: this.generation
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
this.segment = null;
|
|
213
|
+
this.writer = null;
|
|
214
|
+
this.segmentName = null;
|
|
215
|
+
this.slotByteLength = 0;
|
|
216
|
+
this.logger.error("decoder shm ring: segment create failed", {
|
|
217
|
+
meta: {
|
|
218
|
+
segment: name,
|
|
219
|
+
slotByteLength,
|
|
220
|
+
error: err instanceof Error ? err.message : String(err)
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Unmap + unlink the current segment, if any. */
|
|
226
|
+
releaseSegment() {
|
|
227
|
+
const segment = this.segment;
|
|
228
|
+
if (segment === null) return;
|
|
229
|
+
this.segment = null;
|
|
230
|
+
this.writer = null;
|
|
231
|
+
const name = this.segmentName;
|
|
232
|
+
this.segmentName = null;
|
|
233
|
+
try {
|
|
234
|
+
segment.close();
|
|
235
|
+
segment.unlink();
|
|
236
|
+
this.logger.info("decoder shm ring: segment released", {
|
|
237
|
+
meta: { segment: name }
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
this.logger.warn("decoder shm ring: segment release failed", {
|
|
241
|
+
meta: {
|
|
242
|
+
segment: name,
|
|
243
|
+
error: err instanceof Error ? err.message : String(err)
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
27
249
|
function backendToHwDeviceConst(backend, consts) {
|
|
28
250
|
switch (backend) {
|
|
29
251
|
case "videotoolbox":
|
|
@@ -115,7 +337,19 @@ class NodeAvDecoderSession {
|
|
|
115
337
|
config;
|
|
116
338
|
logger;
|
|
117
339
|
frameCallbacks = /* @__PURE__ */ new Set();
|
|
340
|
+
handleCallbacks = /* @__PURE__ */ new Set();
|
|
118
341
|
destroyed = false;
|
|
342
|
+
/**
|
|
343
|
+
* Frame delivery mode (see {@link DecoderFrameSink}). `'shm'` lazily
|
|
344
|
+
* constructs `frameRingSink` and routes every decoded frame through it.
|
|
345
|
+
*/
|
|
346
|
+
frameSink;
|
|
347
|
+
/**
|
|
348
|
+
* The shared-memory ring writer for this stream. Created lazily on the
|
|
349
|
+
* first decoded frame in `'shm'` mode (the segment cannot be sized until
|
|
350
|
+
* the output geometry is known) and torn down in `destroy`.
|
|
351
|
+
*/
|
|
352
|
+
frameRingSink = null;
|
|
119
353
|
// Low-level node-av objects (initialized on first keyframe)
|
|
120
354
|
parser = null;
|
|
121
355
|
codecCtx = null;
|
|
@@ -207,6 +441,8 @@ class NodeAvDecoderSession {
|
|
|
207
441
|
startTime = Date.now();
|
|
208
442
|
hwaccelPref;
|
|
209
443
|
hwaccelResolver;
|
|
444
|
+
/** Cluster node id stamped into every `FrameHandle` the `'shm'` sink emits. */
|
|
445
|
+
nodeId;
|
|
210
446
|
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
211
447
|
activeHwAccel = "none";
|
|
212
448
|
hwDevice = null;
|
|
@@ -221,6 +457,39 @@ class NodeAvDecoderSession {
|
|
|
221
457
|
this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
|
|
222
458
|
this.hwaccelPref = options?.hwaccel ?? "auto";
|
|
223
459
|
this.hwaccelResolver = options?.hwaccelResolver ?? null;
|
|
460
|
+
this.frameSink = options?.frameSink ?? "callback";
|
|
461
|
+
this.nodeId = options?.nodeId ?? "local";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The shared-memory ring sink for this stream, or `null` before the first
|
|
465
|
+
* `'shm'`-mode frame lazily creates it. Exposed so the owning addon can
|
|
466
|
+
* surface ring stats via `decoder.getShmStats`.
|
|
467
|
+
*/
|
|
468
|
+
get frameRingSinkOrNull() {
|
|
469
|
+
return this.frameRingSink;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Lazily build the shared-memory ring sink for this stream. The segment
|
|
473
|
+
* itself is still created lazily by the sink on its first `writeFrame`,
|
|
474
|
+
* once the decoded geometry is known.
|
|
475
|
+
*/
|
|
476
|
+
ensureFrameRingSink() {
|
|
477
|
+
if (this.frameRingSink === null) {
|
|
478
|
+
const seedParts = [];
|
|
479
|
+
if (typeof this.config.deviceId === "number") {
|
|
480
|
+
seedParts.push(String(this.config.deviceId));
|
|
481
|
+
}
|
|
482
|
+
if (typeof this.config.tag === "string" && this.config.tag.length > 0) {
|
|
483
|
+
seedParts.push(this.config.tag);
|
|
484
|
+
}
|
|
485
|
+
const seed = seedParts.length > 0 ? seedParts.join(":") : "anon";
|
|
486
|
+
this.frameRingSink = new DecoderFrameRingSink({
|
|
487
|
+
seed,
|
|
488
|
+
logger: this.logger,
|
|
489
|
+
nodeId: this.nodeId
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return this.frameRingSink;
|
|
224
493
|
}
|
|
225
494
|
/**
|
|
226
495
|
* Resolve the backend preference list and try each one against
|
|
@@ -521,6 +790,10 @@ class NodeAvDecoderSession {
|
|
|
521
790
|
}
|
|
522
791
|
if (!this.dstFrame || !this.scaler) return;
|
|
523
792
|
const decodeStart = performance.now();
|
|
793
|
+
if (this.frameSink === "shm" && this.outputMode !== "jpeg") {
|
|
794
|
+
this.scaleIntoRingSlot(frame, decodeStart);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
524
797
|
try {
|
|
525
798
|
this.dstFrame.makeWritable();
|
|
526
799
|
this.scaler.scaleFrameSync(this.dstFrame, frame);
|
|
@@ -566,6 +839,104 @@ class NodeAvDecoderSession {
|
|
|
566
839
|
this.emitRawFrame(rawBuf, "gray", decodeStart);
|
|
567
840
|
}
|
|
568
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Scale a decoded frame **directly into a shared-memory ring slot** — the
|
|
844
|
+
* zero write-side copy path (Phase 5 / D9 Task 7c).
|
|
845
|
+
*
|
|
846
|
+
* `beginFrame` reserves the slot (its seqlock open / odd — readers skip it);
|
|
847
|
+
* `SoftwareScaleContext.scaleSync` (the low-level `sws_scale` mapping) writes
|
|
848
|
+
* the packed pixels straight into the slot buffer with `linesize = width ×
|
|
849
|
+
* bpp` — no linesize padding, so there is nothing to strip and nothing to
|
|
850
|
+
* memcpy; `commitFrame` writes the metadata, closes the seqlock and publishes
|
|
851
|
+
* the slot. The decoded source planes feed `scaleSync` as `srcSlice` +
|
|
852
|
+
* `srcStride` (`frame.data` / `frame.linesize`).
|
|
853
|
+
*
|
|
854
|
+
* Used only for raw output formats (rgb/bgr/gray). The jpeg path keeps the
|
|
855
|
+
* `dstFrame` + sharp encode route because sharp must consume an RGB buffer
|
|
856
|
+
* before the final (variable-length) jpeg bytes exist.
|
|
857
|
+
*/
|
|
858
|
+
scaleIntoRingSlot(frame, decodeStart) {
|
|
859
|
+
if (!this.scaler) return;
|
|
860
|
+
const format = this.outputMode === "gray" ? "gray" : "rgb";
|
|
861
|
+
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
862
|
+
const sink = this.ensureFrameRingSink();
|
|
863
|
+
const reserved = sink.beginFrame(this.outWidth, this.outHeight, format);
|
|
864
|
+
if (reserved === null) {
|
|
865
|
+
this.droppedFrames++;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const srcPlanes = frame.data;
|
|
869
|
+
if (!srcPlanes || srcPlanes.length === 0) {
|
|
870
|
+
sink.abortFrame(reserved.slot);
|
|
871
|
+
this.droppedFrames++;
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const dstStride = this.outWidth * channels;
|
|
875
|
+
try {
|
|
876
|
+
const srcStrides = Array.from(frame.linesize).slice(0, srcPlanes.length);
|
|
877
|
+
this.scaler.scaleSync(
|
|
878
|
+
Array.from(srcPlanes),
|
|
879
|
+
srcStrides,
|
|
880
|
+
0,
|
|
881
|
+
frame.height,
|
|
882
|
+
[reserved.buffer],
|
|
883
|
+
[dstStride]
|
|
884
|
+
);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
this.logger.warn("node-av scale-into-slot error", {
|
|
887
|
+
meta: { error: index.errMsg(err) }
|
|
888
|
+
});
|
|
889
|
+
sink.abortFrame(reserved.slot);
|
|
890
|
+
this.droppedFrames++;
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const decodeMs = performance.now() - decodeStart;
|
|
894
|
+
this.totalDecodeTimeMs += decodeMs;
|
|
895
|
+
this.outputFrames++;
|
|
896
|
+
this.lastEmitTime = performance.now();
|
|
897
|
+
if (!this.firstFrameLogged) {
|
|
898
|
+
this.firstFrameLogged = true;
|
|
899
|
+
this.logger.info("node-av: scaled directly into shm ring slot", {
|
|
900
|
+
meta: {
|
|
901
|
+
phase: "frame-debug",
|
|
902
|
+
width: this.outWidth,
|
|
903
|
+
height: this.outHeight,
|
|
904
|
+
dstStride,
|
|
905
|
+
format
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (this.outputFrames === 1 || this.outputFrames % 500 === 0) {
|
|
910
|
+
this.logger.info("node-av frame emitted", {
|
|
911
|
+
meta: {
|
|
912
|
+
frameNumber: this.outputFrames,
|
|
913
|
+
width: this.outWidth,
|
|
914
|
+
height: this.outHeight,
|
|
915
|
+
format,
|
|
916
|
+
decodeMs,
|
|
917
|
+
sink: "shm",
|
|
918
|
+
subs: this.handleCallbacks.size
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
const handle = sink.commitFrame(reserved.slot, {
|
|
923
|
+
width: this.outWidth,
|
|
924
|
+
height: this.outHeight,
|
|
925
|
+
format,
|
|
926
|
+
// pts uses performance.now() (monotonic, process-relative) for
|
|
927
|
+
// ring-ordering; DecodedFrameHandle.timestamp carries the wall clock.
|
|
928
|
+
pts: performance.now(),
|
|
929
|
+
byteLength: dstStride * this.outHeight
|
|
930
|
+
});
|
|
931
|
+
if (handle === null) {
|
|
932
|
+
this.droppedFrames++;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const delivered = { handle, timestamp: Date.now() };
|
|
936
|
+
for (const cb of this.handleCallbacks) {
|
|
937
|
+
cb(delivered);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
569
940
|
/**
|
|
570
941
|
* Extract packed pixel buffer from a decoded frame.
|
|
571
942
|
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
@@ -628,10 +999,15 @@ class NodeAvDecoderSession {
|
|
|
628
999
|
height: this.outHeight,
|
|
629
1000
|
format,
|
|
630
1001
|
decodeMs,
|
|
631
|
-
|
|
1002
|
+
sink: this.frameSink,
|
|
1003
|
+
subs: this.frameSink === "shm" ? this.handleCallbacks.size : this.frameCallbacks.size
|
|
632
1004
|
}
|
|
633
1005
|
});
|
|
634
1006
|
}
|
|
1007
|
+
if (this.frameSink === "shm") {
|
|
1008
|
+
this.emitShmFrame(data, format);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
635
1011
|
const decodedFrame = {
|
|
636
1012
|
data,
|
|
637
1013
|
width: this.outWidth,
|
|
@@ -643,12 +1019,53 @@ class NodeAvDecoderSession {
|
|
|
643
1019
|
cb(decodedFrame);
|
|
644
1020
|
}
|
|
645
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Write a decoded frame into this stream's shared-memory ring and deliver
|
|
1024
|
+
* the resulting `FrameHandle` to `onFrameHandle` subscribers. No pixel
|
|
1025
|
+
* `Buffer` is handed to callbacks — consumers open the segment and read the
|
|
1026
|
+
* pixels zero-copy via a `FrameRingReader`.
|
|
1027
|
+
*/
|
|
1028
|
+
emitShmFrame(data, format) {
|
|
1029
|
+
const sink = this.ensureFrameRingSink();
|
|
1030
|
+
const handle = sink.writeFrame(data, {
|
|
1031
|
+
width: this.outWidth,
|
|
1032
|
+
height: this.outHeight,
|
|
1033
|
+
format,
|
|
1034
|
+
// The decoder does not carry a source PTS down to this point; use a
|
|
1035
|
+
// monotone-ish wall clock so a consumer can still order frames.
|
|
1036
|
+
// NOTE: pts uses performance.now() (monotonic, process-relative) while
|
|
1037
|
+
// DecodedFrameHandle.timestamp uses Date.now() (wall-clock epoch).
|
|
1038
|
+
// The two clocks are intentionally distinct — pts is for ring-ordering,
|
|
1039
|
+
// timestamp is for wall-clock correlation — and must not be compared.
|
|
1040
|
+
pts: performance.now(),
|
|
1041
|
+
byteLength: data.byteLength
|
|
1042
|
+
});
|
|
1043
|
+
if (handle === null) {
|
|
1044
|
+
this.droppedFrames++;
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const frame = { handle, timestamp: Date.now() };
|
|
1048
|
+
for (const cb of this.handleCallbacks) {
|
|
1049
|
+
cb(frame);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
646
1052
|
onFrame(callback) {
|
|
647
1053
|
this.frameCallbacks.add(callback);
|
|
648
1054
|
return () => {
|
|
649
1055
|
this.frameCallbacks.delete(callback);
|
|
650
1056
|
};
|
|
651
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Subscribe to shared-memory frame handles. Fires only when the session was
|
|
1060
|
+
* created with `frameSink: 'shm'`; in the legacy `'callback'` mode no
|
|
1061
|
+
* handles are produced and this subscription never fires.
|
|
1062
|
+
*/
|
|
1063
|
+
onFrameHandle(callback) {
|
|
1064
|
+
this.handleCallbacks.add(callback);
|
|
1065
|
+
return () => {
|
|
1066
|
+
this.handleCallbacks.delete(callback);
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
652
1069
|
updateConfig(update) {
|
|
653
1070
|
const prevFormat = this.config.outputFormat;
|
|
654
1071
|
this.config = { ...this.config, ...update };
|
|
@@ -683,6 +1100,9 @@ class NodeAvDecoderSession {
|
|
|
683
1100
|
if (this.destroyed) return;
|
|
684
1101
|
this.destroyed = true;
|
|
685
1102
|
this.frameCallbacks.clear();
|
|
1103
|
+
this.handleCallbacks.clear();
|
|
1104
|
+
this.frameRingSink?.destroy();
|
|
1105
|
+
this.frameRingSink = null;
|
|
686
1106
|
this.dstFrame?.[Symbol.dispose]?.();
|
|
687
1107
|
this.avFrame?.[Symbol.dispose]?.();
|
|
688
1108
|
this.avPacket?.[Symbol.dispose]?.();
|
|
@@ -715,10 +1135,28 @@ class NodeAvDecoderSession {
|
|
|
715
1135
|
}
|
|
716
1136
|
const FRAME_BUFFER_CAPACITY = 32;
|
|
717
1137
|
class DecoderNodeAvAddon extends index.BaseAddon {
|
|
1138
|
+
/**
|
|
1139
|
+
* Sessions are stored as the concrete `NodeAvDecoderSession` (the only type
|
|
1140
|
+
* this addon ever creates) so `getShmStats` can reach the per-session shm
|
|
1141
|
+
* ring sink — `IDecoderSession` does not expose it.
|
|
1142
|
+
*/
|
|
718
1143
|
sessions = /* @__PURE__ */ new Map();
|
|
1144
|
+
/** Pixel-frame buffers — populated only for `frameSink: 'callback'` sessions. */
|
|
719
1145
|
frameBuffers = /* @__PURE__ */ new Map();
|
|
1146
|
+
/** `FrameHandle` buffers — populated only for `frameSink: 'shm'` sessions. */
|
|
1147
|
+
handleBuffers = /* @__PURE__ */ new Map();
|
|
720
1148
|
unsubscribers = /* @__PURE__ */ new Map();
|
|
721
1149
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
1150
|
+
/**
|
|
1151
|
+
* Per-shm-segment `FrameRingReader` cache backing `getFrame`. Opens each
|
|
1152
|
+
* named segment once and reuses the reader for every later handle on it;
|
|
1153
|
+
* built in `onInitialize` (it needs the scoped logger) and closed in
|
|
1154
|
+
* `onShutdown`.
|
|
1155
|
+
*/
|
|
1156
|
+
frameReaders = null;
|
|
1157
|
+
/** Running `getFrame` hit/miss counters surfaced via `getShmStats`. */
|
|
1158
|
+
getFrameHits = 0;
|
|
1159
|
+
getFrameMisses = 0;
|
|
722
1160
|
constructor() {
|
|
723
1161
|
super(index.DEFAULT_DECODER_HWACCEL_CONFIG);
|
|
724
1162
|
}
|
|
@@ -755,6 +1193,7 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
755
1193
|
}
|
|
756
1194
|
async onInitialize() {
|
|
757
1195
|
this.ctx.logger.info("node-av decoder addon initialized");
|
|
1196
|
+
this.frameReaders = new shmRing.FrameRingReaderCache(this.ctx.logger);
|
|
758
1197
|
if (!this.config.probedBestHwaccel) {
|
|
759
1198
|
this.reprobeHwaccel().catch((err) => {
|
|
760
1199
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", {
|
|
@@ -813,15 +1252,48 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
813
1252
|
priority: 10
|
|
814
1253
|
};
|
|
815
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* The cluster node id of this decoder — the Moleculer nodeID (`hub`,
|
|
1257
|
+
* `dev-agent-0`, …), not the addon id. Stamped into session-owned
|
|
1258
|
+
* `FrameHandle`s so a downstream consumer routes `getFrame` to the node
|
|
1259
|
+
* that holds the shm ring. A hierarchical broker nodeID (`hub/...`) is
|
|
1260
|
+
* collapsed to the cluster-visible parent; in-process boot is left as-is.
|
|
1261
|
+
* Mirrors the resolution `PipelineRunnerAddon.onInitialize` performs.
|
|
1262
|
+
*/
|
|
1263
|
+
resolveLocalNodeId() {
|
|
1264
|
+
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
1265
|
+
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
1266
|
+
}
|
|
816
1267
|
async createSession(config) {
|
|
817
1268
|
const sessionId = crypto.randomUUID();
|
|
818
1269
|
const hwaccel = this.resolveHwAccelPref();
|
|
1270
|
+
const { frameSink } = config;
|
|
1271
|
+
const nodeId = this.resolveLocalNodeId();
|
|
819
1272
|
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
820
1273
|
hwaccel,
|
|
821
|
-
hwaccelResolver: this.ctx.kernel.hwaccel
|
|
1274
|
+
hwaccelResolver: this.ctx.kernel.hwaccel,
|
|
1275
|
+
frameSink,
|
|
1276
|
+
nodeId
|
|
822
1277
|
});
|
|
1278
|
+
const unsub = frameSink === "shm" ? this.wireShmSink(sessionId, session) : this.wireCallbackSink(sessionId, session);
|
|
1279
|
+
this.sessions.set(sessionId, session);
|
|
1280
|
+
this.unsubscribers.set(sessionId, unsub);
|
|
1281
|
+
this.sessionMeta.set(sessionId, {
|
|
1282
|
+
codec: config.codec,
|
|
1283
|
+
outputFormat: config.outputFormat,
|
|
1284
|
+
createdAtMs: Date.now()
|
|
1285
|
+
});
|
|
1286
|
+
this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel, frameSink, nodeId } });
|
|
1287
|
+
return { sessionId, nodeId };
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Subscribe a `'callback'` session's pixel frames into a per-session
|
|
1291
|
+
* ring buffer drained by `pullFrames`.
|
|
1292
|
+
*/
|
|
1293
|
+
wireCallbackSink(sessionId, session) {
|
|
823
1294
|
const ringBuffer = new index.RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
824
|
-
|
|
1295
|
+
this.frameBuffers.set(sessionId, ringBuffer);
|
|
1296
|
+
return session.onFrame((frame) => {
|
|
825
1297
|
const { format } = frame;
|
|
826
1298
|
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
827
1299
|
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
@@ -836,16 +1308,18 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
836
1308
|
};
|
|
837
1309
|
ringBuffer.push(capFrame);
|
|
838
1310
|
});
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Subscribe a `'shm'` session's `FrameHandle`s into a per-session ring
|
|
1314
|
+
* buffer drained by `pullHandles`. No pixel bytes cross the cap boundary
|
|
1315
|
+
* — the broker opens the named segment and reads the pixels zero-copy.
|
|
1316
|
+
*/
|
|
1317
|
+
wireShmSink(sessionId, session) {
|
|
1318
|
+
const handleRing = new index.RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1319
|
+
this.handleBuffers.set(sessionId, handleRing);
|
|
1320
|
+
return session.onFrameHandle((frame) => {
|
|
1321
|
+
handleRing.push(frame.handle);
|
|
846
1322
|
});
|
|
847
|
-
this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } });
|
|
848
|
-
return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? "local" };
|
|
849
1323
|
}
|
|
850
1324
|
async destroySession(input) {
|
|
851
1325
|
const { sessionId } = input;
|
|
@@ -858,6 +1332,7 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
858
1332
|
await session.destroy();
|
|
859
1333
|
this.sessions.delete(sessionId);
|
|
860
1334
|
this.frameBuffers.delete(sessionId);
|
|
1335
|
+
this.handleBuffers.delete(sessionId);
|
|
861
1336
|
this.unsubscribers.delete(sessionId);
|
|
862
1337
|
this.sessionMeta.delete(sessionId);
|
|
863
1338
|
this.ctx.logger.info("node-av: destroyed session", { meta: { sessionId } });
|
|
@@ -883,17 +1358,24 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
883
1358
|
if (!session) {
|
|
884
1359
|
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
885
1360
|
}
|
|
886
|
-
|
|
887
|
-
await session.openStream(input.url);
|
|
888
|
-
}
|
|
1361
|
+
void input.url;
|
|
889
1362
|
}
|
|
890
1363
|
async pullFrames(input) {
|
|
891
|
-
|
|
892
|
-
if (!ringBuffer) {
|
|
1364
|
+
if (!this.sessions.has(input.sessionId)) {
|
|
893
1365
|
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
894
1366
|
}
|
|
1367
|
+
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
1368
|
+
if (!ringBuffer) return [];
|
|
895
1369
|
return ringBuffer.drain(input.maxCount);
|
|
896
1370
|
}
|
|
1371
|
+
async pullHandles(input) {
|
|
1372
|
+
if (!this.sessions.has(input.sessionId)) {
|
|
1373
|
+
throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
1374
|
+
}
|
|
1375
|
+
const handleRing = this.handleBuffers.get(input.sessionId);
|
|
1376
|
+
if (!handleRing) return [];
|
|
1377
|
+
return handleRing.drain(input.maxCount);
|
|
1378
|
+
}
|
|
897
1379
|
async updateConfig(input) {
|
|
898
1380
|
const session = this.sessions.get(input.sessionId);
|
|
899
1381
|
if (!session) {
|
|
@@ -908,6 +1390,53 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
908
1390
|
}
|
|
909
1391
|
return session.getStats();
|
|
910
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Read back the pixels a `FrameHandle` refers to from this node's shm ring
|
|
1395
|
+
* (Phase 5 / D9 downstream access). Returns `null` when the slot was
|
|
1396
|
+
* already recycled (latest-wins drop) or the segment could not be opened.
|
|
1397
|
+
* The reader cache opens each named segment once and reuses the reader.
|
|
1398
|
+
*
|
|
1399
|
+
* The reader hands back a `Buffer` view over its **reusable scratch
|
|
1400
|
+
* buffer**, only valid until the next `read` on the same reader; the
|
|
1401
|
+
* cap caller may keep the frame across reads, so the pixels are copied
|
|
1402
|
+
* into a fresh detached `ArrayBuffer` here. This matches the existing
|
|
1403
|
+
* callback-path copy in `wireCallbackSink`.
|
|
1404
|
+
*/
|
|
1405
|
+
async getFrame(input) {
|
|
1406
|
+
const frame = this.frameReaders?.read(input.handle) ?? null;
|
|
1407
|
+
if (!frame) {
|
|
1408
|
+
this.getFrameMisses += 1;
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
this.getFrameHits += 1;
|
|
1412
|
+
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
1413
|
+
new Uint8Array(arrayBuf).set(frame.data);
|
|
1414
|
+
return {
|
|
1415
|
+
data: new Uint8Array(arrayBuf),
|
|
1416
|
+
width: frame.width,
|
|
1417
|
+
height: frame.height,
|
|
1418
|
+
format: frame.format,
|
|
1419
|
+
timestamp: frame.timestamp
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* shm ring usage stats for a `frameSink: 'shm'` session — slot geometry,
|
|
1424
|
+
* frames written, byte budget, plus this addon's running `getFrame`
|
|
1425
|
+
* hit/miss counters. Returns `null` for an unknown session or one whose
|
|
1426
|
+
* shm ring has not yet been armed by a first decoded frame.
|
|
1427
|
+
*/
|
|
1428
|
+
async getShmStats(input) {
|
|
1429
|
+
const session = this.sessions.get(input.sessionId);
|
|
1430
|
+
const stats = session?.frameRingSinkOrNull?.getShmStats() ?? null;
|
|
1431
|
+
if (!stats) return null;
|
|
1432
|
+
return {
|
|
1433
|
+
sessionId: input.sessionId,
|
|
1434
|
+
...stats,
|
|
1435
|
+
budgetMb: RING_BUDGET_MB,
|
|
1436
|
+
getFrameHits: this.getFrameHits,
|
|
1437
|
+
getFrameMisses: this.getFrameMisses
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
911
1440
|
async onShutdown() {
|
|
912
1441
|
this.ctx.logger.info("node-av decoder addon shutdown — destroying all sessions");
|
|
913
1442
|
const destroyPromises = [];
|
|
@@ -919,11 +1448,16 @@ class DecoderNodeAvAddon extends index.BaseAddon {
|
|
|
919
1448
|
await Promise.all(destroyPromises);
|
|
920
1449
|
this.sessions.clear();
|
|
921
1450
|
this.frameBuffers.clear();
|
|
1451
|
+
this.handleBuffers.clear();
|
|
922
1452
|
this.unsubscribers.clear();
|
|
923
1453
|
this.sessionMeta.clear();
|
|
1454
|
+
this.frameReaders?.close();
|
|
1455
|
+
this.frameReaders = null;
|
|
924
1456
|
}
|
|
925
1457
|
}
|
|
1458
|
+
exports.DecoderFrameRingSink = DecoderFrameRingSink;
|
|
926
1459
|
exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
|
|
927
1460
|
exports.NodeAvDecoderSession = NodeAvDecoderSession;
|
|
928
1461
|
exports.default = DecoderNodeAvAddon;
|
|
1462
|
+
exports.makeSegmentName = makeSegmentName;
|
|
929
1463
|
//# sourceMappingURL=index.js.map
|