@camstack/addon-pipeline 1.1.34 → 1.1.36
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/assets/reference-audio/dog-bark.wav +0 -0
- package/assets/reference-audio/meow.wav +0 -0
- package/assets/reference-audio/silence.wav +0 -0
- package/assets/reference-audio/siren.wav +0 -0
- package/assets/reference-audio/speech.wav +0 -0
- package/assets/reference-audio/synthetic-alarm.wav +0 -0
- package/assets/reference-audio/synthetic-bark.wav +0 -0
- package/assets/reference-audio/synthetic-cry.wav +0 -0
- package/assets/reference-audio/synthetic-speech.wav +0 -0
- package/assets/reference-images/1-bird.jpg +0 -0
- package/assets/reference-images/1-car-plate.jpg +0 -0
- package/assets/reference-images/2-birds.jpg +0 -0
- package/assets/reference-images/2-cars-plates.jpg +0 -0
- package/assets/reference-images/3-persons.jpg +0 -0
- package/assets/reference-images/README.md +16 -0
- package/assets/reference-images/animals-group.jpg +0 -0
- package/assets/reference-images/ground-truth/1-bird.json +22 -0
- package/assets/reference-images/ground-truth/1-car-plate.json +85 -0
- package/assets/reference-images/ground-truth/2-birds.json +35 -0
- package/assets/reference-images/ground-truth/2-cars-plates.json +137 -0
- package/assets/reference-images/ground-truth/3-persons.json +186 -0
- package/assets/reference-images/ground-truth/animals-group.json +87 -0
- package/assets/reference-images/ground-truth/persons-cars-animal.json +136 -0
- package/assets/reference-images/ground-truth/sinner.json +73 -0
- package/assets/reference-images/persons-cars-animal.jpg +0 -0
- package/assets/reference-images/sinner.jpg +0 -0
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +3 -1
- package/dist/detection-pipeline/index.mjs +3 -1
- package/dist/{dist-CfSORkv2.mjs → dist-BESCfHIV.mjs} +35 -128
- package/dist/{dist-BrIE85BG.js → dist-BZBQc2QZ.js} +34 -145
- package/dist/{frame-handle-plane-C05rq9Zs.mjs → frame-handle-plane-CYtnVPIg.mjs} +1 -1
- package/dist/{frame-handle-plane-zMju9vmk.js → frame-handle-plane-ChjxN8oI.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BDLr3512.mjs} +2 -2
- package/dist/stream-broker/{hostInit-C8slrxw1.mjs → hostInit-C0Y3vfFE.mjs} +2 -2
- package/dist/stream-broker/index.js +133 -18
- package/dist/stream-broker/index.mjs +119 -4
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js → MaskShapeCanvas-DI4BY7W2-BL-S3O0Z.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CF7FXXLs.js → MotionZonesSettings-NcxxQN8r-DcnKZAt6.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-qQCpVob3.js → PrivacyMaskSettings-APgPLF7p-DQ0MK5T9.js} +1 -1
- package/embed-dist/assets/{index-BNhisVUZ.js → index-BCGUC-8v.js} +7 -7
- package/embed-dist/index.html +1 -1
- package/package.json +3 -57
- package/dist/decoder-ffmpeg/index.js +0 -1114
- package/dist/decoder-ffmpeg/index.mjs +0 -1097
- package/dist/decoder-nodeav/index.js +0 -1422
- package/dist/decoder-nodeav/index.mjs +0 -1414
- package/dist/ffmpeg-args-CpeRWwL4.mjs +0 -326
- package/dist/ffmpeg-args-Dl9mLcOC.js +0 -421
- package/dist/frame-dropper-AjheBGMG.mjs +0 -22
- package/dist/frame-dropper-DKLM6pMz.js +0 -27
- package/dist/frame-ring-sink-1aLGcNNf.mjs +0 -557
- package/dist/frame-ring-sink-CvlcAUOu.js +0 -604
|
@@ -1,1114 +0,0 @@
|
|
|
1
|
-
Object.defineProperties(exports, {
|
|
2
|
-
__esModule: { value: true },
|
|
3
|
-
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
-
});
|
|
5
|
-
const require_dist = require("../dist-BrIE85BG.js");
|
|
6
|
-
const require_frame_ring_sink = require("../frame-ring-sink-CvlcAUOu.js");
|
|
7
|
-
const require_ffmpeg_args = require("../ffmpeg-args-Dl9mLcOC.js");
|
|
8
|
-
const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
|
|
9
|
-
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
10
|
-
let node_crypto = require("node:crypto");
|
|
11
|
-
let node_child_process = require("node:child_process");
|
|
12
|
-
//#region src/decoder-ffmpeg/frame-stream-splitter.ts
|
|
13
|
-
var FrameStreamSplitter = class {
|
|
14
|
-
frameSize;
|
|
15
|
-
ctl;
|
|
16
|
-
/** Whether a frame is currently being filled (partially received). */
|
|
17
|
-
open = false;
|
|
18
|
-
/** The current frame's fill target, or `null` when the open frame is dropped. */
|
|
19
|
-
target = null;
|
|
20
|
-
/** Bytes already written into the current frame. */
|
|
21
|
-
filled = 0;
|
|
22
|
-
constructor(frameSize, ctl) {
|
|
23
|
-
if (!Number.isInteger(frameSize) || frameSize <= 0) throw new Error(`FrameStreamSplitter: frameSize must be a positive integer, got ${frameSize}`);
|
|
24
|
-
this.frameSize = frameSize;
|
|
25
|
-
this.ctl = ctl;
|
|
26
|
-
}
|
|
27
|
-
/** True while a frame has been started (a live slot may be open) but not yet
|
|
28
|
-
* completed — the session aborts an open slot on teardown. */
|
|
29
|
-
get hasOpenFrame() {
|
|
30
|
-
return this.open && this.target !== null;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Consume a stdout chunk, emitting every frame boundary it completes. A chunk
|
|
34
|
-
* may finish the open frame, contain several whole frames, and/or leave a
|
|
35
|
-
* partial frame buffered for the next chunk.
|
|
36
|
-
*/
|
|
37
|
-
push(chunk) {
|
|
38
|
-
const n = chunk.length;
|
|
39
|
-
let off = 0;
|
|
40
|
-
while (off < n) {
|
|
41
|
-
if (!this.open) {
|
|
42
|
-
this.target = this.ctl.startFrame();
|
|
43
|
-
this.open = true;
|
|
44
|
-
this.filled = 0;
|
|
45
|
-
}
|
|
46
|
-
const take = Math.min(this.frameSize - this.filled, n - off);
|
|
47
|
-
if (this.target !== null) chunk.copy(this.target, this.filled, off, off + take);
|
|
48
|
-
this.filled += take;
|
|
49
|
-
off += take;
|
|
50
|
-
if (this.filled === this.frameSize) {
|
|
51
|
-
const finished = this.target;
|
|
52
|
-
this.open = false;
|
|
53
|
-
this.target = null;
|
|
54
|
-
this.filled = 0;
|
|
55
|
-
if (finished !== null) this.ctl.finishFrame(finished);
|
|
56
|
-
else this.ctl.dropFrame();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/decoder-ffmpeg/ffmpeg-decoder-session.ts
|
|
63
|
-
/**
|
|
64
|
-
* FfmpegDecoderSession — an `IDecoderSession` that decodes video in an **ffmpeg
|
|
65
|
-
* subprocess** and feeds the shared-memory frame ring (Phase 5 / D9), the same
|
|
66
|
-
* ring `decoder-nodeav` writes into.
|
|
67
|
-
*
|
|
68
|
-
* Why a subprocess: on the Intel hub, node-av's in-process VA-API decode path
|
|
69
|
-
* SIGBUSes (uncatchable) on 8MP h264 and kills the whole decoder runner. ffmpeg
|
|
70
|
-
* decodes the identical hardware cleanly because it runs out-of-process — a
|
|
71
|
-
* crashing child just stops producing frames; the addon survives and the
|
|
72
|
-
* frame-plane rotates/retries.
|
|
73
|
-
*
|
|
74
|
-
* Data flow per session:
|
|
75
|
-
* Annex-B packets → ffmpeg stdin (pipe:0)
|
|
76
|
-
* ffmpeg → `-vf <scale_vaapi=…,hwdownload,… | hwdownload,…,scale=…>` (GPU or
|
|
77
|
-
* CPU scale, per {@link buildFfmpegDecodeArgs})
|
|
78
|
-
* raw video → ffmpeg stdout (pipe:1) → a {@link FrameStreamSplitter} that fills
|
|
79
|
-
* each frame's bytes DIRECTLY into its shm ring slot (zero-copy
|
|
80
|
-
* `beginFrame`/`commitFrame`) → `FrameHandle` (shm mode), or into a
|
|
81
|
-
* per-frame buffer → `DecodedFrame` via `onFrame` (callback mode).
|
|
82
|
-
* No `Buffer.concat` accumulator, one copy per byte, none for dropped
|
|
83
|
-
* frames.
|
|
84
|
-
*
|
|
85
|
-
* Output geometry: the broker does not pass source width/height for the shm
|
|
86
|
-
* path, so ffmpeg picks the height (`scale=min(iw\,maxW):-2`) and the session
|
|
87
|
-
* learns the exact output W×H by parsing the muxer's `Output #0` stderr block
|
|
88
|
-
* ({@link parseFfmpegOutputDims}). Until dims are known, stdout is buffered.
|
|
89
|
-
*/
|
|
90
|
-
var noopLogger = {
|
|
91
|
-
debug() {},
|
|
92
|
-
info() {},
|
|
93
|
-
warn() {},
|
|
94
|
-
error() {},
|
|
95
|
-
child() {
|
|
96
|
-
return noopLogger;
|
|
97
|
-
},
|
|
98
|
-
withTags() {
|
|
99
|
-
return noopLogger;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
/** A hardware child that dies faster than this (with no frames) advances the
|
|
103
|
-
* session to the NEXT decode path (next hwaccel, else software) — a genuine
|
|
104
|
-
* hwaccel init failure (missing render node, unsupported codec on GPU, the
|
|
105
|
-
* Intel `qsv` `code=171`) surfaces this early. */
|
|
106
|
-
var HWACCEL_FALLBACK_WINDOW_MS = 4e3;
|
|
107
|
-
/**
|
|
108
|
-
* Delay before a PULL-MODE session re-dials its input URL after the child
|
|
109
|
-
* exits (RTSP EOF, restream teardown, network blip). Push mode never
|
|
110
|
-
* self-respawns on a plain exit — the broker's packet feed drives recovery —
|
|
111
|
-
* but a pull-mode child owns its own input, so the session must re-dial or a
|
|
112
|
-
* single EOF would silence it forever. The delay turns a hard-down source
|
|
113
|
-
* into a slow probe instead of a spawn storm; the consumer tearing the
|
|
114
|
-
* session down (`destroy`) cancels the pending re-dial.
|
|
115
|
-
*/
|
|
116
|
-
var PULL_REDIAL_DELAY_MS = 3e3;
|
|
117
|
-
/**
|
|
118
|
-
* Safety ceiling for the current-GOP replay buffer (push mode). A GOP resets on
|
|
119
|
-
* every keyframe so this is never reached by a well-behaved stream; it only
|
|
120
|
-
* bounds a pathological long/no-keyframe stream. When exceeded, only the
|
|
121
|
-
* keyframe is retained — enough to re-prime the decoder's SPS/PPS on a re-spawn.
|
|
122
|
-
*/
|
|
123
|
-
var MAX_GOP_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
124
|
-
var FfmpegDecoderSession = class {
|
|
125
|
-
config;
|
|
126
|
-
logger;
|
|
127
|
-
frameSink;
|
|
128
|
-
nodeId;
|
|
129
|
-
renderDevice;
|
|
130
|
-
ffmpegPath;
|
|
131
|
-
/**
|
|
132
|
-
* Ordered hardware decode paths to attempt, best-first. Empty ⇒ software
|
|
133
|
-
* only. The session walks it via {@link hwaccelIndex}; an index past the end
|
|
134
|
-
* means software (`null`).
|
|
135
|
-
*/
|
|
136
|
-
hwaccelChain;
|
|
137
|
-
hwaccelIndex = 0;
|
|
138
|
-
hwaccel;
|
|
139
|
-
/** GPU scalers the build offers; drives whether decode scales on the GPU. */
|
|
140
|
-
availableGpuScaleFilters;
|
|
141
|
-
onGpuScaleUnsupported;
|
|
142
|
-
/**
|
|
143
|
-
* Whether the CURRENT backend attempt may use its GPU scaler. Starts true for
|
|
144
|
-
* each backend; a GPU-scale child that dies immediately with no frames flips
|
|
145
|
-
* it false and the SAME backend is retried on the CPU-scale chain (so a broken
|
|
146
|
-
* GPU scaler degrades to hw-decode + CPU-scale, never to software decode).
|
|
147
|
-
*/
|
|
148
|
-
gpuScaleEnabled = true;
|
|
149
|
-
/** The GPU scaler the live child was spawned with, or null (CPU scale). */
|
|
150
|
-
activeGpuScaleFilter = null;
|
|
151
|
-
process = null;
|
|
152
|
-
spawnedAtMs = 0;
|
|
153
|
-
/**
|
|
154
|
-
* Whether a spawn has ever been attempted. Drives the LAZY first spawn
|
|
155
|
-
* (first `pushPacket` / `openStream`) and preserves the recovery semantics
|
|
156
|
-
* afterwards: a push-mode child that later dies is NOT respawned by the
|
|
157
|
-
* packet feed (the consumer / frame-plane self-heal recreates the session),
|
|
158
|
-
* and `updateConfig` only restarts a session that has already spawned.
|
|
159
|
-
*/
|
|
160
|
-
spawnAttempted = false;
|
|
161
|
-
/**
|
|
162
|
-
* PULL-MODE input URL (P2b). `null` = push mode (packets via stdin — the
|
|
163
|
-
* default). Set by {@link openStream}; every (re)spawn since reads this URL
|
|
164
|
-
* directly instead of `pipe:0`. A session is push-mode or pull-mode for its
|
|
165
|
-
* whole life — `openStream` on a packet-fed session throws.
|
|
166
|
-
*/
|
|
167
|
-
pullUrl = null;
|
|
168
|
-
/** Pending pull-mode re-dial timer (armed on a pull child exit). */
|
|
169
|
-
pullRedialTimer = null;
|
|
170
|
-
/**
|
|
171
|
-
* Splits ffmpeg's raw stdout byte stream into frames, filling each DIRECTLY
|
|
172
|
-
* into its shm slot (zero-copy) — created once the output geometry is known.
|
|
173
|
-
*/
|
|
174
|
-
splitter = null;
|
|
175
|
-
/** Stdout chunks received BEFORE geometry is known — replayed into the
|
|
176
|
-
* splitter once `frameSize` is learned. A short-lived list (no concat). */
|
|
177
|
-
pendingStdout = [];
|
|
178
|
-
/** The shm ring slot the splitter is currently filling, if any — aborted on
|
|
179
|
-
* respawn / teardown so a half-filled slot never publishes. */
|
|
180
|
-
openSlot = null;
|
|
181
|
-
stderrAccum = "";
|
|
182
|
-
destroyed = false;
|
|
183
|
-
pixel;
|
|
184
|
-
channels;
|
|
185
|
-
/** The cap-facing raw format label (`'rgb'` / `'gray'`) stamped into frames. */
|
|
186
|
-
frameFormat;
|
|
187
|
-
outWidth = 0;
|
|
188
|
-
outHeight = 0;
|
|
189
|
-
frameSize = 0;
|
|
190
|
-
frameRingSink = null;
|
|
191
|
-
frameCallbacks = /* @__PURE__ */ new Set();
|
|
192
|
-
handleCallbacks = /* @__PURE__ */ new Set();
|
|
193
|
-
frameDropper;
|
|
194
|
-
inputPackets = 0;
|
|
195
|
-
/**
|
|
196
|
-
* Current-GOP Annex-B packet bytes (push mode), from the most recent keyframe
|
|
197
|
-
* forward. Replayed keyframe-first into every freshly-spawned child so a
|
|
198
|
-
* hwaccel cascade re-spawn (`vaapi → qsv → software`) or a config restart
|
|
199
|
-
* resumes on a keyframe instead of the live mid-GOP P-frame the dead child had
|
|
200
|
-
* already consumed — which the fallback child would otherwise choke on
|
|
201
|
-
* (`non-existing PPS 0`) until the next live keyframe. Mirrors the reference
|
|
202
|
-
* NVR libav decoder, which replays its buffered packet list to each hwaccel
|
|
203
|
-
* candidate. Reset on each keyframe; unused in pull mode.
|
|
204
|
-
*/
|
|
205
|
-
gopBuffer = [];
|
|
206
|
-
gopBufferBytes = 0;
|
|
207
|
-
outputFrames = 0;
|
|
208
|
-
droppedFrames = 0;
|
|
209
|
-
startTime = Date.now();
|
|
210
|
-
constructor(config, logger = noopLogger, options) {
|
|
211
|
-
this.config = { ...config };
|
|
212
|
-
const sessionTags = {};
|
|
213
|
-
if (typeof config.deviceId === "number") sessionTags["deviceId"] = config.deviceId;
|
|
214
|
-
if (typeof config.tag === "string" && config.tag.length > 0) sessionTags["tag"] = config.tag;
|
|
215
|
-
this.logger = Object.keys(sessionTags).length > 0 ? logger.withTags(sessionTags) : logger;
|
|
216
|
-
this.frameSink = options?.frameSink ?? "callback";
|
|
217
|
-
this.nodeId = options?.nodeId ?? "local";
|
|
218
|
-
this.renderDevice = options?.renderDevice;
|
|
219
|
-
this.ffmpegPath = options?.ffmpegPath ?? "ffmpeg";
|
|
220
|
-
const seededChain = options?.hwaccelChain ?? (options?.hwaccel != null && !require_ffmpeg_args.isSoftwareHwAccel(options.hwaccel) ? [options.hwaccel] : []);
|
|
221
|
-
this.hwaccelChain = seededChain.filter((m) => !require_ffmpeg_args.isSoftwareHwAccel(m));
|
|
222
|
-
this.hwaccel = this.currentHwAccel();
|
|
223
|
-
this.availableGpuScaleFilters = options?.availableGpuScaleFilters ?? /* @__PURE__ */ new Set();
|
|
224
|
-
this.onGpuScaleUnsupported = options?.onGpuScaleUnsupported;
|
|
225
|
-
this.pixel = require_ffmpeg_args.rawPixelForFormat(config.outputFormat);
|
|
226
|
-
this.channels = require_ffmpeg_args.channelsForPixel(this.pixel);
|
|
227
|
-
this.frameFormat = this.pixel === "gray" ? "gray" : "rgb";
|
|
228
|
-
this.frameDropper = new require_frame_dropper.FrameDropper(config.maxFps);
|
|
229
|
-
}
|
|
230
|
-
/** Exposed so the owning addon can surface ring stats via `getShmStats`. */
|
|
231
|
-
get frameRingSinkOrNull() {
|
|
232
|
-
return this.frameRingSink;
|
|
233
|
-
}
|
|
234
|
-
get isPullMode() {
|
|
235
|
-
return this.pullUrl !== null;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Switch the session to PULL MODE: (re)spawn ffmpeg reading `url` directly
|
|
239
|
-
* (`-rtsp_transport tcp -i <url>` for rtsp) instead of stdin. The whole
|
|
240
|
-
* output path (filtergraph, rawvideo pipe, shm ring sink) is unchanged.
|
|
241
|
-
*
|
|
242
|
-
* - Calling it again re-dials with the FRESH url (a hub restream bounce
|
|
243
|
-
* mints a new token URL — never cache the old one).
|
|
244
|
-
* - Calling it on a session already fed via `pushPacket` throws: a session
|
|
245
|
-
* is push-mode or pull-mode, never both (mixing inputs would corrupt the
|
|
246
|
-
* decode).
|
|
247
|
-
*/
|
|
248
|
-
async openStream(url) {
|
|
249
|
-
if (this.destroyed) return;
|
|
250
|
-
if (this.pullUrl === null && this.inputPackets > 0) throw new Error("ffmpeg decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
|
|
251
|
-
this.pullUrl = url;
|
|
252
|
-
this.logger.info("ffmpeg decoder: pull-mode openStream — dialing input URL", { meta: { url } });
|
|
253
|
-
this.spawnFfmpeg();
|
|
254
|
-
}
|
|
255
|
-
ensureFrameRingSink() {
|
|
256
|
-
if (this.frameRingSink === null) {
|
|
257
|
-
const seedParts = [];
|
|
258
|
-
if (typeof this.config.deviceId === "number") seedParts.push(String(this.config.deviceId));
|
|
259
|
-
if (typeof this.config.tag === "string" && this.config.tag.length > 0) seedParts.push(this.config.tag);
|
|
260
|
-
const seed = seedParts.length > 0 ? seedParts.join(":") : "anon";
|
|
261
|
-
this.frameRingSink = new require_frame_ring_sink.DecoderFrameRingSink({
|
|
262
|
-
seed,
|
|
263
|
-
logger: this.logger,
|
|
264
|
-
nodeId: this.nodeId
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
return this.frameRingSink;
|
|
268
|
-
}
|
|
269
|
-
spawnFfmpeg() {
|
|
270
|
-
if (this.destroyed) return;
|
|
271
|
-
this.spawnAttempted = true;
|
|
272
|
-
this.clearPullRedial();
|
|
273
|
-
this.killFfmpeg();
|
|
274
|
-
this.abortOpenSlot();
|
|
275
|
-
this.splitter = null;
|
|
276
|
-
this.pendingStdout = [];
|
|
277
|
-
this.stderrAccum = "";
|
|
278
|
-
this.outWidth = 0;
|
|
279
|
-
this.outHeight = 0;
|
|
280
|
-
this.frameSize = 0;
|
|
281
|
-
this.activeGpuScaleFilter = this.gpuScaleEnabled ? require_ffmpeg_args.resolveGpuScaleFilter(this.hwaccel, this.availableGpuScaleFilters) : null;
|
|
282
|
-
const args = require_ffmpeg_args.buildFfmpegDecodeArgs({
|
|
283
|
-
codec: this.config.codec,
|
|
284
|
-
scale: this.config.scale,
|
|
285
|
-
outputFormat: this.config.outputFormat
|
|
286
|
-
}, {
|
|
287
|
-
hwaccel: this.hwaccel,
|
|
288
|
-
renderDevice: this.renderDevice,
|
|
289
|
-
gpuScaleFilter: this.activeGpuScaleFilter,
|
|
290
|
-
...this.pullUrl !== null ? { inputUrl: this.pullUrl } : {}
|
|
291
|
-
});
|
|
292
|
-
let child;
|
|
293
|
-
try {
|
|
294
|
-
child = (0, node_child_process.spawn)(this.ffmpegPath, args);
|
|
295
|
-
} catch (err) {
|
|
296
|
-
this.logger.error("ffmpeg decoder: spawn threw", { meta: { error: require_dist.errMsg(err) } });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
this.process = child;
|
|
300
|
-
this.spawnedAtMs = Date.now();
|
|
301
|
-
child.stdin?.on("error", () => {});
|
|
302
|
-
this.replayGopBuffer(child);
|
|
303
|
-
child.stdout?.on("data", (chunk) => {
|
|
304
|
-
if (child === this.process) this.handleStdout(chunk);
|
|
305
|
-
});
|
|
306
|
-
child.stderr?.on("data", (data) => {
|
|
307
|
-
if (child === this.process) this.handleStderr(data);
|
|
308
|
-
});
|
|
309
|
-
child.on("error", (err) => {
|
|
310
|
-
if (child !== this.process) return;
|
|
311
|
-
this.logger.error("ffmpeg decoder: process error", { meta: { error: err.message } });
|
|
312
|
-
});
|
|
313
|
-
child.on("exit", (code, signal) => {
|
|
314
|
-
if (child === this.process) this.handleExit(code, signal);
|
|
315
|
-
});
|
|
316
|
-
this.logger.info("ffmpeg decoder: spawned", { meta: {
|
|
317
|
-
hwaccel: this.hwaccel,
|
|
318
|
-
gpuScale: this.activeGpuScaleFilter ?? "cpu",
|
|
319
|
-
codec: this.config.codec,
|
|
320
|
-
format: this.frameFormat,
|
|
321
|
-
sink: this.frameSink
|
|
322
|
-
} });
|
|
323
|
-
}
|
|
324
|
-
killFfmpeg() {
|
|
325
|
-
const child = this.process;
|
|
326
|
-
if (!child) return;
|
|
327
|
-
this.process = null;
|
|
328
|
-
child.stdout?.removeAllListeners("data");
|
|
329
|
-
child.stderr?.removeAllListeners("data");
|
|
330
|
-
child.removeAllListeners("exit");
|
|
331
|
-
try {
|
|
332
|
-
child.stdin?.end();
|
|
333
|
-
} catch {}
|
|
334
|
-
try {
|
|
335
|
-
child.kill("SIGTERM");
|
|
336
|
-
} catch {}
|
|
337
|
-
const pid = child.pid;
|
|
338
|
-
setTimeout(() => {
|
|
339
|
-
if (pid !== void 0 && child.exitCode === null && child.signalCode === null) try {
|
|
340
|
-
child.kill("SIGKILL");
|
|
341
|
-
} catch {}
|
|
342
|
-
}, 500).unref?.();
|
|
343
|
-
}
|
|
344
|
-
/** The decode path for the current attempt — `hwaccelChain[index]`, or `null`
|
|
345
|
-
* (software) once the chain is exhausted. */
|
|
346
|
-
currentHwAccel() {
|
|
347
|
-
return this.hwaccelChain[this.hwaccelIndex] ?? null;
|
|
348
|
-
}
|
|
349
|
-
handleExit(code, signal) {
|
|
350
|
-
if (this.destroyed) return;
|
|
351
|
-
const diedFast = Date.now() - this.spawnedAtMs < HWACCEL_FALLBACK_WINDOW_MS;
|
|
352
|
-
if (!require_ffmpeg_args.isSoftwareHwAccel(this.hwaccel) && this.outputFrames === 0 && diedFast) {
|
|
353
|
-
if (this.activeGpuScaleFilter !== null) {
|
|
354
|
-
const brokenFilter = this.activeGpuScaleFilter;
|
|
355
|
-
this.gpuScaleEnabled = false;
|
|
356
|
-
this.onGpuScaleUnsupported?.(brokenFilter);
|
|
357
|
-
this.logger.warn("ffmpeg decoder: GPU-scale child exited early with no frames — retrying same backend with CPU scale", { meta: {
|
|
358
|
-
hwaccel: this.hwaccel,
|
|
359
|
-
gpuScaleFilter: brokenFilter,
|
|
360
|
-
code,
|
|
361
|
-
signal
|
|
362
|
-
} });
|
|
363
|
-
this.process = null;
|
|
364
|
-
this.spawnFfmpeg();
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const failed = this.hwaccel;
|
|
368
|
-
this.hwaccelIndex += 1;
|
|
369
|
-
this.hwaccel = this.currentHwAccel();
|
|
370
|
-
this.gpuScaleEnabled = true;
|
|
371
|
-
const next = require_ffmpeg_args.isSoftwareHwAccel(this.hwaccel) ? "software" : this.hwaccel;
|
|
372
|
-
this.logger.warn("ffmpeg decoder: hwaccel child exited early with no frames — trying next decode path", { meta: {
|
|
373
|
-
failed,
|
|
374
|
-
next,
|
|
375
|
-
code,
|
|
376
|
-
signal
|
|
377
|
-
} });
|
|
378
|
-
this.process = null;
|
|
379
|
-
this.spawnFfmpeg();
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
this.process = null;
|
|
383
|
-
this.logger.warn("ffmpeg decoder: child exited", { meta: {
|
|
384
|
-
code,
|
|
385
|
-
signal,
|
|
386
|
-
outputFrames: this.outputFrames
|
|
387
|
-
} });
|
|
388
|
-
if (this.pullUrl !== null) {
|
|
389
|
-
this.clearPullRedial();
|
|
390
|
-
const timer = setTimeout(() => {
|
|
391
|
-
this.pullRedialTimer = null;
|
|
392
|
-
this.spawnFfmpeg();
|
|
393
|
-
}, PULL_REDIAL_DELAY_MS);
|
|
394
|
-
timer.unref?.();
|
|
395
|
-
this.pullRedialTimer = timer;
|
|
396
|
-
this.logger.info("ffmpeg decoder: pull-mode child exited — re-dial scheduled", { meta: {
|
|
397
|
-
code,
|
|
398
|
-
signal,
|
|
399
|
-
redialInMs: PULL_REDIAL_DELAY_MS
|
|
400
|
-
} });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/** Cancel a pending pull-mode re-dial, if armed. */
|
|
404
|
-
clearPullRedial() {
|
|
405
|
-
if (this.pullRedialTimer !== null) {
|
|
406
|
-
clearTimeout(this.pullRedialTimer);
|
|
407
|
-
this.pullRedialTimer = null;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
handleStderr(data) {
|
|
411
|
-
const text = data.toString();
|
|
412
|
-
if (this.frameSize === 0) {
|
|
413
|
-
this.stderrAccum = (this.stderrAccum + text).slice(-16384);
|
|
414
|
-
this.tryLearnGeometry();
|
|
415
|
-
}
|
|
416
|
-
const line = text.trim();
|
|
417
|
-
if (line) this.logger.debug("ffmpeg stderr", { meta: { line } });
|
|
418
|
-
}
|
|
419
|
-
/** Parse the muxer's Output block once to size the raw frames. */
|
|
420
|
-
tryLearnGeometry() {
|
|
421
|
-
const idx = this.stderrAccum.indexOf("Output #");
|
|
422
|
-
if (idx < 0) return;
|
|
423
|
-
const match = this.stderrAccum.slice(idx).match(/(\d{2,5})x(\d{2,5})/);
|
|
424
|
-
if (!match) return;
|
|
425
|
-
const width = Number(match[1]);
|
|
426
|
-
const height = Number(match[2]);
|
|
427
|
-
if (width <= 0 || height <= 0) return;
|
|
428
|
-
this.outWidth = width;
|
|
429
|
-
this.outHeight = height;
|
|
430
|
-
this.frameSize = width * height * this.channels;
|
|
431
|
-
this.logger.info("ffmpeg decoder: learned output geometry", { meta: {
|
|
432
|
-
width,
|
|
433
|
-
height,
|
|
434
|
-
channels: this.channels,
|
|
435
|
-
frameSize: this.frameSize
|
|
436
|
-
} });
|
|
437
|
-
this.splitter = new FrameStreamSplitter(this.frameSize, this.makeSlotController());
|
|
438
|
-
const pending = this.pendingStdout;
|
|
439
|
-
this.pendingStdout = [];
|
|
440
|
-
for (const chunk of pending) this.splitter.push(chunk);
|
|
441
|
-
}
|
|
442
|
-
handleStdout(chunk) {
|
|
443
|
-
if (this.splitter === null) {
|
|
444
|
-
this.pendingStdout.push(chunk);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
this.splitter.push(chunk);
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* The frame lifecycle the {@link FrameStreamSplitter} drives. `startFrame`
|
|
451
|
-
* reserves a shm slot (zero-copy: the splitter fills the slot view in place)
|
|
452
|
-
* or, for the callback sink, allocates one frame buffer; `finishFrame`
|
|
453
|
-
* publishes the filled frame; `dropFrame` accounts an FPS-skipped one.
|
|
454
|
-
*/
|
|
455
|
-
makeSlotController() {
|
|
456
|
-
return {
|
|
457
|
-
startFrame: () => {
|
|
458
|
-
if (!this.frameDropper.shouldKeep()) return null;
|
|
459
|
-
if (this.frameSink === "shm") {
|
|
460
|
-
const reserved = this.ensureFrameRingSink().beginFrame(this.outWidth, this.outHeight, this.frameFormat);
|
|
461
|
-
if (reserved === null) return null;
|
|
462
|
-
this.openSlot = reserved.slot;
|
|
463
|
-
return reserved.buffer;
|
|
464
|
-
}
|
|
465
|
-
return Buffer.allocUnsafe(this.frameSize);
|
|
466
|
-
},
|
|
467
|
-
finishFrame: (target) => {
|
|
468
|
-
this.outputFrames++;
|
|
469
|
-
if (this.frameSink === "shm") {
|
|
470
|
-
const slot = this.openSlot;
|
|
471
|
-
this.openSlot = null;
|
|
472
|
-
const sink = this.frameRingSink;
|
|
473
|
-
if (slot === null || sink === null) return;
|
|
474
|
-
const handle = sink.commitFrame(slot, {
|
|
475
|
-
width: this.outWidth,
|
|
476
|
-
height: this.outHeight,
|
|
477
|
-
format: this.frameFormat,
|
|
478
|
-
pts: performance.now(),
|
|
479
|
-
byteLength: this.frameSize
|
|
480
|
-
});
|
|
481
|
-
if (handle === null) return;
|
|
482
|
-
const delivered = {
|
|
483
|
-
handle,
|
|
484
|
-
timestamp: Date.now()
|
|
485
|
-
};
|
|
486
|
-
for (const cb of this.handleCallbacks) cb(delivered);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const frame = {
|
|
490
|
-
data: target,
|
|
491
|
-
width: this.outWidth,
|
|
492
|
-
height: this.outHeight,
|
|
493
|
-
format: this.frameFormat,
|
|
494
|
-
timestamp: Date.now()
|
|
495
|
-
};
|
|
496
|
-
for (const cb of this.frameCallbacks) cb(frame);
|
|
497
|
-
},
|
|
498
|
-
dropFrame: () => {
|
|
499
|
-
this.droppedFrames++;
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
/** Abandon a shm slot the splitter left open (respawn / teardown mid-frame),
|
|
504
|
-
* so a half-filled slot is never published. */
|
|
505
|
-
abortOpenSlot() {
|
|
506
|
-
const slot = this.openSlot;
|
|
507
|
-
if (slot === null) return;
|
|
508
|
-
this.openSlot = null;
|
|
509
|
-
this.frameRingSink?.abortFrame(slot);
|
|
510
|
-
}
|
|
511
|
-
pushPacket(packet) {
|
|
512
|
-
if (this.destroyed) return;
|
|
513
|
-
if (this.pullUrl !== null) return;
|
|
514
|
-
this.recordGop(packet);
|
|
515
|
-
this.inputPackets++;
|
|
516
|
-
if (this.process === null && !this.spawnAttempted) {
|
|
517
|
-
this.spawnFfmpeg();
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
const stdin = this.process?.stdin;
|
|
521
|
-
if (!stdin) return;
|
|
522
|
-
try {
|
|
523
|
-
stdin.write(packet.data);
|
|
524
|
-
} catch {}
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Accumulate the current GOP for the keyframe-first re-spawn replay. Reset on
|
|
528
|
-
* each keyframe; P-frames before the first keyframe are ignored (useless to
|
|
529
|
-
* prime a decoder). A pathological long/no-keyframe GOP is capped to the
|
|
530
|
-
* keyframe alone (enough to re-prime SPS/PPS).
|
|
531
|
-
*/
|
|
532
|
-
recordGop(packet) {
|
|
533
|
-
if (packet.keyframe) {
|
|
534
|
-
this.gopBuffer = [packet.data];
|
|
535
|
-
this.gopBufferBytes = packet.data.length;
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (this.gopBuffer.length === 0) return;
|
|
539
|
-
this.gopBuffer.push(packet.data);
|
|
540
|
-
this.gopBufferBytes += packet.data.length;
|
|
541
|
-
if (this.gopBufferBytes > MAX_GOP_BUFFER_BYTES) {
|
|
542
|
-
const keyframe = this.gopBuffer[0];
|
|
543
|
-
this.gopBuffer = [keyframe];
|
|
544
|
-
this.gopBufferBytes = keyframe.length;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
/** Replay the buffered GOP (keyframe-first) into a freshly-spawned push child. */
|
|
548
|
-
replayGopBuffer(child) {
|
|
549
|
-
if (this.pullUrl !== null || this.gopBuffer.length === 0) return;
|
|
550
|
-
const stdin = child.stdin;
|
|
551
|
-
if (!stdin) return;
|
|
552
|
-
for (const buf of this.gopBuffer) try {
|
|
553
|
-
stdin.write(buf);
|
|
554
|
-
} catch {
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
onFrame(callback) {
|
|
559
|
-
this.frameCallbacks.add(callback);
|
|
560
|
-
return () => {
|
|
561
|
-
this.frameCallbacks.delete(callback);
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
/** Subscribe to shared-memory frame handles — fires only in `'shm'` mode. */
|
|
565
|
-
onFrameHandle(callback) {
|
|
566
|
-
this.handleCallbacks.add(callback);
|
|
567
|
-
return () => {
|
|
568
|
-
this.handleCallbacks.delete(callback);
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
updateConfig(update) {
|
|
572
|
-
const needsRestart = update.scale !== void 0 && update.scale !== this.config.scale || update.outputFormat !== void 0 && update.outputFormat !== this.config.outputFormat || update.codec !== void 0 && update.codec !== this.config.codec;
|
|
573
|
-
this.config = {
|
|
574
|
-
...this.config,
|
|
575
|
-
...update
|
|
576
|
-
};
|
|
577
|
-
if (update.maxFps !== void 0) this.frameDropper.setMaxFps(update.maxFps);
|
|
578
|
-
if (needsRestart && this.spawnAttempted) this.spawnFfmpeg();
|
|
579
|
-
}
|
|
580
|
-
async destroy() {
|
|
581
|
-
if (this.destroyed) return;
|
|
582
|
-
this.destroyed = true;
|
|
583
|
-
this.clearPullRedial();
|
|
584
|
-
this.killFfmpeg();
|
|
585
|
-
this.abortOpenSlot();
|
|
586
|
-
this.splitter = null;
|
|
587
|
-
this.frameRingSink?.destroy();
|
|
588
|
-
this.frameRingSink = null;
|
|
589
|
-
this.frameCallbacks.clear();
|
|
590
|
-
this.handleCallbacks.clear();
|
|
591
|
-
}
|
|
592
|
-
getStats() {
|
|
593
|
-
const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
|
|
594
|
-
return {
|
|
595
|
-
inputFps: this.inputPackets / uptimeSec,
|
|
596
|
-
outputFps: this.outputFrames / uptimeSec,
|
|
597
|
-
avgDecodeTimeMs: 0,
|
|
598
|
-
droppedFrames: this.droppedFrames
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
};
|
|
602
|
-
//#endregion
|
|
603
|
-
//#region src/decoder-ffmpeg/ffmpeg-filter-probe.ts
|
|
604
|
-
/**
|
|
605
|
-
* One-shot probe of the GPU scale filters an ffmpeg build offers.
|
|
606
|
-
*
|
|
607
|
-
* The decoder runs the downscale on the GPU (`scale_vaapi` / `scale_qsv` /
|
|
608
|
-
* `scale_vt`) instead of downloading the full-resolution frame and scaling it on
|
|
609
|
-
* the CPU — a large saving on VA-API. Whether a given build even compiled those
|
|
610
|
-
* filters in is build-specific, so the addon probes `ffmpeg -filters` ONCE at
|
|
611
|
-
* init and passes the result into every session. The parse
|
|
612
|
-
* ({@link parseAvailableGpuScaleFilters}) is a pure function unit-tested in
|
|
613
|
-
* `ffmpeg-args.spec.ts`; only the spawn lives here.
|
|
614
|
-
*/
|
|
615
|
-
/** How long to wait for `ffmpeg -filters` before giving up (→ empty set). */
|
|
616
|
-
var PROBE_TIMEOUT_MS = 5e3;
|
|
617
|
-
/**
|
|
618
|
-
* Run `<ffmpegPath> -hide_banner -filters` and return the {@link GpuScaleFilter}s
|
|
619
|
-
* the build lists. Never throws — a missing / slow / failing binary resolves to
|
|
620
|
-
* an empty set, which makes every session fall back to the CPU-scale chain.
|
|
621
|
-
*/
|
|
622
|
-
function probeGpuScaleFilters(ffmpegPath, logger) {
|
|
623
|
-
return new Promise((resolve) => {
|
|
624
|
-
let stdout = "";
|
|
625
|
-
let settled = false;
|
|
626
|
-
const done = (set) => {
|
|
627
|
-
if (settled) return;
|
|
628
|
-
settled = true;
|
|
629
|
-
clearTimeout(timer);
|
|
630
|
-
resolve(set);
|
|
631
|
-
};
|
|
632
|
-
let child;
|
|
633
|
-
try {
|
|
634
|
-
child = (0, node_child_process.spawn)(ffmpegPath, ["-hide_banner", "-filters"]);
|
|
635
|
-
} catch (err) {
|
|
636
|
-
logger.warn("decoder-ffmpeg: GPU-scale filter probe spawn failed — CPU scale", { meta: { error: require_dist.errMsg(err) } });
|
|
637
|
-
done(/* @__PURE__ */ new Set());
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const timer = setTimeout(() => {
|
|
641
|
-
try {
|
|
642
|
-
child.kill("SIGKILL");
|
|
643
|
-
} catch {}
|
|
644
|
-
logger.warn("decoder-ffmpeg: GPU-scale filter probe timed out — CPU scale");
|
|
645
|
-
done(/* @__PURE__ */ new Set());
|
|
646
|
-
}, PROBE_TIMEOUT_MS);
|
|
647
|
-
timer.unref?.();
|
|
648
|
-
child.stdout?.on("data", (chunk) => {
|
|
649
|
-
stdout += chunk.toString();
|
|
650
|
-
});
|
|
651
|
-
child.on("error", (err) => {
|
|
652
|
-
logger.warn("decoder-ffmpeg: GPU-scale filter probe errored — CPU scale", { meta: { error: err.message } });
|
|
653
|
-
done(/* @__PURE__ */ new Set());
|
|
654
|
-
});
|
|
655
|
-
child.on("exit", () => {
|
|
656
|
-
done(require_ffmpeg_args.parseAvailableGpuScaleFilters(stdout));
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
//#endregion
|
|
661
|
-
//#region src/decoder-ffmpeg/addon/index.ts
|
|
662
|
-
var FRAME_BUFFER_CAPACITY = 32;
|
|
663
|
-
var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
|
|
664
|
-
...require_dist.DEFAULT_DECODER_HWACCEL_CONFIG,
|
|
665
|
-
backend: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
|
|
666
|
-
binaryPath: ""
|
|
667
|
-
};
|
|
668
|
-
/** The two selectable decoder backends, in default-first order. */
|
|
669
|
-
var DECODER_BACKEND_OPTIONS = [{
|
|
670
|
-
value: "nodeav",
|
|
671
|
-
label: "node-av (in-process FFmpeg bindings — default)"
|
|
672
|
-
}, {
|
|
673
|
-
value: "ffmpeg",
|
|
674
|
-
label: "ffmpeg (subprocess — crash-isolated fallback)"
|
|
675
|
-
}];
|
|
676
|
-
var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
|
|
677
|
-
sessions = /* @__PURE__ */ new Map();
|
|
678
|
-
frameBuffers = /* @__PURE__ */ new Map();
|
|
679
|
-
handleBuffers = /* @__PURE__ */ new Map();
|
|
680
|
-
unsubscribers = /* @__PURE__ */ new Map();
|
|
681
|
-
sessionMeta = /* @__PURE__ */ new Map();
|
|
682
|
-
frameReaders = null;
|
|
683
|
-
getFrameHits = 0;
|
|
684
|
-
getFrameMisses = 0;
|
|
685
|
-
/**
|
|
686
|
-
* The ffmpeg binary every session spawns — resolved once at init from the
|
|
687
|
-
* cluster `ffmpeg` config section (`binaryPath`), defaulting to PATH
|
|
688
|
-
* `ffmpeg`. MUST be the same binary the broker / recorder / probe use so the
|
|
689
|
-
* decode-accel probe reports for the right build.
|
|
690
|
-
*/
|
|
691
|
-
ffmpegPath = "ffmpeg";
|
|
692
|
-
/**
|
|
693
|
-
* GPU scale filters the resolved ffmpeg build offers (`scale_vaapi` /
|
|
694
|
-
* `scale_qsv` / `scale_vt`), probed once at init. A session running one of
|
|
695
|
-
* these downscales on the GPU instead of the CPU (a large VA-API saving).
|
|
696
|
-
*/
|
|
697
|
-
probedGpuScaleFilters = /* @__PURE__ */ new Set();
|
|
698
|
-
/**
|
|
699
|
-
* GPU scalers the build LISTS but that proved non-functional at runtime (a
|
|
700
|
-
* session's GPU-scale child died with no frames and self-healed to CPU scale).
|
|
701
|
-
* Excluded from later sessions so the failed spawn happens at most once per
|
|
702
|
-
* process — e.g. a videotoolbox build whose decode returns software frames.
|
|
703
|
-
*/
|
|
704
|
-
unsupportedGpuScaleFilters = /* @__PURE__ */ new Set();
|
|
705
|
-
constructor() {
|
|
706
|
-
super(DEFAULT_DECODER_FFMPEG_ADDON_CONFIG);
|
|
707
|
-
}
|
|
708
|
-
globalSettingsSchema() {
|
|
709
|
-
return this.schema({ sections: [{
|
|
710
|
-
id: "backend",
|
|
711
|
-
title: "Decoder backend",
|
|
712
|
-
tab: "decoder",
|
|
713
|
-
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.",
|
|
714
|
-
fields: [this.field({
|
|
715
|
-
type: "select",
|
|
716
|
-
key: "backend",
|
|
717
|
-
label: "Backend",
|
|
718
|
-
options: [...DECODER_BACKEND_OPTIONS],
|
|
719
|
-
default: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
|
|
720
|
-
immediate: true,
|
|
721
|
-
perNode: true
|
|
722
|
-
})]
|
|
723
|
-
}, {
|
|
724
|
-
id: "hwaccel",
|
|
725
|
-
title: "Hardware acceleration",
|
|
726
|
-
tab: "decoder",
|
|
727
|
-
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.",
|
|
728
|
-
fields: [
|
|
729
|
-
this.field({
|
|
730
|
-
type: "select",
|
|
731
|
-
key: "hwaccel",
|
|
732
|
-
label: "Preferred backend",
|
|
733
|
-
options: [...require_dist.HWACCEL_OPTIONS],
|
|
734
|
-
default: "auto",
|
|
735
|
-
immediate: true,
|
|
736
|
-
perNode: true
|
|
737
|
-
}),
|
|
738
|
-
this.field({
|
|
739
|
-
type: "text",
|
|
740
|
-
key: "probedBestHwaccel",
|
|
741
|
-
label: "Probed best",
|
|
742
|
-
description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
|
|
743
|
-
readonlyField: true,
|
|
744
|
-
default: "",
|
|
745
|
-
perNode: true,
|
|
746
|
-
actions: [{
|
|
747
|
-
action: "reprobe-hwaccel",
|
|
748
|
-
icon: "refresh-cw",
|
|
749
|
-
tooltip: "Re-probe hwaccel"
|
|
750
|
-
}]
|
|
751
|
-
}),
|
|
752
|
-
this.field({
|
|
753
|
-
type: "text",
|
|
754
|
-
key: "binaryPath",
|
|
755
|
-
label: "ffmpeg binary path (override)",
|
|
756
|
-
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.",
|
|
757
|
-
placeholder: "ffmpeg",
|
|
758
|
-
default: "",
|
|
759
|
-
perNode: true
|
|
760
|
-
})
|
|
761
|
-
]
|
|
762
|
-
}] });
|
|
763
|
-
}
|
|
764
|
-
async onInitialize() {
|
|
765
|
-
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
766
|
-
this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
767
|
-
});
|
|
768
|
-
let backend;
|
|
769
|
-
try {
|
|
770
|
-
backend = require_frame_ring_sink.resolveOwnDecoderBackend(await this.resolveGlobalStore(this.resolveLocalNodeId()));
|
|
771
|
-
} catch (err) {
|
|
772
|
-
this.ctx.logger.warn("decoder-ffmpeg: own-store backend read failed — using default", { meta: {
|
|
773
|
-
default: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
|
|
774
|
-
error: err instanceof Error ? err.message : String(err)
|
|
775
|
-
} });
|
|
776
|
-
backend = require_frame_ring_sink.DEFAULT_DECODER_BACKEND;
|
|
777
|
-
}
|
|
778
|
-
if (backend !== "ffmpeg") {
|
|
779
|
-
this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
780
|
-
return [];
|
|
781
|
-
}
|
|
782
|
-
this.ctx.logger.info("ffmpeg decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
783
|
-
this.frameReaders = new _camstack_shm_ring.FrameRingReaderCache(this.ctx.logger);
|
|
784
|
-
this.ffmpegPath = await this.resolveFfmpegBinaryPath();
|
|
785
|
-
this.probedGpuScaleFilters = await probeGpuScaleFilters(this.ffmpegPath, this.ctx.logger);
|
|
786
|
-
this.ctx.logger.info("decoder-ffmpeg: probed GPU scale filters", { meta: {
|
|
787
|
-
filters: [...this.probedGpuScaleFilters],
|
|
788
|
-
ffmpeg: this.ffmpegPath
|
|
789
|
-
} });
|
|
790
|
-
return [{
|
|
791
|
-
capability: require_dist.decoderCapability,
|
|
792
|
-
provider: this
|
|
793
|
-
}];
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Resolve the ffmpeg binary the sessions spawn.
|
|
797
|
-
*
|
|
798
|
-
* Precedence:
|
|
799
|
-
* 1. An explicit operator override in the cluster `ffmpeg` config section
|
|
800
|
-
* (`binaryPath`) — wins when set, so a hand-picked build can be forced.
|
|
801
|
-
* 2. Otherwise `ctx.deps.ensureFfmpeg()` — provisions a portable static
|
|
802
|
-
* ffmpeg into the node's deps dir when the system has none and returns its
|
|
803
|
-
* absolute path. This GUARANTEES ffmpeg on every node (hub, agent, Mac),
|
|
804
|
-
* so there is no missing-ffmpeg case and no node-av fallback.
|
|
805
|
-
*
|
|
806
|
-
* `ensureFfmpeg()` is single-flighted, so we resolve once at init and reuse
|
|
807
|
-
* the path for every session. If it throws (e.g. a download failure) we log a
|
|
808
|
-
* loud ERROR and fall back to PATH `ffmpeg`; the addon still registers the
|
|
809
|
-
* decoder cap and a later session spawn re-runs the ensure.
|
|
810
|
-
*/
|
|
811
|
-
async resolveFfmpegBinaryPath() {
|
|
812
|
-
const override = this.config.binaryPath;
|
|
813
|
-
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
814
|
-
try {
|
|
815
|
-
return await this.ctx.deps.ensureFfmpeg();
|
|
816
|
-
} catch (err) {
|
|
817
|
-
this.ctx.logger.error("decoder-ffmpeg: ensureFfmpeg() failed to provision ffmpeg — falling back to PATH \"ffmpeg\"; a later session will retry", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
818
|
-
return "ffmpeg";
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Resolve the ORDERED decode-hwaccel chain for a new session — NEVER
|
|
823
|
-
* hardcoded. The session tries the chain best-first and only falls to
|
|
824
|
-
* software once every hardware option is exhausted.
|
|
825
|
-
*
|
|
826
|
-
* - `'none'` → `[]` (pure software).
|
|
827
|
-
* - explicit backend → `[<backend>]` (forced; the session still falls to
|
|
828
|
-
* software if that one child fails).
|
|
829
|
-
* - `'auto'` → the LOCAL node's preferred order from `ctx.kernel.hwaccel` —
|
|
830
|
-
* the in-process resolver every forked child (hub / worker / agent) is
|
|
831
|
-
* given, which derives the backend from `os.platform()` + a GPU-vendor probe
|
|
832
|
-
* (darwin → `videotoolbox`; Intel → `qsv,vaapi`; NVIDIA → `cuda,nvdec`).
|
|
833
|
-
* Ranked via {@link rankDecodeHwAccels} (vaapi ABOVE qsv, since qsv is broken
|
|
834
|
-
* on the Intel stack while vaapi works). On a resolver error → `[]`
|
|
835
|
-
* (software).
|
|
836
|
-
*
|
|
837
|
-
* Two things this deliberately does NOT do:
|
|
838
|
-
* - It does NOT call the `platform-probe` singleton cap. Its per-node
|
|
839
|
-
* `nodePin` does not survive a forked agent child's link chain, so a pinned
|
|
840
|
-
* read resolves to the HUB's elected singleton and returns the hub's
|
|
841
|
-
* hardware — a Mac decoder was handed the hub's Intel `vaapi/qsv/...` chain
|
|
842
|
-
* (no `videotoolbox`) and fell all the way to software h265.
|
|
843
|
-
* - It does NOT probe `ffmpeg -hwaccels` (nor node-av) to intersect the list.
|
|
844
|
-
* The accelerator is deterministic from the host, which the kernel resolver
|
|
845
|
-
* already knows in-process — no subprocess, no cross-process round-trip. The
|
|
846
|
-
* rare case where the resolved backend is not usable by the actual ffmpeg
|
|
847
|
-
* build is covered by the session's per-child runtime fallback (advance to
|
|
848
|
-
* the next chain entry, else software). This mirrors how Scrypted resolves
|
|
849
|
-
* hwaccel (static `os.platform()` map + try-then-fall-back), not a native
|
|
850
|
-
* capability enumeration.
|
|
851
|
-
*
|
|
852
|
-
* macOS special-case: the resolved `videotoolbox` is emitted as `-hwaccel
|
|
853
|
-
* auto`, NOT explicit `-hwaccel videotoolbox`. Explicit videotoolbox tears the
|
|
854
|
-
* ffmpeg child down under MULTIPLE concurrent decode streams (documented by
|
|
855
|
-
* Scrypted NVR's ffmpeg path; our Mac decodes many cameras at once — exactly
|
|
856
|
-
* that case). `auto` lets ffmpeg pick VideoToolbox internally and auto-download
|
|
857
|
-
* frames to software (no GPU surface to dispose, no `scale_vt`). The in-process
|
|
858
|
-
* node-av decoder (`decoder-nodeav`) still uses explicit videotoolbox — this
|
|
859
|
-
* substitution is specific to the ffmpeg-subprocess decode.
|
|
860
|
-
*/
|
|
861
|
-
async resolveDecodeHwaccelChain() {
|
|
862
|
-
const pref = this.config.hwaccel;
|
|
863
|
-
if (pref === "none") return [];
|
|
864
|
-
if (pref !== "auto") return [pref];
|
|
865
|
-
try {
|
|
866
|
-
const resolver = this.ctx.kernel.hwaccel;
|
|
867
|
-
if (!resolver) {
|
|
868
|
-
this.ctx.logger.warn("ffmpeg: no local kernel hwaccel resolver — software decode");
|
|
869
|
-
return [];
|
|
870
|
-
}
|
|
871
|
-
const chain = require_ffmpeg_args.rankDecodeHwAccels((await resolver.resolve(null)).preferred, []);
|
|
872
|
-
if (process.platform === "darwin") return chain.map((method) => method === "videotoolbox" ? "auto" : method);
|
|
873
|
-
return chain;
|
|
874
|
-
} catch (err) {
|
|
875
|
-
this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
876
|
-
return [];
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
async reprobeHwaccel() {
|
|
880
|
-
const resolver = this.ctx.kernel.hwaccel;
|
|
881
|
-
if (!resolver) {
|
|
882
|
-
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
|
|
883
|
-
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
884
|
-
return { backend: "none" };
|
|
885
|
-
}
|
|
886
|
-
try {
|
|
887
|
-
const res = await resolver.resolve();
|
|
888
|
-
const backend = require_ffmpeg_args.rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
|
|
889
|
-
await this.updateGlobalSettings({ probedBestHwaccel: backend });
|
|
890
|
-
this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
|
|
891
|
-
backend,
|
|
892
|
-
rationale: res.rationale,
|
|
893
|
-
preferred: res.preferred
|
|
894
|
-
} });
|
|
895
|
-
return { backend };
|
|
896
|
-
} catch (err) {
|
|
897
|
-
this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
898
|
-
await this.updateGlobalSettings({ probedBestHwaccel: "none" });
|
|
899
|
-
return { backend: "none" };
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
async supportsCodec(input) {
|
|
903
|
-
return [
|
|
904
|
-
"h264",
|
|
905
|
-
"h265",
|
|
906
|
-
"hevc"
|
|
907
|
-
].includes(input.codec.toLowerCase());
|
|
908
|
-
}
|
|
909
|
-
async getInfo() {
|
|
910
|
-
return {
|
|
911
|
-
id: "decoder-ffmpeg",
|
|
912
|
-
name: "Decoder (ffmpeg)",
|
|
913
|
-
isPullMode: true,
|
|
914
|
-
priority: 20
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* The cluster node id of this decoder — stamped into session-owned
|
|
919
|
-
* `FrameHandle`s so a downstream consumer routes `getFrame` to the node that
|
|
920
|
-
* holds the shm ring. Mirrors `decoder-nodeav`.
|
|
921
|
-
*/
|
|
922
|
-
resolveLocalNodeId() {
|
|
923
|
-
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
924
|
-
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
925
|
-
}
|
|
926
|
-
async createSession(config) {
|
|
927
|
-
const sessionId = (0, node_crypto.randomUUID)();
|
|
928
|
-
const { frameSink } = config;
|
|
929
|
-
const nodeId = this.resolveLocalNodeId();
|
|
930
|
-
const hwaccelChain = await this.resolveDecodeHwaccelChain();
|
|
931
|
-
const availableGpuScaleFilters = new Set([...this.probedGpuScaleFilters].filter((f) => !this.unsupportedGpuScaleFilters.has(f)));
|
|
932
|
-
const session = new FfmpegDecoderSession(config, this.ctx.logger, {
|
|
933
|
-
hwaccelChain,
|
|
934
|
-
ffmpegPath: this.ffmpegPath,
|
|
935
|
-
frameSink,
|
|
936
|
-
nodeId,
|
|
937
|
-
availableGpuScaleFilters,
|
|
938
|
-
onGpuScaleUnsupported: (filter) => {
|
|
939
|
-
if (this.unsupportedGpuScaleFilters.has(filter)) return;
|
|
940
|
-
this.unsupportedGpuScaleFilters.add(filter);
|
|
941
|
-
this.ctx.logger.warn("decoder-ffmpeg: GPU scaler non-functional on this build — future sessions use CPU scale", { meta: { filter } });
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
const unsub = frameSink === "shm" ? this.wireShmSink(sessionId, session) : this.wireCallbackSink(sessionId, session);
|
|
945
|
-
this.sessions.set(sessionId, session);
|
|
946
|
-
this.unsubscribers.set(sessionId, unsub);
|
|
947
|
-
this.sessionMeta.set(sessionId, {
|
|
948
|
-
codec: config.codec,
|
|
949
|
-
outputFormat: config.outputFormat,
|
|
950
|
-
createdAtMs: Date.now()
|
|
951
|
-
});
|
|
952
|
-
this.ctx.logger.info("ffmpeg: created session", { meta: {
|
|
953
|
-
sessionId,
|
|
954
|
-
codec: config.codec,
|
|
955
|
-
hwaccelChain,
|
|
956
|
-
frameSink,
|
|
957
|
-
nodeId
|
|
958
|
-
} });
|
|
959
|
-
return {
|
|
960
|
-
sessionId,
|
|
961
|
-
nodeId
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
wireCallbackSink(sessionId, session) {
|
|
965
|
-
const ringBuffer = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
966
|
-
this.frameBuffers.set(sessionId, ringBuffer);
|
|
967
|
-
return session.onFrame((frame) => {
|
|
968
|
-
const { format } = frame;
|
|
969
|
-
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
970
|
-
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
971
|
-
new Uint8Array(arrayBuf).set(frame.data);
|
|
972
|
-
const capFrame = {
|
|
973
|
-
data: new Uint8Array(arrayBuf),
|
|
974
|
-
width: frame.width,
|
|
975
|
-
height: frame.height,
|
|
976
|
-
format,
|
|
977
|
-
timestamp: frame.timestamp
|
|
978
|
-
};
|
|
979
|
-
ringBuffer.push(capFrame);
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
wireShmSink(sessionId, session) {
|
|
983
|
-
const handleRing = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
984
|
-
this.handleBuffers.set(sessionId, handleRing);
|
|
985
|
-
return session.onFrameHandle((frame) => {
|
|
986
|
-
handleRing.push(frame.handle);
|
|
987
|
-
});
|
|
988
|
-
}
|
|
989
|
-
async destroySession(input) {
|
|
990
|
-
const { sessionId } = input;
|
|
991
|
-
const session = this.sessions.get(sessionId);
|
|
992
|
-
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`);
|
|
993
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
994
|
-
if (unsub) unsub();
|
|
995
|
-
this.frameBuffers.get(sessionId)?.cancel();
|
|
996
|
-
this.handleBuffers.get(sessionId)?.cancel();
|
|
997
|
-
await session.destroy();
|
|
998
|
-
this.sessions.delete(sessionId);
|
|
999
|
-
this.frameBuffers.delete(sessionId);
|
|
1000
|
-
this.handleBuffers.delete(sessionId);
|
|
1001
|
-
this.unsubscribers.delete(sessionId);
|
|
1002
|
-
this.sessionMeta.delete(sessionId);
|
|
1003
|
-
this.ctx.logger.info("ffmpeg: destroyed session", { meta: { sessionId } });
|
|
1004
|
-
}
|
|
1005
|
-
async listActiveSessions() {
|
|
1006
|
-
const out = [];
|
|
1007
|
-
for (const [sessionId, meta] of this.sessionMeta) out.push({
|
|
1008
|
-
sessionId,
|
|
1009
|
-
codec: meta.codec,
|
|
1010
|
-
outputFormat: meta.outputFormat,
|
|
1011
|
-
createdAtMs: meta.createdAtMs
|
|
1012
|
-
});
|
|
1013
|
-
return out;
|
|
1014
|
-
}
|
|
1015
|
-
async pushPacket(input) {
|
|
1016
|
-
const session = this.sessions.get(input.sessionId);
|
|
1017
|
-
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1018
|
-
const rawData = input.packet.data;
|
|
1019
|
-
const data = Buffer.isBuffer(rawData) ? rawData : rawData instanceof Uint8Array ? Buffer.from(rawData.buffer, rawData.byteOffset, rawData.byteLength) : Buffer.from(rawData);
|
|
1020
|
-
session.pushPacket({
|
|
1021
|
-
...input.packet,
|
|
1022
|
-
data
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
async openStream(input) {
|
|
1026
|
-
const session = this.sessions.get(input.sessionId);
|
|
1027
|
-
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1028
|
-
await session.openStream(input.url);
|
|
1029
|
-
}
|
|
1030
|
-
async pullFrames(input) {
|
|
1031
|
-
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1032
|
-
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
1033
|
-
if (!ringBuffer) return [];
|
|
1034
|
-
if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
|
|
1035
|
-
return ringBuffer.drain(input.maxCount);
|
|
1036
|
-
}
|
|
1037
|
-
async pullHandles(input) {
|
|
1038
|
-
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1039
|
-
const handleRing = this.handleBuffers.get(input.sessionId);
|
|
1040
|
-
if (!handleRing) return [];
|
|
1041
|
-
if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
|
|
1042
|
-
return handleRing.drain(input.maxCount);
|
|
1043
|
-
}
|
|
1044
|
-
async updateConfig(input) {
|
|
1045
|
-
const session = this.sessions.get(input.sessionId);
|
|
1046
|
-
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1047
|
-
session.updateConfig(input.config);
|
|
1048
|
-
}
|
|
1049
|
-
async getStats(input) {
|
|
1050
|
-
const session = this.sessions.get(input.sessionId);
|
|
1051
|
-
if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
|
|
1052
|
-
return session.getStats();
|
|
1053
|
-
}
|
|
1054
|
-
async getFrame(input) {
|
|
1055
|
-
const frame = this.frameReaders?.read(input.handle) ?? null;
|
|
1056
|
-
if (!frame) {
|
|
1057
|
-
this.getFrameMisses += 1;
|
|
1058
|
-
return null;
|
|
1059
|
-
}
|
|
1060
|
-
this.getFrameHits += 1;
|
|
1061
|
-
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
1062
|
-
new Uint8Array(arrayBuf).set(frame.data);
|
|
1063
|
-
return {
|
|
1064
|
-
data: new Uint8Array(arrayBuf),
|
|
1065
|
-
width: frame.width,
|
|
1066
|
-
height: frame.height,
|
|
1067
|
-
format: frame.format,
|
|
1068
|
-
timestamp: frame.timestamp
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
async getShmStats(input) {
|
|
1072
|
-
const stats = this.sessions.get(input.sessionId)?.frameRingSinkOrNull?.getShmStats() ?? null;
|
|
1073
|
-
if (!stats) return null;
|
|
1074
|
-
return {
|
|
1075
|
-
sessionId: input.sessionId,
|
|
1076
|
-
...stats,
|
|
1077
|
-
budgetMb: require_frame_ring_sink.RING_BUDGET_MB,
|
|
1078
|
-
getFrameHits: this.getFrameHits,
|
|
1079
|
-
getFrameMisses: this.getFrameMisses
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
async onShutdown() {
|
|
1083
|
-
this.ctx.logger.info("ffmpeg decoder addon shutdown — destroying all sessions");
|
|
1084
|
-
const destroyPromises = [];
|
|
1085
|
-
for (const [sessionId, session] of this.sessions) {
|
|
1086
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
1087
|
-
if (unsub) unsub();
|
|
1088
|
-
destroyPromises.push(session.destroy());
|
|
1089
|
-
}
|
|
1090
|
-
await Promise.all(destroyPromises);
|
|
1091
|
-
this.sessions.clear();
|
|
1092
|
-
this.frameBuffers.clear();
|
|
1093
|
-
this.handleBuffers.clear();
|
|
1094
|
-
this.unsubscribers.clear();
|
|
1095
|
-
this.sessionMeta.clear();
|
|
1096
|
-
this.frameReaders?.close();
|
|
1097
|
-
this.frameReaders = null;
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
//#endregion
|
|
1101
|
-
exports.DECODE_HWACCEL_RANK = require_ffmpeg_args.DECODE_HWACCEL_RANK;
|
|
1102
|
-
exports.DEFAULT_VAAPI_RENDER_DEVICE = require_ffmpeg_args.DEFAULT_VAAPI_RENDER_DEVICE;
|
|
1103
|
-
exports.DecoderFfmpegAddon = DecoderFfmpegAddon;
|
|
1104
|
-
exports.FfmpegDecoderSession = FfmpegDecoderSession;
|
|
1105
|
-
exports.buildFfmpegDecodeArgs = require_ffmpeg_args.buildFfmpegDecodeArgs;
|
|
1106
|
-
exports.channelsForPixel = require_ffmpeg_args.channelsForPixel;
|
|
1107
|
-
exports.codecToInputFormat = require_ffmpeg_args.codecToInputFormat;
|
|
1108
|
-
exports.computeOutputGeometry = require_ffmpeg_args.computeOutputGeometry;
|
|
1109
|
-
exports.default = DecoderFfmpegAddon;
|
|
1110
|
-
exports.maxDecodeWidth = require_ffmpeg_args.maxDecodeWidth;
|
|
1111
|
-
exports.parseFfmpegOutputDims = require_ffmpeg_args.parseFfmpegOutputDims;
|
|
1112
|
-
exports.pickDecodeHwAccel = require_ffmpeg_args.pickDecodeHwAccel;
|
|
1113
|
-
exports.rankDecodeHwAccels = require_ffmpeg_args.rankDecodeHwAccels;
|
|
1114
|
-
exports.rawPixelForFormat = require_ffmpeg_args.rawPixelForFormat;
|