@camstack/addon-pipeline 1.1.27 → 1.1.29
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 +7 -6
- package/dist/audio-analyzer/index.mjs +7 -6
- package/dist/audio-codec-ffmpeg/index.js +1 -5
- package/dist/audio-codec-ffmpeg/index.mjs +1 -5
- package/dist/decoder-ffmpeg/index.js +138 -356
- package/dist/decoder-ffmpeg/index.mjs +118 -336
- package/dist/decoder-nodeav/index.js +17 -10
- package/dist/decoder-nodeav/index.mjs +17 -10
- package/dist/detection-pipeline/index.js +341 -379
- package/dist/detection-pipeline/index.mjs +341 -379
- package/dist/{dist-DAIlCdAx.js → dist-Cwc0TUQr.js} +378 -76
- package/dist/{dist-CgEP_0OL.mjs → dist-DjuGmyG9.mjs} +379 -65
- package/dist/ffmpeg-args-C5GPp8Cw.mjs +323 -0
- package/dist/ffmpeg-args-D6h1edXK.js +418 -0
- package/dist/frame-dropper-AjheBGMG.mjs +22 -0
- package/dist/frame-dropper-DKLM6pMz.js +27 -0
- package/dist/{frame-handle-plane-Dq20KtKL.mjs → frame-handle-plane-Bkxz-TTD.mjs} +13 -5
- package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-DQNCTrpC.js} +13 -5
- package/dist/{frame-ring-sink-BbzrqTWj.js → frame-ring-sink-8LLV-cvH.js} +128 -23
- package/dist/{frame-ring-sink-B_NvPTJZ.mjs → frame-ring-sink-ClEWjiRU.mjs} +117 -24
- package/dist/motion-wasm/index.js +55 -3
- package/dist/motion-wasm/index.mjs +55 -3
- package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
- package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
- package/dist/pipeline-runner/index.js +50 -16
- package/dist/pipeline-runner/index.mjs +50 -16
- package/dist/recorder/index.js +36 -40
- package/dist/recorder/index.mjs +36 -40
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.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-BJK0-svt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-B_b3PIZB.mjs} +3 -3
- package/dist/stream-broker/index.js +198 -115
- package/dist/stream-broker/index.mjs +189 -106
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
- package/embed-dist/assets/index-C5UpuPr8.css +2 -0
- package/embed-dist/assets/{index-C-pL8ETk.js → index-CpUy8OIE.js} +10 -10
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/python/inference_pool.py +93 -102
- package/python/test_inference_pool_device_selection.py +43 -38
- package/dist/frame-dropper-7RTo_YyG.js +0 -68
- package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +0 -26
- package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
|
@@ -1,302 +1,10 @@
|
|
|
1
|
-
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS,
|
|
2
|
-
import {
|
|
3
|
-
import { r as
|
|
1
|
+
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
|
|
2
|
+
import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-ClEWjiRU.mjs";
|
|
3
|
+
import { a as codecToInputFormat, c as maxDecodeWidth, d as pickDecodeHwAccel, f as rankDecodeHwAccels, i as channelsForPixel, l as parseAvailableGpuScaleFilters, m as resolveGpuScaleFilter, n as DEFAULT_VAAPI_RENDER_DEVICE, o as computeOutputGeometry, p as rawPixelForFormat, r as buildFfmpegDecodeArgs, s as isSoftwareHwAccel, t as DECODE_HWACCEL_RANK, u as parseFfmpegOutputDims } from "../ffmpeg-args-C5GPp8Cw.mjs";
|
|
4
|
+
import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
|
|
4
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
|
-
//#region src/decoder-ffmpeg/ffmpeg-args.ts
|
|
8
|
-
/**
|
|
9
|
-
* Pure, side-effect-free ffmpeg argument construction for the
|
|
10
|
-
* `decoder-ffmpeg` addon.
|
|
11
|
-
*
|
|
12
|
-
* The addon decodes video in an **ffmpeg subprocess** (unlike `decoder-nodeav`
|
|
13
|
-
* which decodes in-process via node-av's native bindings). On the Intel hub,
|
|
14
|
-
* node-av's VA-API decode path SIGBUSes (uncatchable) on 8MP h264 and takes the
|
|
15
|
-
* whole decoder runner down; ffmpeg-as-subprocess decodes the same hardware
|
|
16
|
-
* cleanly because a crash is isolated to the child process.
|
|
17
|
-
*
|
|
18
|
-
* These helpers are extracted from the session so the argument shape is
|
|
19
|
-
* unit-testable without spawning a real ffmpeg binary.
|
|
20
|
-
*
|
|
21
|
-
* Validated working command on the Intel hub (renderD128, 8MP h264, ~50fps,
|
|
22
|
-
* exit 0):
|
|
23
|
-
*
|
|
24
|
-
* ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \
|
|
25
|
-
* -hwaccel_output_format vaapi -f h264 -i <in> \
|
|
26
|
-
* -vf hwdownload,format=nv12,scale=640:360,format=gray \
|
|
27
|
-
* -f rawvideo -y /dev/null
|
|
28
|
-
*/
|
|
29
|
-
/** Default DRM render node used for the VA-API / QSV hardware decode path. */
|
|
30
|
-
var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
|
|
31
|
-
/** Whether a resolved hwaccel value means "decode in software" (no `-hwaccel`). */
|
|
32
|
-
function isSoftwareHwAccel(hwaccel) {
|
|
33
|
-
return hwaccel === null || hwaccel === "" || hwaccel === "none";
|
|
34
|
-
}
|
|
35
|
-
/** Backends whose decoded frames stay on the GPU and need an explicit
|
|
36
|
-
* `-hwaccel_output_format <m>` + `hwdownload,format=nv12` chain. */
|
|
37
|
-
function needsHwDownload(hwaccel) {
|
|
38
|
-
return hwaccel === "vaapi" || hwaccel === "qsv";
|
|
39
|
-
}
|
|
40
|
-
/** Decode backend → its GPU scaler filter. */
|
|
41
|
-
var GPU_SCALE_FILTER_BY_BACKEND = {
|
|
42
|
-
vaapi: "scale_vaapi",
|
|
43
|
-
qsv: "scale_qsv",
|
|
44
|
-
videotoolbox: "scale_vt"
|
|
45
|
-
};
|
|
46
|
-
/** Every GPU scaler this addon knows how to drive — used to scan `-filters`. */
|
|
47
|
-
var KNOWN_GPU_SCALE_FILTERS = [
|
|
48
|
-
"scale_vaapi",
|
|
49
|
-
"scale_qsv",
|
|
50
|
-
"scale_vt"
|
|
51
|
-
];
|
|
52
|
-
/**
|
|
53
|
-
* The GPU scaler that pairs with a decode backend, ignoring build support — a
|
|
54
|
-
* static hwaccel → filter map. Returns null for software / a backend with no
|
|
55
|
-
* GPU scaler (e.g. `cuda`, `drm`). Case-insensitive.
|
|
56
|
-
*/
|
|
57
|
-
function gpuScaleFilterForBackend(hwaccel) {
|
|
58
|
-
if (hwaccel === null) return null;
|
|
59
|
-
return GPU_SCALE_FILTER_BY_BACKEND[hwaccel.toLowerCase()] ?? null;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the GPU scaler to use for a decode backend GIVEN the filters the
|
|
63
|
-
* configured ffmpeg build actually offers ({@link parseAvailableGpuScaleFilters}).
|
|
64
|
-
* Returns the filter when the backend maps to one AND the build has it; null
|
|
65
|
-
* otherwise — the caller then emits the CPU-scale chain (graceful fallback).
|
|
66
|
-
*/
|
|
67
|
-
function resolveGpuScaleFilter(hwaccel, availableFilters) {
|
|
68
|
-
const filter = gpuScaleFilterForBackend(hwaccel);
|
|
69
|
-
if (filter === null) return null;
|
|
70
|
-
return availableFilters.has(filter) ? filter : null;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Parse `ffmpeg -filters` stdout into the set of {@link KNOWN_GPU_SCALE_FILTERS}
|
|
74
|
-
* the build offers. Matches whole filter tokens (word-boundary) so the plain
|
|
75
|
-
* `scale` filter is never mistaken for a GPU scaler. PURE — the spawn lives in
|
|
76
|
-
* the addon; only the parse is here so it is unit-tested.
|
|
77
|
-
*/
|
|
78
|
-
function parseAvailableGpuScaleFilters(filtersStdout) {
|
|
79
|
-
const present = /* @__PURE__ */ new Set();
|
|
80
|
-
for (const filter of KNOWN_GPU_SCALE_FILTERS) if (new RegExp(`\\b${filter}\\b`).test(filtersStdout)) present.add(filter);
|
|
81
|
-
return present;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Whether the decode keeps frames on the GPU — needs `-hwaccel_output_format`
|
|
85
|
-
* and an `hwdownload` in the filtergraph. VA-API / QSV always do (their decoded
|
|
86
|
-
* surfaces live on the GPU); another backend only when a GPU scaler will run on
|
|
87
|
-
* those surfaces (e.g. `videotoolbox` + `scale_vt`).
|
|
88
|
-
*/
|
|
89
|
-
function keepFramesOnGpu(hwaccel, gpuScaleFilter) {
|
|
90
|
-
return needsHwDownload(hwaccel) || gpuScaleFilter !== null;
|
|
91
|
-
}
|
|
92
|
-
/** Map a cap codec string to the ffmpeg `-f` input demuxer/format. */
|
|
93
|
-
function codecToInputFormat(codec) {
|
|
94
|
-
switch (codec.toLowerCase()) {
|
|
95
|
-
case "h265":
|
|
96
|
-
case "hevc": return "hevc";
|
|
97
|
-
default: return "h264";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/** Map the cap-level output format to a raw ffmpeg pixel format. */
|
|
101
|
-
function rawPixelForFormat(outputFormat) {
|
|
102
|
-
return outputFormat === "gray" ? "gray" : "rgb24";
|
|
103
|
-
}
|
|
104
|
-
/** Packed bytes-per-pixel for a raw output format (rgb24 = 3, gray = 1). */
|
|
105
|
-
function channelsForPixel(pixel) {
|
|
106
|
-
return pixel === "gray" ? 1 : 3;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* The maximum output width for a given `scale` divisor — matches the node-av
|
|
110
|
-
* scaler's `maxW = floor(640 / scale)` rule so both decoders produce
|
|
111
|
-
* comparably-sized frames for the motion / detection pipeline.
|
|
112
|
-
*/
|
|
113
|
-
function maxDecodeWidth(scale) {
|
|
114
|
-
return Math.max(2, Math.floor(640 / (scale > 1 ? scale : 1)));
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Compute exact output geometry from a known source resolution — the node-av
|
|
118
|
-
* rule (`outWidth = min(srcW, floor(640/scale))`, `outHeight =
|
|
119
|
-
* round(outWidth*srcH/srcW)`). Provided for callers that DO know the source
|
|
120
|
-
* dimensions; the live broker path leaves them to ffmpeg (`-2` height).
|
|
121
|
-
*/
|
|
122
|
-
function computeOutputGeometry(srcWidth, srcHeight, scale) {
|
|
123
|
-
const width = Math.min(srcWidth, maxDecodeWidth(scale));
|
|
124
|
-
return {
|
|
125
|
-
width,
|
|
126
|
-
height: Math.round(width * srcHeight / srcWidth)
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
/** Build the `-vf` filtergraph string for the requested hwaccel + pixel format. */
|
|
130
|
-
function buildVideoFilter(hwaccel, pixel, outWidth, outHeight, maxW, gpuScaleFilter) {
|
|
131
|
-
const exact = outWidth !== void 0 && outWidth > 0 && outHeight !== void 0 && outHeight > 0;
|
|
132
|
-
if (gpuScaleFilter !== null) return `${gpuScaleFilter}=${exact ? `w=${outWidth}:h=${outHeight}` : `w=min(iw\\,${maxW}):h=-2`},hwdownload,format=nv12,format=${pixel}`;
|
|
133
|
-
const scaleAndFormat = `scale=${exact ? `${outWidth}:${outHeight}` : `min(iw\\,${maxW}):-2`},format=${pixel}`;
|
|
134
|
-
return needsHwDownload(hwaccel) ? `hwdownload,format=nv12,${scaleAndFormat}` : scaleAndFormat;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Build the full ffmpeg argv for a decode session. PURE — no spawn, no env
|
|
138
|
-
* access — so the argument shape is asserted in unit tests.
|
|
139
|
-
*
|
|
140
|
-
* Notes:
|
|
141
|
-
* - `-loglevel info -nostats`: the session parses the muxer's `Output #0`
|
|
142
|
-
* stream line from stderr to learn the exact output W×H (the broker does not
|
|
143
|
-
* pass source geometry for the shm path). `-nostats` suppresses the periodic
|
|
144
|
-
* `frame= …` counter while keeping the one-shot Output block.
|
|
145
|
-
* - The input is `pipe:0` (Annex-B packets written to stdin) in push mode, or
|
|
146
|
-
* `options.inputUrl` (the owner's restream, dialed by ffmpeg itself —
|
|
147
|
-
* `-rtsp_transport tcp` for rtsp URLs) in pull mode; the output is raw video
|
|
148
|
-
* on `pipe:1` either way.
|
|
149
|
-
*/
|
|
150
|
-
function buildFfmpegDecodeArgs(config, options) {
|
|
151
|
-
const inputFormat = codecToInputFormat(config.codec);
|
|
152
|
-
const pixel = rawPixelForFormat(config.outputFormat);
|
|
153
|
-
const maxW = maxDecodeWidth(config.scale);
|
|
154
|
-
const { hwaccel } = options;
|
|
155
|
-
const gpuScaleFilter = options.gpuScaleFilter ?? null;
|
|
156
|
-
const vf = buildVideoFilter(hwaccel, pixel, options.outWidth, options.outHeight, maxW, gpuScaleFilter);
|
|
157
|
-
let hwaccelArgs;
|
|
158
|
-
if (isSoftwareHwAccel(hwaccel) || hwaccel === null) hwaccelArgs = [];
|
|
159
|
-
else {
|
|
160
|
-
hwaccelArgs = ["-hwaccel", hwaccel];
|
|
161
|
-
if (needsHwDownload(hwaccel)) hwaccelArgs.push("-hwaccel_device", options.renderDevice ?? "/dev/dri/renderD128");
|
|
162
|
-
if (keepFramesOnGpu(hwaccel, gpuScaleFilter)) hwaccelArgs.push("-hwaccel_output_format", hwaccel);
|
|
163
|
-
}
|
|
164
|
-
const inputArgs = options.inputUrl !== void 0 ? [
|
|
165
|
-
...options.inputUrl.startsWith("rtsp") ? ["-rtsp_transport", "tcp"] : [],
|
|
166
|
-
"-i",
|
|
167
|
-
options.inputUrl
|
|
168
|
-
] : [
|
|
169
|
-
"-f",
|
|
170
|
-
inputFormat,
|
|
171
|
-
"-i",
|
|
172
|
-
"pipe:0"
|
|
173
|
-
];
|
|
174
|
-
return [
|
|
175
|
-
...logBannerArgs("info"),
|
|
176
|
-
"-nostats",
|
|
177
|
-
"-fflags",
|
|
178
|
-
"+nobuffer+flush_packets",
|
|
179
|
-
"-flags",
|
|
180
|
-
"low_delay",
|
|
181
|
-
"-probesize",
|
|
182
|
-
"1M",
|
|
183
|
-
"-analyzeduration",
|
|
184
|
-
"0",
|
|
185
|
-
...hwaccelArgs,
|
|
186
|
-
...inputArgs,
|
|
187
|
-
"-vf",
|
|
188
|
-
vf,
|
|
189
|
-
"-f",
|
|
190
|
-
"rawvideo",
|
|
191
|
-
"-pix_fmt",
|
|
192
|
-
pixel,
|
|
193
|
-
"pipe:1"
|
|
194
|
-
];
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Parse the exact output geometry from ffmpeg's stderr. The muxer prints an
|
|
198
|
-
* `Output #0, rawvideo …` block whose `Stream #0:0: Video: rawvideo …, <fmt>,
|
|
199
|
-
* WIDTHxHEIGHT …` line carries the scaled dimensions. Gating on the `Output #`
|
|
200
|
-
* marker avoids picking up the INPUT stream's (source) resolution, which is
|
|
201
|
-
* printed earlier.
|
|
202
|
-
*
|
|
203
|
-
* Returns `null` until the Output block with a `WxH` token has been seen.
|
|
204
|
-
*/
|
|
205
|
-
function parseFfmpegOutputDims(stderr) {
|
|
206
|
-
const outputIdx = stderr.indexOf("Output #");
|
|
207
|
-
if (outputIdx < 0) return null;
|
|
208
|
-
const match = stderr.slice(outputIdx).match(/(\d{2,5})x(\d{2,5})/);
|
|
209
|
-
if (!match) return null;
|
|
210
|
-
const width = Number(match[1]);
|
|
211
|
-
const height = Number(match[2]);
|
|
212
|
-
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) return null;
|
|
213
|
-
return {
|
|
214
|
-
width,
|
|
215
|
-
height
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Pick the first kernel-preferred `-hwaccel` method the configured ffmpeg build
|
|
220
|
-
* actually supports, or `null` (software) when it supports none. Mirrors the
|
|
221
|
-
* broker's `pickDecodeHwAccel` (`ffmpeg-invocation.ts`) so the decoder gates
|
|
222
|
-
* decode-hwaccel against the same probed evidence:
|
|
223
|
-
* - empty `preferred` → `null` (software);
|
|
224
|
-
* - empty `supportedMethods` (probe miss) → keep the top preference and let the
|
|
225
|
-
* session's per-child software fallback cover a genuine failure;
|
|
226
|
-
* - otherwise the first preference the build supports, or `null`.
|
|
227
|
-
*/
|
|
228
|
-
function pickDecodeHwAccel(preferred, supportedMethods) {
|
|
229
|
-
if (preferred.length === 0) return null;
|
|
230
|
-
if (supportedMethods.length === 0) return preferred[0] ?? null;
|
|
231
|
-
const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
|
|
232
|
-
return preferred.find((name) => supported.has(name.toLowerCase())) ?? null;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Canonical decode-hwaccel preference order for the ffmpeg-subprocess decoder.
|
|
236
|
-
*
|
|
237
|
-
* `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
|
|
238
|
-
* child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
|
|
239
|
-
* h264 cleanly at ~50fps. The kernel probe (`resolveHwAccel`) tends to surface
|
|
240
|
-
* `qsv` first on Intel, so we re-rank its result through this table before
|
|
241
|
-
* building the attempt chain. `videotoolbox` leads for macOS; `cuda` / `nvdec`
|
|
242
|
-
* trail for NVIDIA. A method not listed here keeps its incoming relative order,
|
|
243
|
-
* placed AFTER every ranked one.
|
|
244
|
-
*/
|
|
245
|
-
var DECODE_HWACCEL_RANK = [
|
|
246
|
-
"videotoolbox",
|
|
247
|
-
"vaapi",
|
|
248
|
-
"qsv",
|
|
249
|
-
"cuda",
|
|
250
|
-
"nvdec",
|
|
251
|
-
"d3d11va",
|
|
252
|
-
"dxva2",
|
|
253
|
-
"amf",
|
|
254
|
-
"vdpau",
|
|
255
|
-
"drm"
|
|
256
|
-
];
|
|
257
|
-
/** Rank index for {@link DECODE_HWACCEL_RANK} — unranked methods sort last. */
|
|
258
|
-
function decodeHwAccelRankIndex(method) {
|
|
259
|
-
const idx = DECODE_HWACCEL_RANK.indexOf(method.toLowerCase());
|
|
260
|
-
return idx < 0 ? DECODE_HWACCEL_RANK.length : idx;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Build the ORDERED list of hardware `-hwaccel` methods to attempt for an
|
|
264
|
-
* `'auto'` decode session, best-first. The session walks this chain on an
|
|
265
|
-
* early-exit-with-no-frames and only falls to software once it is exhausted.
|
|
266
|
-
*
|
|
267
|
-
* Strategy:
|
|
268
|
-
* - Seed candidates from the kernel-preferred order UNION the canonical rank
|
|
269
|
-
* UNION the build's supported list, so a method the kernel omitted but the
|
|
270
|
-
* build supports (the Intel `vaapi` case — kernel surfaces only `qsv`) is
|
|
271
|
-
* still attempted.
|
|
272
|
-
* - When the build's supported-method list is known (non-empty), keep only
|
|
273
|
-
* methods it actually offers; on a probe miss (empty) keep just the kernel
|
|
274
|
-
* preferences (avoid blindly spawning every backend).
|
|
275
|
-
* - Re-rank the survivors by {@link DECODE_HWACCEL_RANK} so `vaapi` precedes
|
|
276
|
-
* `qsv`. A supported method not in the rank table keeps its position AFTER
|
|
277
|
-
* every ranked one (stable sort). Returns lowercased method names; empty ⇒
|
|
278
|
-
* pure software.
|
|
279
|
-
*/
|
|
280
|
-
function rankDecodeHwAccels(preferred, supportedMethods) {
|
|
281
|
-
const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
|
|
282
|
-
const hasSupportList = supported.size > 0;
|
|
283
|
-
const pool = hasSupportList ? [
|
|
284
|
-
...preferred,
|
|
285
|
-
...DECODE_HWACCEL_RANK,
|
|
286
|
-
...supportedMethods
|
|
287
|
-
] : [...preferred];
|
|
288
|
-
const seen = /* @__PURE__ */ new Set();
|
|
289
|
-
const candidates = [];
|
|
290
|
-
for (const raw of pool) {
|
|
291
|
-
const method = raw.toLowerCase();
|
|
292
|
-
if (seen.has(method)) continue;
|
|
293
|
-
seen.add(method);
|
|
294
|
-
if (hasSupportList && !supported.has(method)) continue;
|
|
295
|
-
candidates.push(method);
|
|
296
|
-
}
|
|
297
|
-
return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
|
|
298
|
-
}
|
|
299
|
-
//#endregion
|
|
300
8
|
//#region src/decoder-ffmpeg/frame-stream-splitter.ts
|
|
301
9
|
var FrameStreamSplitter = class {
|
|
302
10
|
frameSize;
|
|
@@ -892,6 +600,19 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
|
|
|
892
600
|
//#endregion
|
|
893
601
|
//#region src/decoder-ffmpeg/addon/index.ts
|
|
894
602
|
var FRAME_BUFFER_CAPACITY = 32;
|
|
603
|
+
var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
|
|
604
|
+
...DEFAULT_DECODER_HWACCEL_CONFIG,
|
|
605
|
+
backend: DEFAULT_DECODER_BACKEND,
|
|
606
|
+
binaryPath: ""
|
|
607
|
+
};
|
|
608
|
+
/** The two selectable decoder backends, in default-first order. */
|
|
609
|
+
var DECODER_BACKEND_OPTIONS = [{
|
|
610
|
+
value: "ffmpeg",
|
|
611
|
+
label: "ffmpeg (subprocess — crash-isolated, default)"
|
|
612
|
+
}, {
|
|
613
|
+
value: "nodeav",
|
|
614
|
+
label: "node-av (in-process FFmpeg bindings)"
|
|
615
|
+
}];
|
|
895
616
|
var DecoderFfmpegAddon = class extends BaseAddon {
|
|
896
617
|
sessions = /* @__PURE__ */ new Map();
|
|
897
618
|
frameBuffers = /* @__PURE__ */ new Map();
|
|
@@ -922,38 +643,66 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
922
643
|
*/
|
|
923
644
|
unsupportedGpuScaleFilters = /* @__PURE__ */ new Set();
|
|
924
645
|
constructor() {
|
|
925
|
-
super(
|
|
646
|
+
super(DEFAULT_DECODER_FFMPEG_ADDON_CONFIG);
|
|
926
647
|
}
|
|
927
648
|
globalSettingsSchema() {
|
|
928
649
|
return this.schema({ sections: [{
|
|
929
|
-
id: "
|
|
930
|
-
title: "
|
|
650
|
+
id: "backend",
|
|
651
|
+
title: "Decoder backend",
|
|
931
652
|
tab: "decoder",
|
|
932
|
-
description: "
|
|
653
|
+
description: "Which decoder addon provides the decoder capability on this node — per-node selection, stored centrally on the hub. Selection is decided once at addon init (registration-time, race-free), so a change applies at the next restart of the decoder addons on the node.",
|
|
933
654
|
fields: [this.field({
|
|
934
655
|
type: "select",
|
|
935
|
-
key: "
|
|
936
|
-
label: "
|
|
937
|
-
options: [...
|
|
938
|
-
default:
|
|
939
|
-
immediate: true
|
|
940
|
-
|
|
941
|
-
type: "text",
|
|
942
|
-
key: "probedBestHwaccel",
|
|
943
|
-
label: "Probed best",
|
|
944
|
-
description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
|
|
945
|
-
readonlyField: true,
|
|
946
|
-
default: "",
|
|
947
|
-
actions: [{
|
|
948
|
-
action: "reprobe-hwaccel",
|
|
949
|
-
icon: "refresh-cw",
|
|
950
|
-
tooltip: "Re-probe hwaccel"
|
|
951
|
-
}]
|
|
656
|
+
key: "backend",
|
|
657
|
+
label: "Backend",
|
|
658
|
+
options: [...DECODER_BACKEND_OPTIONS],
|
|
659
|
+
default: DEFAULT_DECODER_BACKEND,
|
|
660
|
+
immediate: true,
|
|
661
|
+
perNode: true
|
|
952
662
|
})]
|
|
663
|
+
}, {
|
|
664
|
+
id: "hwaccel",
|
|
665
|
+
title: "Hardware acceleration",
|
|
666
|
+
tab: "decoder",
|
|
667
|
+
description: "Backend used by ffmpeg-subprocess decoder sessions. \"Auto\" defers to the probed best (VA-API on Intel); \"Off\" forces pure software. Only the VA-API hardware path is validated — other concrete backends degrade to software. Changes apply to NEW sessions.",
|
|
668
|
+
fields: [
|
|
669
|
+
this.field({
|
|
670
|
+
type: "select",
|
|
671
|
+
key: "hwaccel",
|
|
672
|
+
label: "Preferred backend",
|
|
673
|
+
options: [...HWACCEL_OPTIONS],
|
|
674
|
+
default: "auto",
|
|
675
|
+
immediate: true,
|
|
676
|
+
perNode: true
|
|
677
|
+
}),
|
|
678
|
+
this.field({
|
|
679
|
+
type: "text",
|
|
680
|
+
key: "probedBestHwaccel",
|
|
681
|
+
label: "Probed best",
|
|
682
|
+
description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
|
|
683
|
+
readonlyField: true,
|
|
684
|
+
default: "",
|
|
685
|
+
perNode: true,
|
|
686
|
+
actions: [{
|
|
687
|
+
action: "reprobe-hwaccel",
|
|
688
|
+
icon: "refresh-cw",
|
|
689
|
+
tooltip: "Re-probe hwaccel"
|
|
690
|
+
}]
|
|
691
|
+
}),
|
|
692
|
+
this.field({
|
|
693
|
+
type: "text",
|
|
694
|
+
key: "binaryPath",
|
|
695
|
+
label: "ffmpeg binary path (override)",
|
|
696
|
+
description: "Optional per-node override for the ffmpeg binary. Leave empty to auto-provision a portable static ffmpeg on this node (recommended). Per-node because the path differs per OS/arch.",
|
|
697
|
+
placeholder: "ffmpeg",
|
|
698
|
+
default: "",
|
|
699
|
+
perNode: true
|
|
700
|
+
})
|
|
701
|
+
]
|
|
953
702
|
}] });
|
|
954
703
|
}
|
|
955
704
|
async onInitialize() {
|
|
956
|
-
const backend = await resolveDecoderBackend(this.ctx.
|
|
705
|
+
const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
957
706
|
if (backend !== "ffmpeg") {
|
|
958
707
|
this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
959
708
|
return [];
|
|
@@ -991,10 +740,8 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
991
740
|
* decoder cap and a later session spawn re-runs the ensure.
|
|
992
741
|
*/
|
|
993
742
|
async resolveFfmpegBinaryPath() {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (typeof bp === "string" && bp.trim().length > 0) return bp;
|
|
997
|
-
} catch {}
|
|
743
|
+
const override = this.config.binaryPath;
|
|
744
|
+
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
998
745
|
try {
|
|
999
746
|
return await this.ctx.deps.ensureFfmpeg();
|
|
1000
747
|
} catch (err) {
|
|
@@ -1010,20 +757,51 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1010
757
|
* - `'none'` → `[]` (pure software).
|
|
1011
758
|
* - explicit backend → `[<backend>]` (forced; the session still falls to
|
|
1012
759
|
* software if that one child fails).
|
|
1013
|
-
* - `'auto'` →
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
* (
|
|
1017
|
-
*
|
|
760
|
+
* - `'auto'` → the LOCAL node's preferred order from `ctx.kernel.hwaccel` —
|
|
761
|
+
* the in-process resolver every forked child (hub / worker / agent) is
|
|
762
|
+
* given, which derives the backend from `os.platform()` + a GPU-vendor probe
|
|
763
|
+
* (darwin → `videotoolbox`; Intel → `qsv,vaapi`; NVIDIA → `cuda,nvdec`).
|
|
764
|
+
* Ranked via {@link rankDecodeHwAccels} (vaapi ABOVE qsv, since qsv is broken
|
|
765
|
+
* on the Intel stack while vaapi works). On a resolver error → `[]`
|
|
766
|
+
* (software).
|
|
767
|
+
*
|
|
768
|
+
* Two things this deliberately does NOT do:
|
|
769
|
+
* - It does NOT call the `platform-probe` singleton cap. Its per-node
|
|
770
|
+
* `nodePin` does not survive a forked agent child's link chain, so a pinned
|
|
771
|
+
* read resolves to the HUB's elected singleton and returns the hub's
|
|
772
|
+
* hardware — a Mac decoder was handed the hub's Intel `vaapi/qsv/...` chain
|
|
773
|
+
* (no `videotoolbox`) and fell all the way to software h265.
|
|
774
|
+
* - It does NOT probe `ffmpeg -hwaccels` (nor node-av) to intersect the list.
|
|
775
|
+
* The accelerator is deterministic from the host, which the kernel resolver
|
|
776
|
+
* already knows in-process — no subprocess, no cross-process round-trip. The
|
|
777
|
+
* rare case where the resolved backend is not usable by the actual ffmpeg
|
|
778
|
+
* build is covered by the session's per-child runtime fallback (advance to
|
|
779
|
+
* the next chain entry, else software). This mirrors how Scrypted resolves
|
|
780
|
+
* hwaccel (static `os.platform()` map + try-then-fall-back), not a native
|
|
781
|
+
* capability enumeration.
|
|
782
|
+
*
|
|
783
|
+
* macOS special-case: the resolved `videotoolbox` is emitted as `-hwaccel
|
|
784
|
+
* auto`, NOT explicit `-hwaccel videotoolbox`. Explicit videotoolbox tears the
|
|
785
|
+
* ffmpeg child down under MULTIPLE concurrent decode streams (documented by
|
|
786
|
+
* Scrypted NVR's ffmpeg path; our Mac decodes many cameras at once — exactly
|
|
787
|
+
* that case). `auto` lets ffmpeg pick VideoToolbox internally and auto-download
|
|
788
|
+
* frames to software (no GPU surface to dispose, no `scale_vt`). The in-process
|
|
789
|
+
* node-av decoder (`decoder-nodeav`) still uses explicit videotoolbox — this
|
|
790
|
+
* substitution is specific to the ffmpeg-subprocess decode.
|
|
1018
791
|
*/
|
|
1019
792
|
async resolveDecodeHwaccelChain() {
|
|
1020
793
|
const pref = this.config.hwaccel;
|
|
1021
794
|
if (pref === "none") return [];
|
|
1022
795
|
if (pref !== "auto") return [pref];
|
|
1023
796
|
try {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
797
|
+
const resolver = this.ctx.kernel.hwaccel;
|
|
798
|
+
if (!resolver) {
|
|
799
|
+
this.ctx.logger.warn("ffmpeg: no local kernel hwaccel resolver — software decode");
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
const chain = rankDecodeHwAccels((await resolver.resolve(null)).preferred, []);
|
|
803
|
+
if (process.platform === "darwin") return chain.map((method) => method === "videotoolbox" ? "auto" : method);
|
|
804
|
+
return chain;
|
|
1027
805
|
} catch (err) {
|
|
1028
806
|
this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1029
807
|
return [];
|
|
@@ -1033,13 +811,13 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1033
811
|
const resolver = this.ctx.kernel.hwaccel;
|
|
1034
812
|
if (!resolver) {
|
|
1035
813
|
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
|
|
1036
|
-
await this.
|
|
814
|
+
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
1037
815
|
return { backend: "none" };
|
|
1038
816
|
}
|
|
1039
817
|
try {
|
|
1040
818
|
const res = await resolver.resolve();
|
|
1041
|
-
const backend = res.preferred[0] ?? "none";
|
|
1042
|
-
await this.
|
|
819
|
+
const backend = rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
|
|
820
|
+
await this.updateGlobalSettings({ probedBestHwaccel: backend });
|
|
1043
821
|
this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
|
|
1044
822
|
backend,
|
|
1045
823
|
rationale: res.rationale,
|
|
@@ -1048,7 +826,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1048
826
|
return { backend };
|
|
1049
827
|
} catch (err) {
|
|
1050
828
|
this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1051
|
-
await this.
|
|
829
|
+
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
1052
830
|
return { backend: "none" };
|
|
1053
831
|
}
|
|
1054
832
|
}
|
|
@@ -1115,7 +893,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1115
893
|
};
|
|
1116
894
|
}
|
|
1117
895
|
wireCallbackSink(sessionId, session) {
|
|
1118
|
-
const ringBuffer = new
|
|
896
|
+
const ringBuffer = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1119
897
|
this.frameBuffers.set(sessionId, ringBuffer);
|
|
1120
898
|
return session.onFrame((frame) => {
|
|
1121
899
|
const { format } = frame;
|
|
@@ -1133,7 +911,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1133
911
|
});
|
|
1134
912
|
}
|
|
1135
913
|
wireShmSink(sessionId, session) {
|
|
1136
|
-
const handleRing = new
|
|
914
|
+
const handleRing = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1137
915
|
this.handleBuffers.set(sessionId, handleRing);
|
|
1138
916
|
return session.onFrameHandle((frame) => {
|
|
1139
917
|
handleRing.push(frame.handle);
|
|
@@ -1145,6 +923,8 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1145
923
|
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`);
|
|
1146
924
|
const unsub = this.unsubscribers.get(sessionId);
|
|
1147
925
|
if (unsub) unsub();
|
|
926
|
+
this.frameBuffers.get(sessionId)?.cancel();
|
|
927
|
+
this.handleBuffers.get(sessionId)?.cancel();
|
|
1148
928
|
await session.destroy();
|
|
1149
929
|
this.sessions.delete(sessionId);
|
|
1150
930
|
this.frameBuffers.delete(sessionId);
|
|
@@ -1182,12 +962,14 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
1182
962
|
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1183
963
|
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
1184
964
|
if (!ringBuffer) return [];
|
|
965
|
+
if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
|
|
1185
966
|
return ringBuffer.drain(input.maxCount);
|
|
1186
967
|
}
|
|
1187
968
|
async pullHandles(input) {
|
|
1188
969
|
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1189
970
|
const handleRing = this.handleBuffers.get(input.sessionId);
|
|
1190
971
|
if (!handleRing) return [];
|
|
972
|
+
if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
|
|
1191
973
|
return handleRing.drain(input.maxCount);
|
|
1192
974
|
}
|
|
1193
975
|
async updateConfig(input) {
|
|
@@ -2,8 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_frame_ring_sink = require("../frame-ring-sink-
|
|
5
|
+
const require_dist = require("../dist-Cwc0TUQr.js");
|
|
6
|
+
const require_frame_ring_sink = require("../frame-ring-sink-8LLV-cvH.js");
|
|
7
|
+
const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
|
|
7
8
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
9
|
let node_crypto = require("node:crypto");
|
|
9
10
|
//#region src/decoder-nodeav/scaler-geometry.ts
|
|
@@ -919,7 +920,8 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
919
920
|
label: "Preferred backend",
|
|
920
921
|
options: [...require_dist.HWACCEL_OPTIONS],
|
|
921
922
|
default: "auto",
|
|
922
|
-
immediate: true
|
|
923
|
+
immediate: true,
|
|
924
|
+
perNode: true
|
|
923
925
|
}), this.field({
|
|
924
926
|
type: "text",
|
|
925
927
|
key: "probedBestHwaccel",
|
|
@@ -927,6 +929,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
927
929
|
description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
|
|
928
930
|
readonlyField: true,
|
|
929
931
|
default: "",
|
|
932
|
+
perNode: true,
|
|
930
933
|
actions: [{
|
|
931
934
|
action: "reprobe-hwaccel",
|
|
932
935
|
icon: "refresh-cw",
|
|
@@ -936,7 +939,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
936
939
|
}] });
|
|
937
940
|
}
|
|
938
941
|
async onInitialize() {
|
|
939
|
-
const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.
|
|
942
|
+
const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
940
943
|
if (backend !== "nodeav") {
|
|
941
944
|
this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
942
945
|
return [];
|
|
@@ -970,13 +973,13 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
970
973
|
const resolver = this.ctx.kernel.hwaccel;
|
|
971
974
|
if (!resolver) {
|
|
972
975
|
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
|
|
973
|
-
await this.
|
|
976
|
+
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
974
977
|
return { backend: "none" };
|
|
975
978
|
}
|
|
976
979
|
try {
|
|
977
980
|
const res = await resolver.resolve();
|
|
978
|
-
const backend = res.preferred[0] ?? "none";
|
|
979
|
-
await this.
|
|
981
|
+
const backend = require_ffmpeg_args.rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
|
|
982
|
+
await this.updateGlobalSettings({ probedBestHwaccel: backend });
|
|
980
983
|
this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
|
|
981
984
|
backend,
|
|
982
985
|
rationale: res.rationale,
|
|
@@ -985,7 +988,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
985
988
|
return { backend };
|
|
986
989
|
} catch (err) {
|
|
987
990
|
this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
988
|
-
await this.
|
|
991
|
+
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
989
992
|
return { backend: "none" };
|
|
990
993
|
}
|
|
991
994
|
}
|
|
@@ -1052,7 +1055,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
1052
1055
|
* ring buffer drained by `pullFrames`.
|
|
1053
1056
|
*/
|
|
1054
1057
|
wireCallbackSink(sessionId, session) {
|
|
1055
|
-
const ringBuffer = new
|
|
1058
|
+
const ringBuffer = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1056
1059
|
this.frameBuffers.set(sessionId, ringBuffer);
|
|
1057
1060
|
return session.onFrame((frame) => {
|
|
1058
1061
|
const { format } = frame;
|
|
@@ -1075,7 +1078,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
1075
1078
|
* — the broker opens the named segment and reads the pixels zero-copy.
|
|
1076
1079
|
*/
|
|
1077
1080
|
wireShmSink(sessionId, session) {
|
|
1078
|
-
const handleRing = new
|
|
1081
|
+
const handleRing = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
1079
1082
|
this.handleBuffers.set(sessionId, handleRing);
|
|
1080
1083
|
return session.onFrameHandle((frame) => {
|
|
1081
1084
|
handleRing.push(frame.handle);
|
|
@@ -1087,6 +1090,8 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
1087
1090
|
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
|
|
1088
1091
|
const unsub = this.unsubscribers.get(sessionId);
|
|
1089
1092
|
if (unsub) unsub();
|
|
1093
|
+
this.frameBuffers.get(sessionId)?.cancel();
|
|
1094
|
+
this.handleBuffers.get(sessionId)?.cancel();
|
|
1090
1095
|
await session.destroy();
|
|
1091
1096
|
this.sessions.delete(sessionId);
|
|
1092
1097
|
this.frameBuffers.delete(sessionId);
|
|
@@ -1123,12 +1128,14 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
1123
1128
|
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
1124
1129
|
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
1125
1130
|
if (!ringBuffer) return [];
|
|
1131
|
+
if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
|
|
1126
1132
|
return ringBuffer.drain(input.maxCount);
|
|
1127
1133
|
}
|
|
1128
1134
|
async pullHandles(input) {
|
|
1129
1135
|
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
1130
1136
|
const handleRing = this.handleBuffers.get(input.sessionId);
|
|
1131
1137
|
if (!handleRing) return [];
|
|
1138
|
+
if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
|
|
1132
1139
|
return handleRing.drain(input.maxCount);
|
|
1133
1140
|
}
|
|
1134
1141
|
async updateConfig(input) {
|