node-av 6.2.0-beta.2 → 6.2.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/audio-frame-buffer.d.ts +73 -1
- package/dist/api/audio-frame-buffer.js +127 -1
- 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.js +1 -1
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/demuxer.d.ts +7 -0
- package/dist/api/demuxer.js +88 -29
- package/dist/api/demuxer.js.map +1 -1
- package/dist/api/encoder-pool.d.ts +43 -1
- package/dist/api/encoder-pool.js +113 -9
- package/dist/api/encoder-pool.js.map +1 -1
- package/dist/api/encoder.d.ts +2 -0
- package/dist/api/encoder.js +105 -41
- 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 +93 -2
- package/dist/api/fmp4-stream.js +133 -15
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/hardware.js +4 -3
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/io-stream.js +61 -5
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/muxer.d.ts +13 -0
- package/dist/api/muxer.js +69 -19
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/pipeline.d.ts +2 -0
- package/dist/api/pipeline.js +23 -5
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +5 -6
- package/dist/api/rtp-stream.js +36 -26
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +102 -16
- package/dist/api/scaler.js +316 -106
- 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/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/ffmpeg/install.js +115 -29
- package/dist/ffmpeg/install.js.map +1 -1
- package/dist/lib/binding.d.ts +1 -0
- package/dist/lib/binding.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 +0 -1
- package/dist/lib/filter-context.js +2 -11
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/format-context.d.ts +45 -4
- package/dist/lib/format-context.js +50 -16
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +11 -11
- package/dist/lib/frame.js +13 -20
- 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/native-types.d.ts +2 -2
- 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 +3 -4
- package/dist/lib/stream.js +17 -24
- package/dist/lib/stream.js.map +1 -1
- package/install/check.js +26 -63
- package/package.json +33 -16
- 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, AV_PIX_FMT_GRAY8,
|
|
1
|
+
import { AV_CODEC_FLAG_QSCALE, AV_HWDEVICE_TYPE_OPENCL, 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.
|
|
@@ -103,9 +108,15 @@ export class Scaler {
|
|
|
103
108
|
out.free();
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
|
-
|
|
111
|
+
const sw = await this.downloadToSoftware(frame);
|
|
112
|
+
try {
|
|
113
|
+
return await this.processNative(sw, options);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
sw.free();
|
|
117
|
+
}
|
|
107
118
|
}
|
|
108
|
-
return await this.
|
|
119
|
+
return await this.processNative(frame, options);
|
|
109
120
|
}
|
|
110
121
|
/**
|
|
111
122
|
* Scale, crop, and/or convert a frame and return the raw pixel data.
|
|
@@ -140,9 +151,15 @@ export class Scaler {
|
|
|
140
151
|
out.free();
|
|
141
152
|
}
|
|
142
153
|
}
|
|
143
|
-
|
|
154
|
+
const sw = this.downloadToSoftwareSync(frame);
|
|
155
|
+
try {
|
|
156
|
+
return this.getSyncNative().processSync(sw.getNative(), options);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
sw.free();
|
|
160
|
+
}
|
|
144
161
|
}
|
|
145
|
-
return this.
|
|
162
|
+
return this.getSyncNative().processSync(frame.getNative(), options);
|
|
146
163
|
}
|
|
147
164
|
/**
|
|
148
165
|
* Scale/crop a frame and encode it to a JPEG image.
|
|
@@ -172,16 +189,21 @@ export class Scaler {
|
|
|
172
189
|
throw new Error('Scaler has been disposed');
|
|
173
190
|
}
|
|
174
191
|
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');
|
|
192
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? await this.downloadToSoftware(frame) : null;
|
|
179
193
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
// mjpeg wants planar YUV; tag it full-range so colors are correct without the
|
|
195
|
+
// deprecated yuvj* formats.
|
|
196
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
197
|
+
try {
|
|
198
|
+
this.prepareJpegFrame(out, quality);
|
|
199
|
+
return await this.getJpegPool().encode(out);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
out.free();
|
|
203
|
+
}
|
|
182
204
|
}
|
|
183
205
|
finally {
|
|
184
|
-
|
|
206
|
+
downloaded?.free();
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
209
|
/**
|
|
@@ -208,14 +230,19 @@ export class Scaler {
|
|
|
208
230
|
throw new Error('Scaler has been disposed');
|
|
209
231
|
}
|
|
210
232
|
const quality = options.quality ?? 90;
|
|
211
|
-
const
|
|
212
|
-
const out = this.toFrameSync(src, options.crop, options.resize, 'yuv420p');
|
|
233
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? this.downloadToSoftwareSync(frame) : null;
|
|
213
234
|
try {
|
|
214
|
-
this.
|
|
215
|
-
|
|
235
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
236
|
+
try {
|
|
237
|
+
this.prepareJpegFrame(out, quality);
|
|
238
|
+
return this.getJpegPool().encodeSync(out);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
out.free();
|
|
242
|
+
}
|
|
216
243
|
}
|
|
217
244
|
finally {
|
|
218
|
-
|
|
245
|
+
downloaded?.free();
|
|
219
246
|
}
|
|
220
247
|
}
|
|
221
248
|
/**
|
|
@@ -245,13 +272,18 @@ export class Scaler {
|
|
|
245
272
|
throw new Error('Scaler has been disposed');
|
|
246
273
|
}
|
|
247
274
|
const format = options.format ?? 'rgb';
|
|
248
|
-
const
|
|
249
|
-
const out = await this.toFrame(src, options.crop, options.resize, format);
|
|
275
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? await this.downloadToSoftware(frame) : null;
|
|
250
276
|
try {
|
|
251
|
-
|
|
277
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, format);
|
|
278
|
+
try {
|
|
279
|
+
return await this.getPngPool().encode(out);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
out.free();
|
|
283
|
+
}
|
|
252
284
|
}
|
|
253
285
|
finally {
|
|
254
|
-
|
|
286
|
+
downloaded?.free();
|
|
255
287
|
}
|
|
256
288
|
}
|
|
257
289
|
/**
|
|
@@ -278,13 +310,18 @@ export class Scaler {
|
|
|
278
310
|
throw new Error('Scaler has been disposed');
|
|
279
311
|
}
|
|
280
312
|
const format = options.format ?? 'rgb';
|
|
281
|
-
const
|
|
282
|
-
const out = this.toFrameSync(src, options.crop, options.resize, format);
|
|
313
|
+
const downloaded = frame.isHwFrame() && !this.hardware ? this.downloadToSoftwareSync(frame) : null;
|
|
283
314
|
try {
|
|
284
|
-
|
|
315
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, format);
|
|
316
|
+
try {
|
|
317
|
+
return this.getPngPool().encodeSync(out);
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
out.free();
|
|
321
|
+
}
|
|
285
322
|
}
|
|
286
323
|
finally {
|
|
287
|
-
|
|
324
|
+
downloaded?.free();
|
|
288
325
|
}
|
|
289
326
|
}
|
|
290
327
|
/**
|
|
@@ -301,34 +338,41 @@ export class Scaler {
|
|
|
301
338
|
}
|
|
302
339
|
this.disposed = true;
|
|
303
340
|
this.native.close();
|
|
341
|
+
this.syncNative?.close();
|
|
342
|
+
this.syncNative = undefined;
|
|
343
|
+
// In-use graphs are retired, not disposed: the last in-flight scale disposes
|
|
344
|
+
// them on release, so close() during concurrent calls never crashes them.
|
|
304
345
|
for (const entry of this.graphs.values()) {
|
|
305
|
-
|
|
346
|
+
this.retireEntry(entry);
|
|
306
347
|
}
|
|
307
348
|
this.graphs.clear();
|
|
308
349
|
this.jpegPool?.close();
|
|
309
350
|
this.jpegPool = undefined;
|
|
310
351
|
this.pngPool?.close();
|
|
311
352
|
this.pngPool = undefined;
|
|
312
|
-
this.downloadFrame?.free();
|
|
313
|
-
this.downloadFrame = undefined;
|
|
314
353
|
}
|
|
315
354
|
/**
|
|
316
355
|
* Download a hardware frame to system memory (no hardware context available).
|
|
317
356
|
*
|
|
318
|
-
*
|
|
319
|
-
* selects a supported software
|
|
357
|
+
* Allocates a fresh target per call - concurrent downloads must not share a
|
|
358
|
+
* staging frame. The frame's own hwframe context selects a supported software
|
|
359
|
+
* format. The caller frees the returned frame.
|
|
320
360
|
*
|
|
321
361
|
* @param frame - Hardware source frame
|
|
322
362
|
*
|
|
323
|
-
* @returns Software frame
|
|
363
|
+
* @returns Software frame (caller frees)
|
|
324
364
|
*
|
|
325
365
|
* @throws {FFmpegError} If the transfer fails
|
|
326
366
|
*
|
|
327
367
|
* @internal
|
|
328
368
|
*/
|
|
329
369
|
async downloadToSoftware(frame) {
|
|
330
|
-
const dst =
|
|
370
|
+
const dst = new Frame();
|
|
371
|
+
dst.alloc();
|
|
331
372
|
const ret = await frame.hwframeTransferData(dst, 0);
|
|
373
|
+
if (ret < 0) {
|
|
374
|
+
dst.free();
|
|
375
|
+
}
|
|
332
376
|
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
333
377
|
dst.copyProps(frame);
|
|
334
378
|
return dst;
|
|
@@ -338,35 +382,56 @@ export class Scaler {
|
|
|
338
382
|
*
|
|
339
383
|
* @param frame - Hardware source frame
|
|
340
384
|
*
|
|
341
|
-
* @returns Software frame
|
|
385
|
+
* @returns Software frame (caller frees)
|
|
342
386
|
*
|
|
343
387
|
* @throws {FFmpegError} If the transfer fails
|
|
344
388
|
*
|
|
345
389
|
* @internal
|
|
346
390
|
*/
|
|
347
391
|
downloadToSoftwareSync(frame) {
|
|
348
|
-
const dst =
|
|
392
|
+
const dst = new Frame();
|
|
393
|
+
dst.alloc();
|
|
349
394
|
const ret = frame.hwframeTransferDataSync(dst, 0);
|
|
395
|
+
if (ret < 0) {
|
|
396
|
+
dst.free();
|
|
397
|
+
}
|
|
350
398
|
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
351
399
|
dst.copyProps(frame);
|
|
352
400
|
return dst;
|
|
353
401
|
}
|
|
354
402
|
/**
|
|
355
|
-
*
|
|
403
|
+
* Run the pooled native software scaler, serialized per instance.
|
|
404
|
+
*
|
|
405
|
+
* The native scaler reuses staging frames and sws contexts per configuration;
|
|
406
|
+
* overlapping async calls would race on them in the worker threads.
|
|
407
|
+
*
|
|
408
|
+
* @param frame - Software source frame
|
|
356
409
|
*
|
|
357
|
-
* @
|
|
410
|
+
* @param options - Crop, resize, and format options
|
|
411
|
+
*
|
|
412
|
+
* @returns Tightly packed pixel data
|
|
413
|
+
*
|
|
414
|
+
* @throws {FFmpegError} If scaling fails
|
|
358
415
|
*
|
|
359
416
|
* @internal
|
|
360
417
|
*/
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
418
|
+
async processNative(frame, options) {
|
|
419
|
+
return await this.nativeLock.run(() => this.native.process(frame.getNative(), options));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The native scaler used by the sync path (lazily created).
|
|
423
|
+
*
|
|
424
|
+
* Sync calls block the event loop but can land while an async call's worker is
|
|
425
|
+
* scaling in the background; a separate native instance keeps them off the async
|
|
426
|
+
* scaler's pooled contexts, which are not safe to share across threads.
|
|
427
|
+
*
|
|
428
|
+
* @returns The sync-path native scaler
|
|
429
|
+
*
|
|
430
|
+
* @internal
|
|
431
|
+
*/
|
|
432
|
+
getSyncNative() {
|
|
433
|
+
this.syncNative ??= new bindings.Scaler(this.flags);
|
|
434
|
+
return this.syncNative;
|
|
370
435
|
}
|
|
371
436
|
/**
|
|
372
437
|
* Scale/crop/convert a frame through a cached filter graph and return the
|
|
@@ -392,27 +457,68 @@ export class Scaler {
|
|
|
392
457
|
* @internal
|
|
393
458
|
*/
|
|
394
459
|
async toFrame(frame, crop, resize, format) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
460
|
+
// Retry loop: an entry can be retired while we queue on its lock (QSV flush
|
|
461
|
+
// eviction, mid-stream failure, or LRU pressure from concurrent calls). The
|
|
462
|
+
// next attempt then resolves a freshly built graph.
|
|
463
|
+
for (;;) {
|
|
464
|
+
if (this.disposed) {
|
|
465
|
+
throw new Error('Scaler has been disposed');
|
|
466
|
+
}
|
|
467
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
468
|
+
const { entry, key } = acq;
|
|
469
|
+
// Hold the entry across the awaits below: eviction of an in-use graph is
|
|
470
|
+
// deferred to releaseGraph(), never disposing it mid-scale.
|
|
471
|
+
entry.refs++;
|
|
472
|
+
try {
|
|
473
|
+
// Serialize crop command + process + receive per graph: concurrent scales
|
|
474
|
+
// on the same output config would otherwise interleave on one FilterAPI
|
|
475
|
+
// and hand frames to the wrong caller.
|
|
476
|
+
const out = await entry.lock.run(async () => {
|
|
477
|
+
if (entry.evicted) {
|
|
478
|
+
return null; // Retired while we queued - retry with a fresh graph
|
|
479
|
+
}
|
|
480
|
+
this.prepareGraph(acq, frame);
|
|
481
|
+
try {
|
|
482
|
+
await entry.filter.process(frame);
|
|
483
|
+
let received = await entry.filter.receive();
|
|
484
|
+
if (!(received instanceof Frame)) {
|
|
485
|
+
// Some hardware scalers (notably QSV's vpp_qsv) buffer internally and don't
|
|
486
|
+
// emit a frame synchronously after a single input - the buffersink returns
|
|
487
|
+
// EAGAIN. Flush to force the pending frame out, then evict the now-EOF'd
|
|
488
|
+
// graph so the next call rebuilds it. Zero-latency scalers (scale_vaapi,
|
|
489
|
+
// scale_vt) deliver on the first receive and never hit this path.
|
|
490
|
+
await entry.filter.flush();
|
|
491
|
+
received = await entry.filter.receive();
|
|
492
|
+
this.evictGraph(key, entry);
|
|
493
|
+
}
|
|
494
|
+
if (!(received instanceof Frame)) {
|
|
495
|
+
throw new Error('Scaler: filter produced no frame');
|
|
496
|
+
}
|
|
497
|
+
return received;
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
// The graph is in an unknown mid-stream state; retire it so the next
|
|
501
|
+
// call rebuilds instead of inheriting the wreckage.
|
|
502
|
+
this.evictGraph(key, entry);
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
if (out) {
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
this.releaseGraph(entry);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
412
514
|
}
|
|
413
515
|
/**
|
|
414
516
|
* Synchronous version of toFrame.
|
|
415
517
|
*
|
|
518
|
+
* Sync scales cannot interleave with each other (they block the event loop),
|
|
519
|
+
* but they can land between the awaits of an in-flight async scale; a busy
|
|
520
|
+
* cached graph is bypassed with a throwaway graph in that case.
|
|
521
|
+
*
|
|
416
522
|
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
417
523
|
*
|
|
418
524
|
* @param crop - Crop region, or undefined for the full frame
|
|
@@ -426,25 +532,67 @@ export class Scaler {
|
|
|
426
532
|
* @internal
|
|
427
533
|
*/
|
|
428
534
|
toFrameSync(frame, crop, resize, format) {
|
|
429
|
-
const
|
|
430
|
-
entry
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
535
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
536
|
+
const { entry, key } = acq;
|
|
537
|
+
if (entry.refs > 0) {
|
|
538
|
+
return this.scaleOneShotSync(frame, acq);
|
|
539
|
+
}
|
|
540
|
+
this.prepareGraph(acq, frame);
|
|
541
|
+
try {
|
|
542
|
+
entry.filter.processSync(frame);
|
|
543
|
+
let out = entry.filter.receiveSync();
|
|
544
|
+
if (!(out instanceof Frame)) {
|
|
545
|
+
// See toFrame: QSV's vpp_qsv buffers a frame; flush to drain it and evict
|
|
546
|
+
// the now-EOF'd graph so the next call rebuilds.
|
|
547
|
+
entry.filter.flushSync();
|
|
548
|
+
out = entry.filter.receiveSync();
|
|
549
|
+
this.evictGraph(key, entry);
|
|
550
|
+
}
|
|
551
|
+
if (!(out instanceof Frame)) {
|
|
552
|
+
throw new Error('Scaler: filter produced no frame');
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
this.evictGraph(key, entry);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
443
560
|
}
|
|
444
561
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
562
|
+
* Scale through a throwaway graph with the crop baked in, bypassing the cache.
|
|
563
|
+
*
|
|
564
|
+
* @param frame - Source frame
|
|
565
|
+
*
|
|
566
|
+
* @param acq - Resolved operation configuration
|
|
567
|
+
*
|
|
568
|
+
* @returns The scaled output frame (caller frees)
|
|
569
|
+
*
|
|
570
|
+
* @throws {Error} If the graph produces no frame
|
|
571
|
+
*
|
|
572
|
+
* @internal
|
|
573
|
+
*/
|
|
574
|
+
scaleOneShotSync(frame, acq) {
|
|
575
|
+
const filter = FilterAPI.create(this.buildGraph(acq.isHw, acq.outW, acq.outH, acq.format, acq.crop, false), acq.isHw ? { hardware: this.hardware } : {});
|
|
576
|
+
try {
|
|
577
|
+
filter.processSync(frame);
|
|
578
|
+
let out = filter.receiveSync();
|
|
579
|
+
if (!(out instanceof Frame)) {
|
|
580
|
+
filter.flushSync();
|
|
581
|
+
out = filter.receiveSync();
|
|
582
|
+
}
|
|
583
|
+
if (!(out instanceof Frame)) {
|
|
584
|
+
throw new Error('Scaler: filter produced no frame');
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
filter[Symbol.dispose]();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Resolve (and, on demand, build) the cached filter graph for a frame. Shared
|
|
594
|
+
* by the sync and async scaling paths; all of this work is synchronous, so a
|
|
595
|
+
* resolved entry cannot be evicted before the caller marks it in use.
|
|
448
596
|
*
|
|
449
597
|
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
450
598
|
*
|
|
@@ -454,7 +602,7 @@ export class Scaler {
|
|
|
454
602
|
*
|
|
455
603
|
* @param format - Output pixel format
|
|
456
604
|
*
|
|
457
|
-
* @returns The cached graph entry
|
|
605
|
+
* @returns The cached graph entry and the resolved operation configuration
|
|
458
606
|
*
|
|
459
607
|
* @throws {Error} If the crop is out of bounds
|
|
460
608
|
*
|
|
@@ -488,7 +636,7 @@ export class Scaler {
|
|
|
488
636
|
// on the first processed frame, before any command can be sent, so the
|
|
489
637
|
// initial crop must already be in place. Later crops use sendCommand.
|
|
490
638
|
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 };
|
|
639
|
+
entry = { filter, lastCrop: commandable ? { x: cropX, y: cropY, w: cropW, h: cropH } : null, lastSrc: null, lock: new Mutex(), refs: 0, evicted: false };
|
|
492
640
|
this.cacheSet(key, entry);
|
|
493
641
|
}
|
|
494
642
|
else {
|
|
@@ -496,38 +644,95 @@ export class Scaler {
|
|
|
496
644
|
this.graphs.delete(key);
|
|
497
645
|
this.graphs.set(key, entry);
|
|
498
646
|
}
|
|
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 };
|
|
647
|
+
return { entry, key, isHw, commandable, crop: cropRegion, outW, outH, format };
|
|
514
648
|
}
|
|
515
649
|
/**
|
|
516
|
-
*
|
|
650
|
+
* Bring a cached graph in line with the current call. Must run under the entry
|
|
651
|
+
* lock (async path) or with no async call in flight (sync path).
|
|
517
652
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
653
|
+
* Rebuilds the graph when the source parameters changed: FilterAPI would
|
|
654
|
+
* otherwise reinitialize itself from the original graph description, silently
|
|
655
|
+
* resurrecting the crop baked in at build time instead of the current one. A
|
|
656
|
+
* graph that has not seen a frame yet is likewise rebuilt when the crop
|
|
657
|
+
* differs, because commands cannot be sent before initialization. Otherwise a
|
|
658
|
+
* changed crop is re-aimed with runtime commands - identical crops reuse the
|
|
659
|
+
* graph untouched (no config_input/config_output re-run).
|
|
520
660
|
*
|
|
521
|
-
* @param
|
|
661
|
+
* @param acq - Resolved graph and operation configuration
|
|
662
|
+
*
|
|
663
|
+
* @param frame - Frame about to be processed
|
|
522
664
|
*
|
|
523
665
|
* @internal
|
|
524
666
|
*/
|
|
525
|
-
|
|
526
|
-
const entry =
|
|
527
|
-
|
|
667
|
+
prepareGraph(acq, frame) {
|
|
668
|
+
const { entry, isHw, commandable, crop, outW, outH, format } = acq;
|
|
669
|
+
const src = entry.lastSrc;
|
|
670
|
+
const srcChanged = src !== null && (src.width !== frame.width || src.height !== frame.height || src.format !== frame.format);
|
|
671
|
+
const c = entry.lastCrop;
|
|
672
|
+
const cropChanged = commandable && (c?.x !== crop.x || c?.y !== crop.y || c?.w !== crop.width || c?.h !== crop.height);
|
|
673
|
+
if (srcChanged || (cropChanged && src === null)) {
|
|
528
674
|
entry.filter[Symbol.dispose]();
|
|
675
|
+
entry.filter = FilterAPI.create(this.buildGraph(isHw, outW, outH, format, crop, commandable), isHw ? { hardware: this.hardware } : {});
|
|
676
|
+
entry.lastCrop = commandable ? { x: crop.x, y: crop.y, w: crop.width, h: crop.height } : null;
|
|
677
|
+
}
|
|
678
|
+
else if (cropChanged) {
|
|
679
|
+
// w/h are validated against the input dims (in resolveGraph); x/y are clamped
|
|
680
|
+
// per frame by the crop filter, so this order never produces a transient error.
|
|
681
|
+
entry.filter.sendCommand('crop@sc', 'w', String(crop.width));
|
|
682
|
+
entry.filter.sendCommand('crop@sc', 'h', String(crop.height));
|
|
683
|
+
entry.filter.sendCommand('crop@sc', 'x', String(crop.x));
|
|
684
|
+
entry.filter.sendCommand('crop@sc', 'y', String(crop.y));
|
|
685
|
+
entry.lastCrop = { x: crop.x, y: crop.y, w: crop.width, h: crop.height };
|
|
686
|
+
}
|
|
687
|
+
entry.lastSrc = { width: frame.width, height: frame.height, format: frame.format };
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Remove a graph from the cache and retire it.
|
|
691
|
+
*
|
|
692
|
+
* Identity-checked so a newer graph cached under the same key is left alone.
|
|
693
|
+
* Used after a flush (the graph is at EOF and can no longer accept frames) and
|
|
694
|
+
* after mid-stream failures, so the next operation rebuilds it.
|
|
695
|
+
*
|
|
696
|
+
* @param key - Cache key of the graph
|
|
697
|
+
*
|
|
698
|
+
* @param entry - The exact entry to evict
|
|
699
|
+
*
|
|
700
|
+
* @internal
|
|
701
|
+
*/
|
|
702
|
+
evictGraph(key, entry) {
|
|
703
|
+
if (this.graphs.get(key) === entry) {
|
|
529
704
|
this.graphs.delete(key);
|
|
530
705
|
}
|
|
706
|
+
this.retireEntry(entry);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Take a graph out of service, disposing it now or on last release.
|
|
710
|
+
*
|
|
711
|
+
* An entry with in-flight scales is only marked; the final {@link releaseGraph}
|
|
712
|
+
* disposes the filter once nothing uses it anymore.
|
|
713
|
+
*
|
|
714
|
+
* @param entry - Entry removed from the cache
|
|
715
|
+
*
|
|
716
|
+
* @internal
|
|
717
|
+
*/
|
|
718
|
+
retireEntry(entry) {
|
|
719
|
+
entry.evicted = true;
|
|
720
|
+
if (entry.refs === 0) {
|
|
721
|
+
entry.filter[Symbol.dispose]();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Drop an in-flight reference, disposing the graph if it was retired meanwhile.
|
|
726
|
+
*
|
|
727
|
+
* @param entry - Entry acquired by {@link toFrame}
|
|
728
|
+
*
|
|
729
|
+
* @internal
|
|
730
|
+
*/
|
|
731
|
+
releaseGraph(entry) {
|
|
732
|
+
entry.refs--;
|
|
733
|
+
if (entry.refs === 0 && entry.evicted) {
|
|
734
|
+
entry.filter[Symbol.dispose]();
|
|
735
|
+
}
|
|
531
736
|
}
|
|
532
737
|
/**
|
|
533
738
|
* Tag a frame for MJPEG encoding: full color range and a per-frame quality.
|
|
@@ -633,8 +838,13 @@ export class Scaler {
|
|
|
633
838
|
if (oldest === undefined) {
|
|
634
839
|
break;
|
|
635
840
|
}
|
|
636
|
-
this.graphs.get(oldest)
|
|
841
|
+
const evictee = this.graphs.get(oldest);
|
|
637
842
|
this.graphs.delete(oldest);
|
|
843
|
+
if (evictee) {
|
|
844
|
+
// In-use graphs are retired, not disposed - the last in-flight scale
|
|
845
|
+
// disposes them on release.
|
|
846
|
+
this.retireEntry(evictee);
|
|
847
|
+
}
|
|
638
848
|
}
|
|
639
849
|
}
|
|
640
850
|
/**
|