node-av 6.2.0-beta.3 → 6.2.0-beta.31
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/README.md +13 -8
- package/dist/api/audio-frame-buffer.d.ts +438 -7
- package/dist/api/audio-frame-buffer.js +792 -52
- package/dist/api/audio-frame-buffer.js.map +1 -1
- package/dist/api/bitstream-filter.d.ts +82 -4
- package/dist/api/bitstream-filter.js +197 -100
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +5 -0
- package/dist/api/decoder.js +13 -6
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/demuxer.d.ts +29 -4
- package/dist/api/demuxer.js +209 -60
- package/dist/api/demuxer.js.map +1 -1
- package/dist/api/encoder-pool.d.ts +43 -1
- package/dist/api/encoder-pool.js +121 -9
- package/dist/api/encoder-pool.js.map +1 -1
- package/dist/api/encoder.d.ts +58 -2
- package/dist/api/encoder.js +173 -57
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter.js +15 -6
- package/dist/api/filter.js.map +1 -1
- package/dist/api/fmp4-stream.d.ts +327 -8
- package/dist/api/fmp4-stream.js +663 -123
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/hardware.d.ts +2 -0
- package/dist/api/hardware.js +25 -4
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +3 -2
- package/dist/api/io-stream.js +77 -5
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/muxer.d.ts +289 -7
- package/dist/api/muxer.js +696 -109
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/pipeline.d.ts +24 -0
- package/dist/api/pipeline.js +76 -5
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +77 -8
- package/dist/api/rtp-stream.js +316 -88
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +124 -21
- package/dist/api/scaler.js +347 -114
- package/dist/api/scaler.js.map +1 -1
- package/dist/api/utilities/async-queue.d.ts +3 -0
- package/dist/api/utilities/async-queue.js +8 -0
- package/dist/api/utilities/async-queue.js.map +1 -1
- package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
- package/dist/api/utilities/packet-age-watchdog.js +594 -0
- package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +2 -0
- package/dist/api/webrtc-stream.js +46 -12
- package/dist/api/webrtc-stream.js.map +1 -1
- package/dist/api/whisper.d.ts +2 -0
- package/dist/api/whisper.js +63 -23
- package/dist/api/whisper.js.map +1 -1
- package/dist/constants/constants.d.ts +1 -0
- package/dist/constants/constants.js +1 -0
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/filter-options.d.ts +2 -0
- package/dist/ffmpeg/install.js +104 -26
- package/dist/ffmpeg/install.js.map +1 -1
- package/dist/ffmpeg/utils.js +7 -4
- package/dist/ffmpeg/utils.js.map +1 -1
- package/dist/lib/binding.d.ts +10 -1
- package/dist/lib/binding.js +25 -16
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/bitstream-filter-context.d.ts +2 -0
- package/dist/lib/bitstream-filter-context.js +2 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -1
- package/dist/lib/codec-context.d.ts +0 -2
- package/dist/lib/codec-context.js +4 -22
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/filter-context.d.ts +3 -1
- package/dist/lib/filter-context.js +5 -11
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph-segment.d.ts +3 -2
- package/dist/lib/filter-graph-segment.js +3 -2
- package/dist/lib/filter-graph-segment.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +4 -4
- package/dist/lib/filter-graph.js +4 -4
- package/dist/lib/filter-inout.d.ts +4 -0
- package/dist/lib/filter-inout.js +4 -0
- package/dist/lib/filter-inout.js.map +1 -1
- package/dist/lib/format-context.d.ts +72 -7
- package/dist/lib/format-context.js +80 -22
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +17 -13
- package/dist/lib/frame.js +24 -23
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-frames-context.d.ts +0 -1
- package/dist/lib/hardware-frames-context.js +2 -8
- package/dist/lib/hardware-frames-context.js.map +1 -1
- package/dist/lib/io-context.d.ts +4 -2
- package/dist/lib/io-context.js +4 -2
- package/dist/lib/io-context.js.map +1 -1
- package/dist/lib/native-types.d.ts +7 -5
- package/dist/lib/option.d.ts +26 -0
- package/dist/lib/option.js +42 -18
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/packet.d.ts +5 -0
- package/dist/lib/packet.js +5 -0
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/stream.d.ts +8 -4
- package/dist/lib/stream.js +22 -24
- package/dist/lib/stream.js.map +1 -1
- package/install/check.js +40 -79
- package/package.json +45 -34
- package/build_mac_local.sh +0 -69
package/dist/api/scaler.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { AV_CODEC_FLAG_QSCALE, AV_HWDEVICE_TYPE_OPENCL,
|
|
1
|
+
import { AV_CODEC_FLAG_QSCALE, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_OPENCL, AV_HWDEVICE_TYPE_QSV, AV_HWDEVICE_TYPE_VAAPI, AV_PIX_FMT_GRAY8, AV_PIX_FMT_NV12, AV_PIX_FMT_RGB24, AV_PIX_FMT_RGBA, AV_PIX_FMT_YUV420P, AVCOL_RANGE_JPEG, FF_QP2LAMBDA, SWS_BILINEAR, } from '../constants/constants.js';
|
|
2
2
|
import { FF_ENCODER_MJPEG, FF_ENCODER_PNG } from '../constants/encoders.js';
|
|
3
3
|
import { bindings } from '../lib/binding.js';
|
|
4
4
|
import { FFmpegError } from '../lib/error.js';
|
|
5
5
|
import { Frame } from '../lib/frame.js';
|
|
6
|
-
import { EncoderPool } from './encoder-pool.js';
|
|
6
|
+
import { EncoderPool, Mutex } from './encoder-pool.js';
|
|
7
7
|
import { FilterPreset } from './filter-presets.js';
|
|
8
8
|
import { FilterAPI } from './filter.js';
|
|
9
9
|
const FORMAT_TO_PIXFMT = {
|
|
@@ -23,6 +23,10 @@ const FORMAT_TO_PIXFMT = {
|
|
|
23
23
|
* Software frames are scaled with swscale; hardware frames are cropped, scaled,
|
|
24
24
|
* and converted on the GPU with only the small result downloaded.
|
|
25
25
|
*
|
|
26
|
+
* Safe for concurrent use: calls that share a cached graph or pooled encoder are
|
|
27
|
+
* serialized on that resource, while distinct output configurations run fully in
|
|
28
|
+
* parallel, and cache eviction never disposes a resource that is still in flight.
|
|
29
|
+
*
|
|
26
30
|
* @example
|
|
27
31
|
* ```typescript
|
|
28
32
|
* import { Scaler } from 'node-av/api';
|
|
@@ -41,13 +45,14 @@ const FORMAT_TO_PIXFMT = {
|
|
|
41
45
|
*/
|
|
42
46
|
export class Scaler {
|
|
43
47
|
native;
|
|
48
|
+
syncNative;
|
|
49
|
+
nativeLock = new Mutex();
|
|
44
50
|
hardware;
|
|
45
51
|
flags;
|
|
46
52
|
maxCacheSize;
|
|
47
53
|
graphs = new Map();
|
|
48
54
|
jpegPool;
|
|
49
55
|
pngPool;
|
|
50
|
-
downloadFrame;
|
|
51
56
|
disposed = false;
|
|
52
57
|
/**
|
|
53
58
|
* Create a new scaler.
|
|
@@ -73,15 +78,21 @@ export class Scaler {
|
|
|
73
78
|
*
|
|
74
79
|
* @param frame - Source frame (software or hardware; hardware frames are processed on the GPU and downloaded)
|
|
75
80
|
*
|
|
76
|
-
* @param options - Crop, resize, and
|
|
81
|
+
* @param options - Crop, resize, format, and output buffer options
|
|
77
82
|
*
|
|
78
|
-
* @returns Tightly packed pixel data
|
|
83
|
+
* @returns Tightly packed pixel data (a view of `options.output` when given)
|
|
79
84
|
*
|
|
80
85
|
* @throws {FFmpegError} If scaling fails
|
|
81
86
|
*
|
|
87
|
+
* @throws {TypeError} If `options.output` is not a Buffer or is smaller than the result
|
|
88
|
+
*
|
|
82
89
|
* @example
|
|
83
90
|
* ```typescript
|
|
84
91
|
* const gray = await scaler.toBuffer(frame, { resize: { width: 320, height: 180 }, format: 'gray' });
|
|
92
|
+
*
|
|
93
|
+
* // Reuse one buffer across frames
|
|
94
|
+
* const reused = Buffer.allocUnsafe(320 * 180);
|
|
95
|
+
* await scaler.toBuffer(frame, { resize: { width: 320, height: 180 }, format: 'gray', output: reused });
|
|
85
96
|
* ```
|
|
86
97
|
*
|
|
87
98
|
* @see {@link toBufferSync} For synchronous version
|
|
@@ -90,6 +101,10 @@ export class Scaler {
|
|
|
90
101
|
if (this.disposed) {
|
|
91
102
|
throw new Error('Scaler has been disposed');
|
|
92
103
|
}
|
|
104
|
+
// The native side accepts any Uint8Array, but the result would then not be a Buffer
|
|
105
|
+
if (options.output !== undefined && !Buffer.isBuffer(options.output)) {
|
|
106
|
+
throw new TypeError('output must be a Buffer');
|
|
107
|
+
}
|
|
93
108
|
if (frame.isHwFrame()) {
|
|
94
109
|
// With a hardware context, crop/scale/convert on the GPU and download only
|
|
95
110
|
// the small result. Without one, fall back to downloading the full frame
|
|
@@ -97,15 +112,21 @@ export class Scaler {
|
|
|
97
112
|
if (this.hardware) {
|
|
98
113
|
const out = await this.toFrame(frame, options.crop, options.resize, options.format ?? 'nv12');
|
|
99
114
|
try {
|
|
100
|
-
return out.toBuffer();
|
|
115
|
+
return out.toBuffer(options.output);
|
|
101
116
|
}
|
|
102
117
|
finally {
|
|
103
118
|
out.free();
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
|
-
|
|
121
|
+
const sw = await this.downloadToSoftware(frame);
|
|
122
|
+
try {
|
|
123
|
+
return await this.processNative(sw, options);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
sw.free();
|
|
127
|
+
}
|
|
107
128
|
}
|
|
108
|
-
return await this.
|
|
129
|
+
return await this.processNative(frame, options);
|
|
109
130
|
}
|
|
110
131
|
/**
|
|
111
132
|
* Scale, crop, and/or convert a frame and return the raw pixel data.
|
|
@@ -113,12 +134,14 @@ export class Scaler {
|
|
|
113
134
|
*
|
|
114
135
|
* @param frame - Source frame (software or hardware; hardware frames are processed on the GPU and downloaded)
|
|
115
136
|
*
|
|
116
|
-
* @param options - Crop, resize, and
|
|
137
|
+
* @param options - Crop, resize, format, and output buffer options
|
|
117
138
|
*
|
|
118
|
-
* @returns Tightly packed pixel data
|
|
139
|
+
* @returns Tightly packed pixel data (a view of `options.output` when given)
|
|
119
140
|
*
|
|
120
141
|
* @throws {FFmpegError} If scaling fails
|
|
121
142
|
*
|
|
143
|
+
* @throws {TypeError} If `options.output` is not a Buffer or is smaller than the result
|
|
144
|
+
*
|
|
122
145
|
* @example
|
|
123
146
|
* ```typescript
|
|
124
147
|
* const gray = scaler.toBufferSync(frame, { resize: { width: 320, height: 180 }, format: 'gray' });
|
|
@@ -130,19 +153,28 @@ export class Scaler {
|
|
|
130
153
|
if (this.disposed) {
|
|
131
154
|
throw new Error('Scaler has been disposed');
|
|
132
155
|
}
|
|
156
|
+
if (options.output !== undefined && !Buffer.isBuffer(options.output)) {
|
|
157
|
+
throw new TypeError('output must be a Buffer');
|
|
158
|
+
}
|
|
133
159
|
if (frame.isHwFrame()) {
|
|
134
160
|
if (this.hardware) {
|
|
135
161
|
const out = this.toFrameSync(frame, options.crop, options.resize, options.format ?? 'nv12');
|
|
136
162
|
try {
|
|
137
|
-
return out.toBuffer();
|
|
163
|
+
return out.toBuffer(options.output);
|
|
138
164
|
}
|
|
139
165
|
finally {
|
|
140
166
|
out.free();
|
|
141
167
|
}
|
|
142
168
|
}
|
|
143
|
-
|
|
169
|
+
const sw = this.downloadToSoftwareSync(frame);
|
|
170
|
+
try {
|
|
171
|
+
return this.getSyncNative().processSync(sw.getNative(), options);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
sw.free();
|
|
175
|
+
}
|
|
144
176
|
}
|
|
145
|
-
return this.
|
|
177
|
+
return this.getSyncNative().processSync(frame.getNative(), options);
|
|
146
178
|
}
|
|
147
179
|
/**
|
|
148
180
|
* Scale/crop a frame and encode it to a JPEG image.
|
|
@@ -172,16 +204,21 @@ export class Scaler {
|
|
|
172
204
|
throw new Error('Scaler has been disposed');
|
|
173
205
|
}
|
|
174
206
|
const quality = options.quality ?? 90;
|
|
175
|
-
const
|
|
176
|
-
// mjpeg wants planar YUV; tag it full-range so colors are correct without the
|
|
177
|
-
// deprecated yuvj* formats.
|
|
178
|
-
const out = await this.toFrame(src, options.crop, options.resize, 'yuv420p');
|
|
207
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? await this.downloadToSoftware(frame) : null;
|
|
179
208
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
// mjpeg wants planar YUV; tag it full-range so colors are correct without the
|
|
210
|
+
// deprecated yuvj* formats.
|
|
211
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
212
|
+
try {
|
|
213
|
+
this.prepareJpegFrame(out, quality);
|
|
214
|
+
return await this.getJpegPool().encode(out);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
out.free();
|
|
218
|
+
}
|
|
182
219
|
}
|
|
183
220
|
finally {
|
|
184
|
-
|
|
221
|
+
downloaded?.free();
|
|
185
222
|
}
|
|
186
223
|
}
|
|
187
224
|
/**
|
|
@@ -208,14 +245,19 @@ export class Scaler {
|
|
|
208
245
|
throw new Error('Scaler has been disposed');
|
|
209
246
|
}
|
|
210
247
|
const quality = options.quality ?? 90;
|
|
211
|
-
const
|
|
212
|
-
const out = this.toFrameSync(src, options.crop, options.resize, 'yuv420p');
|
|
248
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? this.downloadToSoftwareSync(frame) : null;
|
|
213
249
|
try {
|
|
214
|
-
this.
|
|
215
|
-
|
|
250
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
251
|
+
try {
|
|
252
|
+
this.prepareJpegFrame(out, quality);
|
|
253
|
+
return this.getJpegPool().encodeSync(out);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
out.free();
|
|
257
|
+
}
|
|
216
258
|
}
|
|
217
259
|
finally {
|
|
218
|
-
|
|
260
|
+
downloaded?.free();
|
|
219
261
|
}
|
|
220
262
|
}
|
|
221
263
|
/**
|
|
@@ -245,13 +287,18 @@ export class Scaler {
|
|
|
245
287
|
throw new Error('Scaler has been disposed');
|
|
246
288
|
}
|
|
247
289
|
const format = options.format ?? 'rgb';
|
|
248
|
-
const
|
|
249
|
-
const out = await this.toFrame(src, options.crop, options.resize, format);
|
|
290
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? await this.downloadToSoftware(frame) : null;
|
|
250
291
|
try {
|
|
251
|
-
|
|
292
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, format);
|
|
293
|
+
try {
|
|
294
|
+
return await this.getPngPool().encode(out);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
out.free();
|
|
298
|
+
}
|
|
252
299
|
}
|
|
253
300
|
finally {
|
|
254
|
-
|
|
301
|
+
downloaded?.free();
|
|
255
302
|
}
|
|
256
303
|
}
|
|
257
304
|
/**
|
|
@@ -278,13 +325,18 @@ export class Scaler {
|
|
|
278
325
|
throw new Error('Scaler has been disposed');
|
|
279
326
|
}
|
|
280
327
|
const format = options.format ?? 'rgb';
|
|
281
|
-
const
|
|
282
|
-
const out = this.toFrameSync(src, options.crop, options.resize, format);
|
|
328
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? this.downloadToSoftwareSync(frame) : null;
|
|
283
329
|
try {
|
|
284
|
-
|
|
330
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, format);
|
|
331
|
+
try {
|
|
332
|
+
return this.getPngPool().encodeSync(out);
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
out.free();
|
|
336
|
+
}
|
|
285
337
|
}
|
|
286
338
|
finally {
|
|
287
|
-
|
|
339
|
+
downloaded?.free();
|
|
288
340
|
}
|
|
289
341
|
}
|
|
290
342
|
/**
|
|
@@ -301,34 +353,41 @@ export class Scaler {
|
|
|
301
353
|
}
|
|
302
354
|
this.disposed = true;
|
|
303
355
|
this.native.close();
|
|
356
|
+
this.syncNative?.close();
|
|
357
|
+
this.syncNative = undefined;
|
|
358
|
+
// In-use graphs are retired, not disposed: the last in-flight scale disposes
|
|
359
|
+
// them on release, so close() during concurrent calls never crashes them.
|
|
304
360
|
for (const entry of this.graphs.values()) {
|
|
305
|
-
|
|
361
|
+
this.retireEntry(entry);
|
|
306
362
|
}
|
|
307
363
|
this.graphs.clear();
|
|
308
364
|
this.jpegPool?.close();
|
|
309
365
|
this.jpegPool = undefined;
|
|
310
366
|
this.pngPool?.close();
|
|
311
367
|
this.pngPool = undefined;
|
|
312
|
-
this.downloadFrame?.free();
|
|
313
|
-
this.downloadFrame = undefined;
|
|
314
368
|
}
|
|
315
369
|
/**
|
|
316
370
|
* Download a hardware frame to system memory (no hardware context available).
|
|
317
371
|
*
|
|
318
|
-
*
|
|
319
|
-
* selects a supported software
|
|
372
|
+
* Allocates a fresh target per call - concurrent downloads must not share a
|
|
373
|
+
* staging frame. The frame's own hwframe context selects a supported software
|
|
374
|
+
* format. The caller frees the returned frame.
|
|
320
375
|
*
|
|
321
376
|
* @param frame - Hardware source frame
|
|
322
377
|
*
|
|
323
|
-
* @returns Software frame
|
|
378
|
+
* @returns Software frame (caller frees)
|
|
324
379
|
*
|
|
325
380
|
* @throws {FFmpegError} If the transfer fails
|
|
326
381
|
*
|
|
327
382
|
* @internal
|
|
328
383
|
*/
|
|
329
384
|
async downloadToSoftware(frame) {
|
|
330
|
-
const dst =
|
|
385
|
+
const dst = new Frame();
|
|
386
|
+
dst.alloc();
|
|
331
387
|
const ret = await frame.hwframeTransferData(dst, 0);
|
|
388
|
+
if (ret < 0) {
|
|
389
|
+
dst.free();
|
|
390
|
+
}
|
|
332
391
|
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
333
392
|
dst.copyProps(frame);
|
|
334
393
|
return dst;
|
|
@@ -338,35 +397,56 @@ export class Scaler {
|
|
|
338
397
|
*
|
|
339
398
|
* @param frame - Hardware source frame
|
|
340
399
|
*
|
|
341
|
-
* @returns Software frame
|
|
400
|
+
* @returns Software frame (caller frees)
|
|
342
401
|
*
|
|
343
402
|
* @throws {FFmpegError} If the transfer fails
|
|
344
403
|
*
|
|
345
404
|
* @internal
|
|
346
405
|
*/
|
|
347
406
|
downloadToSoftwareSync(frame) {
|
|
348
|
-
const dst =
|
|
407
|
+
const dst = new Frame();
|
|
408
|
+
dst.alloc();
|
|
349
409
|
const ret = frame.hwframeTransferDataSync(dst, 0);
|
|
410
|
+
if (ret < 0) {
|
|
411
|
+
dst.free();
|
|
412
|
+
}
|
|
350
413
|
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
351
414
|
dst.copyProps(frame);
|
|
352
415
|
return dst;
|
|
353
416
|
}
|
|
354
417
|
/**
|
|
355
|
-
*
|
|
418
|
+
* Run the pooled native software scaler, serialized per instance.
|
|
356
419
|
*
|
|
357
|
-
*
|
|
420
|
+
* The native scaler reuses staging frames and sws contexts per configuration;
|
|
421
|
+
* overlapping async calls would race on them in the worker threads.
|
|
422
|
+
*
|
|
423
|
+
* @param frame - Software source frame
|
|
424
|
+
*
|
|
425
|
+
* @param options - Crop, resize, and format options
|
|
426
|
+
*
|
|
427
|
+
* @returns Tightly packed pixel data
|
|
428
|
+
*
|
|
429
|
+
* @throws {FFmpegError} If scaling fails
|
|
358
430
|
*
|
|
359
431
|
* @internal
|
|
360
432
|
*/
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
433
|
+
async processNative(frame, options) {
|
|
434
|
+
return await this.nativeLock.run(() => this.native.process(frame.getNative(), options));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The native scaler used by the sync path (lazily created).
|
|
438
|
+
*
|
|
439
|
+
* Sync calls block the event loop but can land while an async call's worker is
|
|
440
|
+
* scaling in the background; a separate native instance keeps them off the async
|
|
441
|
+
* scaler's pooled contexts, which are not safe to share across threads.
|
|
442
|
+
*
|
|
443
|
+
* @returns The sync-path native scaler
|
|
444
|
+
*
|
|
445
|
+
* @internal
|
|
446
|
+
*/
|
|
447
|
+
getSyncNative() {
|
|
448
|
+
this.syncNative ??= new bindings.Scaler(this.flags);
|
|
449
|
+
return this.syncNative;
|
|
370
450
|
}
|
|
371
451
|
/**
|
|
372
452
|
* Scale/crop/convert a frame through a cached filter graph and return the
|
|
@@ -392,27 +472,68 @@ export class Scaler {
|
|
|
392
472
|
* @internal
|
|
393
473
|
*/
|
|
394
474
|
async toFrame(frame, crop, resize, format) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
475
|
+
// Retry loop: an entry can be retired while we queue on its lock (QSV flush
|
|
476
|
+
// eviction, mid-stream failure, or LRU pressure from concurrent calls). The
|
|
477
|
+
// next attempt then resolves a freshly built graph.
|
|
478
|
+
for (;;) {
|
|
479
|
+
if (this.disposed) {
|
|
480
|
+
throw new Error('Scaler has been disposed');
|
|
481
|
+
}
|
|
482
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
483
|
+
const { entry, key } = acq;
|
|
484
|
+
// Hold the entry across the awaits below: eviction of an in-use graph is
|
|
485
|
+
// deferred to releaseGraph(), never disposing it mid-scale.
|
|
486
|
+
entry.refs++;
|
|
487
|
+
try {
|
|
488
|
+
// Serialize crop command + process + receive per graph: concurrent scales
|
|
489
|
+
// on the same output config would otherwise interleave on one FilterAPI
|
|
490
|
+
// and hand frames to the wrong caller.
|
|
491
|
+
const out = await entry.lock.run(async () => {
|
|
492
|
+
if (entry.evicted) {
|
|
493
|
+
return null; // Retired while we queued - retry with a fresh graph
|
|
494
|
+
}
|
|
495
|
+
this.prepareGraph(acq, frame);
|
|
496
|
+
try {
|
|
497
|
+
await entry.filter.process(frame);
|
|
498
|
+
let received = await entry.filter.receive();
|
|
499
|
+
if (!(received instanceof Frame)) {
|
|
500
|
+
// Some hardware scalers (notably QSV's vpp_qsv) buffer internally and don't
|
|
501
|
+
// emit a frame synchronously after a single input - the buffersink returns
|
|
502
|
+
// EAGAIN. Flush to force the pending frame out, then evict the now-EOF'd
|
|
503
|
+
// graph so the next call rebuilds it. Zero-latency scalers (scale_vaapi,
|
|
504
|
+
// scale_vt) deliver on the first receive and never hit this path.
|
|
505
|
+
await entry.filter.flush();
|
|
506
|
+
received = await entry.filter.receive();
|
|
507
|
+
this.evictGraph(key, entry);
|
|
508
|
+
}
|
|
509
|
+
if (!(received instanceof Frame)) {
|
|
510
|
+
throw new Error('Scaler: filter produced no frame');
|
|
511
|
+
}
|
|
512
|
+
return received;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
// The graph is in an unknown mid-stream state; retire it so the next
|
|
516
|
+
// call rebuilds instead of inheriting the wreckage.
|
|
517
|
+
this.evictGraph(key, entry);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
if (out) {
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
this.releaseGraph(entry);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
412
529
|
}
|
|
413
530
|
/**
|
|
414
531
|
* Synchronous version of toFrame.
|
|
415
532
|
*
|
|
533
|
+
* Sync scales cannot interleave with each other (they block the event loop),
|
|
534
|
+
* but they can land between the awaits of an in-flight async scale; a busy
|
|
535
|
+
* cached graph is bypassed with a throwaway graph in that case.
|
|
536
|
+
*
|
|
416
537
|
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
417
538
|
*
|
|
418
539
|
* @param crop - Crop region, or undefined for the full frame
|
|
@@ -426,25 +547,67 @@ export class Scaler {
|
|
|
426
547
|
* @internal
|
|
427
548
|
*/
|
|
428
549
|
toFrameSync(frame, crop, resize, format) {
|
|
429
|
-
const
|
|
430
|
-
entry
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
550
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
551
|
+
const { entry, key } = acq;
|
|
552
|
+
if (entry.refs > 0) {
|
|
553
|
+
return this.scaleOneShotSync(frame, acq);
|
|
554
|
+
}
|
|
555
|
+
this.prepareGraph(acq, frame);
|
|
556
|
+
try {
|
|
557
|
+
entry.filter.processSync(frame);
|
|
558
|
+
let out = entry.filter.receiveSync();
|
|
559
|
+
if (!(out instanceof Frame)) {
|
|
560
|
+
// See toFrame: QSV's vpp_qsv buffers a frame; flush to drain it and evict
|
|
561
|
+
// the now-EOF'd graph so the next call rebuilds.
|
|
562
|
+
entry.filter.flushSync();
|
|
563
|
+
out = entry.filter.receiveSync();
|
|
564
|
+
this.evictGraph(key, entry);
|
|
565
|
+
}
|
|
566
|
+
if (!(out instanceof Frame)) {
|
|
567
|
+
throw new Error('Scaler: filter produced no frame');
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
this.evictGraph(key, entry);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
443
575
|
}
|
|
444
576
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
577
|
+
* Scale through a throwaway graph with the crop baked in, bypassing the cache.
|
|
578
|
+
*
|
|
579
|
+
* @param frame - Source frame
|
|
580
|
+
*
|
|
581
|
+
* @param acq - Resolved operation configuration
|
|
582
|
+
*
|
|
583
|
+
* @returns The scaled output frame (caller frees)
|
|
584
|
+
*
|
|
585
|
+
* @throws {Error} If the graph produces no frame
|
|
586
|
+
*
|
|
587
|
+
* @internal
|
|
588
|
+
*/
|
|
589
|
+
scaleOneShotSync(frame, acq) {
|
|
590
|
+
const filter = FilterAPI.create(this.buildGraph(acq.isHw, acq.outW, acq.outH, acq.format, acq.crop, false), acq.isHw ? { hardware: this.hardware } : {});
|
|
591
|
+
try {
|
|
592
|
+
filter.processSync(frame);
|
|
593
|
+
let out = filter.receiveSync();
|
|
594
|
+
if (!(out instanceof Frame)) {
|
|
595
|
+
filter.flushSync();
|
|
596
|
+
out = filter.receiveSync();
|
|
597
|
+
}
|
|
598
|
+
if (!(out instanceof Frame)) {
|
|
599
|
+
throw new Error('Scaler: filter produced no frame');
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
filter[Symbol.dispose]();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Resolve (and, on demand, build) the cached filter graph for a frame. Shared
|
|
609
|
+
* by the sync and async scaling paths; all of this work is synchronous, so a
|
|
610
|
+
* resolved entry cannot be evicted before the caller marks it in use.
|
|
448
611
|
*
|
|
449
612
|
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
450
613
|
*
|
|
@@ -454,7 +617,7 @@ export class Scaler {
|
|
|
454
617
|
*
|
|
455
618
|
* @param format - Output pixel format
|
|
456
619
|
*
|
|
457
|
-
* @returns The cached graph entry
|
|
620
|
+
* @returns The cached graph entry and the resolved operation configuration
|
|
458
621
|
*
|
|
459
622
|
* @throws {Error} If the crop is out of bounds
|
|
460
623
|
*
|
|
@@ -488,7 +651,7 @@ export class Scaler {
|
|
|
488
651
|
// on the first processed frame, before any command can be sent, so the
|
|
489
652
|
// initial crop must already be in place. Later crops use sendCommand.
|
|
490
653
|
const filter = FilterAPI.create(this.buildGraph(isHw, outW, outH, format, cropRegion, commandable), isHw ? { hardware: this.hardware } : {});
|
|
491
|
-
entry = { filter, lastCrop: commandable ? { x: cropX, y: cropY, w: cropW, h: cropH } : null };
|
|
654
|
+
entry = { filter, lastCrop: commandable ? { x: cropX, y: cropY, w: cropW, h: cropH } : null, lastSrc: null, lock: new Mutex(), refs: 0, evicted: false };
|
|
492
655
|
this.cacheSet(key, entry);
|
|
493
656
|
}
|
|
494
657
|
else {
|
|
@@ -496,38 +659,95 @@ export class Scaler {
|
|
|
496
659
|
this.graphs.delete(key);
|
|
497
660
|
this.graphs.set(key, entry);
|
|
498
661
|
}
|
|
499
|
-
|
|
500
|
-
// Reconfigure the crop only when it actually changed - identical crops reuse
|
|
501
|
-
// the graph untouched (no config_input/config_output re-run).
|
|
502
|
-
const c = entry.lastCrop;
|
|
503
|
-
if (c?.x !== cropX || c?.y !== cropY || c?.w !== cropW || c?.h !== cropH) {
|
|
504
|
-
// w/h are validated against the input dims (above); x/y are clamped per
|
|
505
|
-
// frame by the crop filter, so this order never produces a transient error.
|
|
506
|
-
entry.filter.sendCommand('crop@sc', 'w', String(cropW));
|
|
507
|
-
entry.filter.sendCommand('crop@sc', 'h', String(cropH));
|
|
508
|
-
entry.filter.sendCommand('crop@sc', 'x', String(cropX));
|
|
509
|
-
entry.filter.sendCommand('crop@sc', 'y', String(cropY));
|
|
510
|
-
entry.lastCrop = { x: cropX, y: cropY, w: cropW, h: cropH };
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return { entry, key };
|
|
662
|
+
return { entry, key, isHw, commandable, crop: cropRegion, outW, outH, format };
|
|
514
663
|
}
|
|
515
664
|
/**
|
|
516
|
-
*
|
|
665
|
+
* Bring a cached graph in line with the current call. Must run under the entry
|
|
666
|
+
* lock (async path) or with no async call in flight (sync path).
|
|
517
667
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
668
|
+
* Rebuilds the graph when the source parameters changed: FilterAPI would
|
|
669
|
+
* otherwise reinitialize itself from the original graph description, silently
|
|
670
|
+
* resurrecting the crop baked in at build time instead of the current one. A
|
|
671
|
+
* graph that has not seen a frame yet is likewise rebuilt when the crop
|
|
672
|
+
* differs, because commands cannot be sent before initialization. Otherwise a
|
|
673
|
+
* changed crop is re-aimed with runtime commands - identical crops reuse the
|
|
674
|
+
* graph untouched (no config_input/config_output re-run).
|
|
520
675
|
*
|
|
521
|
-
* @param
|
|
676
|
+
* @param acq - Resolved graph and operation configuration
|
|
677
|
+
*
|
|
678
|
+
* @param frame - Frame about to be processed
|
|
522
679
|
*
|
|
523
680
|
* @internal
|
|
524
681
|
*/
|
|
525
|
-
|
|
526
|
-
const entry =
|
|
527
|
-
|
|
682
|
+
prepareGraph(acq, frame) {
|
|
683
|
+
const { entry, isHw, commandable, crop, outW, outH, format } = acq;
|
|
684
|
+
const src = entry.lastSrc;
|
|
685
|
+
const srcChanged = src !== null && (src.width !== frame.width || src.height !== frame.height || src.format !== frame.format);
|
|
686
|
+
const c = entry.lastCrop;
|
|
687
|
+
const cropChanged = commandable && (c?.x !== crop.x || c?.y !== crop.y || c?.w !== crop.width || c?.h !== crop.height);
|
|
688
|
+
if (srcChanged || (cropChanged && src === null)) {
|
|
528
689
|
entry.filter[Symbol.dispose]();
|
|
690
|
+
entry.filter = FilterAPI.create(this.buildGraph(isHw, outW, outH, format, crop, commandable), isHw ? { hardware: this.hardware } : {});
|
|
691
|
+
entry.lastCrop = commandable ? { x: crop.x, y: crop.y, w: crop.width, h: crop.height } : null;
|
|
692
|
+
}
|
|
693
|
+
else if (cropChanged) {
|
|
694
|
+
// w/h are validated against the input dims (in resolveGraph); x/y are clamped
|
|
695
|
+
// per frame by the crop filter, so this order never produces a transient error.
|
|
696
|
+
entry.filter.sendCommand('crop@sc', 'w', String(crop.width));
|
|
697
|
+
entry.filter.sendCommand('crop@sc', 'h', String(crop.height));
|
|
698
|
+
entry.filter.sendCommand('crop@sc', 'x', String(crop.x));
|
|
699
|
+
entry.filter.sendCommand('crop@sc', 'y', String(crop.y));
|
|
700
|
+
entry.lastCrop = { x: crop.x, y: crop.y, w: crop.width, h: crop.height };
|
|
701
|
+
}
|
|
702
|
+
entry.lastSrc = { width: frame.width, height: frame.height, format: frame.format };
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Remove a graph from the cache and retire it.
|
|
706
|
+
*
|
|
707
|
+
* Identity-checked so a newer graph cached under the same key is left alone.
|
|
708
|
+
* Used after a flush (the graph is at EOF and can no longer accept frames) and
|
|
709
|
+
* after mid-stream failures, so the next operation rebuilds it.
|
|
710
|
+
*
|
|
711
|
+
* @param key - Cache key of the graph
|
|
712
|
+
*
|
|
713
|
+
* @param entry - The exact entry to evict
|
|
714
|
+
*
|
|
715
|
+
* @internal
|
|
716
|
+
*/
|
|
717
|
+
evictGraph(key, entry) {
|
|
718
|
+
if (this.graphs.get(key) === entry) {
|
|
529
719
|
this.graphs.delete(key);
|
|
530
720
|
}
|
|
721
|
+
this.retireEntry(entry);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Take a graph out of service, disposing it now or on last release.
|
|
725
|
+
*
|
|
726
|
+
* An entry with in-flight scales is only marked; the final {@link releaseGraph}
|
|
727
|
+
* disposes the filter once nothing uses it anymore.
|
|
728
|
+
*
|
|
729
|
+
* @param entry - Entry removed from the cache
|
|
730
|
+
*
|
|
731
|
+
* @internal
|
|
732
|
+
*/
|
|
733
|
+
retireEntry(entry) {
|
|
734
|
+
entry.evicted = true;
|
|
735
|
+
if (entry.refs === 0) {
|
|
736
|
+
entry.filter[Symbol.dispose]();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Drop an in-flight reference, disposing the graph if it was retired meanwhile.
|
|
741
|
+
*
|
|
742
|
+
* @param entry - Entry acquired by {@link toFrame}
|
|
743
|
+
*
|
|
744
|
+
* @internal
|
|
745
|
+
*/
|
|
746
|
+
releaseGraph(entry) {
|
|
747
|
+
entry.refs--;
|
|
748
|
+
if (entry.refs === 0 && entry.evicted) {
|
|
749
|
+
entry.filter[Symbol.dispose]();
|
|
750
|
+
}
|
|
531
751
|
}
|
|
532
752
|
/**
|
|
533
753
|
* Tag a frame for MJPEG encoding: full color range and a per-frame quality.
|
|
@@ -575,7 +795,9 @@ export class Scaler {
|
|
|
575
795
|
* For software frames this is `crop,scale,format`. For hardware frames the crop
|
|
576
796
|
* filter sets crop metadata that the hardware scaler (scale_vt/scale_cuda/
|
|
577
797
|
* scale_vaapi) applies on the GPU; the result is kept in NV12, downloaded, and
|
|
578
|
-
* converted on the CPU.
|
|
798
|
+
* converted on the CPU. Scalers with a same-size/same-format passthrough mode
|
|
799
|
+
* (scale_vaapi/scale_cuda/vpp_qsv) get `passthrough=0`, since skipping the scaler
|
|
800
|
+
* would drop the crop metadata unapplied. When `commandable` the crop is a labeled instance
|
|
579
801
|
* (`crop@sc`) whose initial region is `crop` but which is re-aimed per frame via
|
|
580
802
|
* `sendCommand`. Otherwise (OpenCL) the crop is fixed in the graph.
|
|
581
803
|
*
|
|
@@ -604,7 +826,13 @@ export class Scaler {
|
|
|
604
826
|
else {
|
|
605
827
|
chain = chain.crop(crop.width, crop.height, crop.x, crop.y);
|
|
606
828
|
}
|
|
607
|
-
|
|
829
|
+
// The crop filter only sets crop metadata on hardware frames; the hw scaler
|
|
830
|
+
// materializes it. scale_vaapi/scale_cuda/vpp_qsv skip themselves entirely when
|
|
831
|
+
// input and output size/format match (decided at config time, before any crop
|
|
832
|
+
// command), which would silently drop the crop - so their passthrough is disabled.
|
|
833
|
+
const passthroughCapable = [AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_QSV];
|
|
834
|
+
const noPassthrough = isHw && passthroughCapable.includes(this.hardware.deviceType);
|
|
835
|
+
chain = chain.scale(outW, outH, noPassthrough ? { passthrough: 0 } : undefined);
|
|
608
836
|
if (isHw) {
|
|
609
837
|
// Keep the GPU result in NV12, download the small frame, then convert on CPU.
|
|
610
838
|
chain = chain.scaleFormat(AV_PIX_FMT_NV12).hwdownload().format(AV_PIX_FMT_NV12);
|
|
@@ -633,8 +861,13 @@ export class Scaler {
|
|
|
633
861
|
if (oldest === undefined) {
|
|
634
862
|
break;
|
|
635
863
|
}
|
|
636
|
-
this.graphs.get(oldest)
|
|
864
|
+
const evictee = this.graphs.get(oldest);
|
|
637
865
|
this.graphs.delete(oldest);
|
|
866
|
+
if (evictee) {
|
|
867
|
+
// In-use graphs are retired, not disposed - the last in-flight scale
|
|
868
|
+
// disposes them on release.
|
|
869
|
+
this.retireEntry(evictee);
|
|
870
|
+
}
|
|
638
871
|
}
|
|
639
872
|
}
|
|
640
873
|
/**
|