node-av-lgpl 0.0.0 → 7.1.0-beta.2
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/LICENSE.md +22 -0
- package/README.md +711 -0
- package/dist/api/audio-frame-buffer.d.ts +636 -0
- package/dist/api/audio-frame-buffer.js +1027 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +938 -0
- package/dist/api/bitstream-filter.js +1427 -0
- package/dist/api/bitstream-filter.js.map +1 -0
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_h264.h264 +0 -0
- package/dist/api/data/test_hevc.h265 +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +1413 -0
- package/dist/api/decoder.js +2367 -0
- package/dist/api/decoder.js.map +1 -0
- package/dist/api/demuxer.d.ts +1295 -0
- package/dist/api/demuxer.js +2344 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/device.d.ts +589 -0
- package/dist/api/device.js +964 -0
- package/dist/api/device.js.map +1 -0
- package/dist/api/encoder-pool.d.ts +262 -0
- package/dist/api/encoder-pool.js +397 -0
- package/dist/api/encoder-pool.js.map +1 -0
- package/dist/api/encoder.d.ts +1509 -0
- package/dist/api/encoder.js +2619 -0
- package/dist/api/encoder.js.map +1 -0
- package/dist/api/filter-complex.d.ts +828 -0
- package/dist/api/filter-complex.js +1661 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +800 -0
- package/dist/api/filter-presets.js +1554 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +1339 -0
- package/dist/api/filter.js +2075 -0
- package/dist/api/filter.js.map +1 -0
- package/dist/api/fmp4-stream.d.ts +797 -0
- package/dist/api/fmp4-stream.js +1428 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +677 -0
- package/dist/api/hardware.js +1300 -0
- package/dist/api/hardware.js.map +1 -0
- package/dist/api/index.d.ts +21 -0
- package/dist/api/index.js +38 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/io-stream.d.ts +308 -0
- package/dist/api/io-stream.js +358 -0
- package/dist/api/io-stream.js.map +1 -0
- package/dist/api/muxer.d.ts +1311 -0
- package/dist/api/muxer.js +2672 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +801 -0
- package/dist/api/pipeline.js +1344 -0
- package/dist/api/pipeline.js.map +1 -0
- package/dist/api/probe.d.ts +128 -0
- package/dist/api/probe.js +227 -0
- package/dist/api/probe.js.map +1 -0
- package/dist/api/rtp-stream.d.ts +497 -0
- package/dist/api/rtp-stream.js +1131 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/scaler.d.ts +561 -0
- package/dist/api/scaler.js +918 -0
- package/dist/api/scaler.js.map +1 -0
- package/dist/api/types.d.ts +17 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +149 -0
- package/dist/api/utilities/async-queue.js +254 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +117 -0
- package/dist/api/utilities/audio-sample.js +112 -0
- package/dist/api/utilities/audio-sample.js.map +1 -0
- package/dist/api/utilities/audio-transcode.d.ts +20 -0
- package/dist/api/utilities/audio-transcode.js +27 -0
- package/dist/api/utilities/audio-transcode.js.map +1 -0
- package/dist/api/utilities/channel-layout.d.ts +76 -0
- package/dist/api/utilities/channel-layout.js +80 -0
- package/dist/api/utilities/channel-layout.js.map +1 -0
- package/dist/api/utilities/codec-format.d.ts +87 -0
- package/dist/api/utilities/codec-format.js +117 -0
- package/dist/api/utilities/codec-format.js.map +1 -0
- package/dist/api/utilities/context-options.d.ts +25 -0
- package/dist/api/utilities/context-options.js +23 -0
- package/dist/api/utilities/context-options.js.map +1 -0
- package/dist/api/utilities/electron-shared-texture.d.ts +368 -0
- package/dist/api/utilities/electron-shared-texture.js +540 -0
- package/dist/api/utilities/electron-shared-texture.js.map +1 -0
- package/dist/api/utilities/image.d.ts +207 -0
- package/dist/api/utilities/image.js +213 -0
- package/dist/api/utilities/image.js.map +1 -0
- package/dist/api/utilities/index.d.ts +14 -0
- package/dist/api/utilities/index.js +27 -0
- package/dist/api/utilities/index.js.map +1 -0
- package/dist/api/utilities/live-decoding.d.ts +15 -0
- package/dist/api/utilities/live-decoding.js +32 -0
- package/dist/api/utilities/live-decoding.js.map +1 -0
- package/dist/api/utilities/media-type.d.ts +49 -0
- package/dist/api/utilities/media-type.js +53 -0
- package/dist/api/utilities/media-type.js.map +1 -0
- 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/utilities/pixel-format.d.ts +89 -0
- package/dist/api/utilities/pixel-format.js +97 -0
- package/dist/api/utilities/pixel-format.js.map +1 -0
- package/dist/api/utilities/sample-format.d.ts +129 -0
- package/dist/api/utilities/sample-format.js +141 -0
- package/dist/api/utilities/sample-format.js.map +1 -0
- package/dist/api/utilities/scale-size.d.ts +51 -0
- package/dist/api/utilities/scale-size.js +55 -0
- package/dist/api/utilities/scale-size.js.map +1 -0
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +186 -0
- package/dist/api/utilities/streaming.js +309 -0
- package/dist/api/utilities/streaming.js.map +1 -0
- package/dist/api/utilities/timestamp.d.ts +193 -0
- package/dist/api/utilities/timestamp.js +206 -0
- package/dist/api/utilities/timestamp.js.map +1 -0
- package/dist/api/utilities/video-transcode.d.ts +218 -0
- package/dist/api/utilities/video-transcode.js +624 -0
- package/dist/api/utilities/video-transcode.js.map +1 -0
- package/dist/api/utilities/whisper-model.d.ts +330 -0
- package/dist/api/utilities/whisper-model.js +587 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- package/dist/api/utils.d.ts +19 -0
- package/dist/api/utils.js +39 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +294 -0
- package/dist/api/webrtc-stream.js +546 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/api/whisper.d.ts +326 -0
- package/dist/api/whisper.js +402 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/bsf-options.d.ts +364 -0
- package/dist/constants/bsf-options.js +7 -0
- package/dist/constants/bsf-options.js.map +1 -0
- package/dist/constants/channel-layouts.d.ts +53 -0
- package/dist/constants/channel-layouts.js +57 -0
- package/dist/constants/channel-layouts.js.map +1 -0
- package/dist/constants/constants.d.ts +2435 -0
- package/dist/constants/constants.js +1989 -0
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +651 -0
- package/dist/constants/decoders.js +641 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +314 -0
- package/dist/constants/encoders.js +309 -0
- package/dist/constants/encoders.js.map +1 -0
- package/dist/constants/filter-options.d.ts +12099 -0
- package/dist/constants/filter-options.js +7 -0
- package/dist/constants/filter-options.js.map +1 -0
- package/dist/constants/format-options.d.ts +4319 -0
- package/dist/constants/format-options.js +7 -0
- package/dist/constants/format-options.js.map +1 -0
- package/dist/constants/formats.d.ts +18 -0
- package/dist/constants/formats.js +7 -0
- package/dist/constants/formats.js.map +1 -0
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +10 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/options.d.ts +6217 -0
- package/dist/constants/options.js +7 -0
- package/dist/constants/options.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +101 -0
- package/dist/ffmpeg/index.js +161 -0
- package/dist/ffmpeg/index.js.map +1 -0
- package/dist/ffmpeg/version.d.ts +6 -0
- package/dist/ffmpeg/version.js +7 -0
- package/dist/ffmpeg/version.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/audio-fifo.d.ts +399 -0
- package/dist/lib/audio-fifo.js +431 -0
- package/dist/lib/audio-fifo.js.map +1 -0
- package/dist/lib/binding.d.ts +242 -0
- package/dist/lib/binding.js +102 -0
- package/dist/lib/binding.js.map +1 -0
- package/dist/lib/bitstream-filter-context.d.ts +381 -0
- package/dist/lib/bitstream-filter-context.js +443 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -0
- package/dist/lib/bitstream-filter.d.ts +140 -0
- package/dist/lib/bitstream-filter.js +154 -0
- package/dist/lib/bitstream-filter.js.map +1 -0
- package/dist/lib/codec-context.d.ts +1069 -0
- package/dist/lib/codec-context.js +1336 -0
- package/dist/lib/codec-context.js.map +1 -0
- package/dist/lib/codec-parameters.d.ts +664 -0
- package/dist/lib/codec-parameters.js +813 -0
- package/dist/lib/codec-parameters.js.map +1 -0
- package/dist/lib/codec-parser.d.ts +201 -0
- package/dist/lib/codec-parser.js +213 -0
- package/dist/lib/codec-parser.js.map +1 -0
- package/dist/lib/codec.d.ts +617 -0
- package/dist/lib/codec.js +746 -0
- package/dist/lib/codec.js.map +1 -0
- package/dist/lib/device.d.ts +294 -0
- package/dist/lib/device.js +326 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/dictionary.d.ts +333 -0
- package/dist/lib/dictionary.js +372 -0
- package/dist/lib/dictionary.js.map +1 -0
- package/dist/lib/error.d.ts +311 -0
- package/dist/lib/error.js +395 -0
- package/dist/lib/error.js.map +1 -0
- package/dist/lib/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +714 -0
- package/dist/lib/filter-context.js +783 -0
- package/dist/lib/filter-context.js.map +1 -0
- package/dist/lib/filter-graph-segment.d.ts +161 -0
- package/dist/lib/filter-graph-segment.js +172 -0
- package/dist/lib/filter-graph-segment.js.map +1 -0
- package/dist/lib/filter-graph.d.ts +641 -0
- package/dist/lib/filter-graph.js +704 -0
- package/dist/lib/filter-graph.js.map +1 -0
- package/dist/lib/filter-inout.d.ts +202 -0
- package/dist/lib/filter-inout.js +261 -0
- package/dist/lib/filter-inout.js.map +1 -0
- package/dist/lib/filter.d.ts +243 -0
- package/dist/lib/filter.js +272 -0
- package/dist/lib/filter.js.map +1 -0
- package/dist/lib/format-context.d.ts +1335 -0
- package/dist/lib/format-context.js +1455 -0
- package/dist/lib/format-context.js.map +1 -0
- package/dist/lib/frame-utils.d.ts +116 -0
- package/dist/lib/frame-utils.js +98 -0
- package/dist/lib/frame-utils.js.map +1 -0
- package/dist/lib/frame.d.ts +1315 -0
- package/dist/lib/frame.js +1535 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/hardware-device-context.d.ts +362 -0
- package/dist/lib/hardware-device-context.js +383 -0
- package/dist/lib/hardware-device-context.js.map +1 -0
- package/dist/lib/hardware-frames-context.d.ts +418 -0
- package/dist/lib/hardware-frames-context.js +471 -0
- package/dist/lib/hardware-frames-context.js.map +1 -0
- package/dist/lib/index.d.ts +35 -0
- package/dist/lib/index.js +60 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/input-format.d.ts +249 -0
- package/dist/lib/input-format.js +306 -0
- package/dist/lib/input-format.js.map +1 -0
- package/dist/lib/io-context.d.ts +698 -0
- package/dist/lib/io-context.js +771 -0
- package/dist/lib/io-context.js.map +1 -0
- package/dist/lib/log.d.ts +174 -0
- package/dist/lib/log.js +184 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/native-types.d.ts +1024 -0
- package/dist/lib/native-types.js +2 -0
- package/dist/lib/native-types.js.map +1 -0
- package/dist/lib/option.d.ts +953 -0
- package/dist/lib/option.js +1607 -0
- package/dist/lib/option.js.map +1 -0
- package/dist/lib/output-format.d.ts +222 -0
- package/dist/lib/output-format.js +259 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/lib/packet.d.ts +521 -0
- package/dist/lib/packet.js +613 -0
- package/dist/lib/packet.js.map +1 -0
- package/dist/lib/rational.d.ts +251 -0
- package/dist/lib/rational.js +278 -0
- package/dist/lib/rational.js.map +1 -0
- package/dist/lib/software-resample-context.d.ts +552 -0
- package/dist/lib/software-resample-context.js +592 -0
- package/dist/lib/software-resample-context.js.map +1 -0
- package/dist/lib/software-scale-context.d.ts +344 -0
- package/dist/lib/software-scale-context.js +366 -0
- package/dist/lib/software-scale-context.js.map +1 -0
- package/dist/lib/stream.d.ts +384 -0
- package/dist/lib/stream.js +525 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +34 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utilities.d.ts +1198 -0
- package/dist/lib/utilities.js +1302 -0
- package/dist/lib/utilities.js.map +1 -0
- package/dist/utils/electron.d.ts +49 -0
- package/dist/utils/electron.js +63 -0
- package/dist/utils/electron.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/libc.d.ts +11 -0
- package/dist/utils/libc.js +35 -0
- package/dist/utils/libc.js.map +1 -0
- package/dist/utils/variant.d.ts +26 -0
- package/dist/utils/variant.js +31 -0
- package/dist/utils/variant.js.map +1 -0
- package/dist/webrtc/index.d.ts +4 -0
- package/dist/webrtc/index.js +7 -0
- package/dist/webrtc/index.js.map +1 -0
- package/install/check.js +124 -0
- package/install/ffmpeg.js +66 -0
- package/package.json +165 -3
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
import { AV_CODEC_FLAG_QSCALE, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_D3D12VA, 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
|
+
import { FF_ENCODER_MJPEG, FF_ENCODER_PNG } from '../constants/encoders.js';
|
|
3
|
+
import { bindings } from '../lib/binding.js';
|
|
4
|
+
import { FFmpegError } from '../lib/error.js';
|
|
5
|
+
import { Frame } from '../lib/frame.js';
|
|
6
|
+
import { EncoderPool, Mutex } from './encoder-pool.js';
|
|
7
|
+
import { FilterPreset } from './filter-presets.js';
|
|
8
|
+
import { FilterAPI } from './filter.js';
|
|
9
|
+
const FORMAT_TO_PIXFMT = {
|
|
10
|
+
rgb: AV_PIX_FMT_RGB24,
|
|
11
|
+
rgba: AV_PIX_FMT_RGBA,
|
|
12
|
+
gray: AV_PIX_FMT_GRAY8,
|
|
13
|
+
nv12: AV_PIX_FMT_NV12,
|
|
14
|
+
yuv420p: AV_PIX_FMT_YUV420P,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Devices whose GPU scaler ignores the crop metadata (scale_d3d11, scale_d3d12).
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const CROP_BLIND_DEVICES = [AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_D3D12VA];
|
|
22
|
+
/**
|
|
23
|
+
* High-level image scaler, cropper, and pixel-format converter.
|
|
24
|
+
*
|
|
25
|
+
* Scales, crops, and converts decoded frames to raw pixel buffers or encoded
|
|
26
|
+
* JPEG/PNG images. Built for the detection/thumbnail/snapshot workload: one
|
|
27
|
+
* instance pools its swscale contexts, GPU filter graphs, and encoders, so
|
|
28
|
+
* reusing it for every frame and every crop avoids per-frame allocation.
|
|
29
|
+
* Software frames are scaled with swscale; hardware frames are cropped, scaled,
|
|
30
|
+
* and converted on the GPU with only the small result downloaded.
|
|
31
|
+
*
|
|
32
|
+
* Safe for concurrent use: calls that share a cached graph or pooled encoder are
|
|
33
|
+
* serialized on that resource, while distinct output configurations run fully in
|
|
34
|
+
* parallel, and cache eviction never disposes a resource that is still in flight.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { Scaler } from 'node-av/api';
|
|
39
|
+
*
|
|
40
|
+
* using scaler = new Scaler({ hardware });
|
|
41
|
+
*
|
|
42
|
+
* // Scale + convert to RGB (on the GPU for hardware frames)
|
|
43
|
+
* const rgb = await scaler.toBuffer(frame, { resize: { width: 640, height: 360 }, format: 'rgb' });
|
|
44
|
+
*
|
|
45
|
+
* // Crop a detected region and encode a snapshot
|
|
46
|
+
* const jpeg = await scaler.toJpeg(frame, { crop: { x, y, width: w, height: h }, quality: 85 });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @see {@link EncoderPool} For pooled image encoders
|
|
50
|
+
* @see {@link FilterAPI} For streaming filtergraphs
|
|
51
|
+
*/
|
|
52
|
+
export class Scaler {
|
|
53
|
+
native;
|
|
54
|
+
syncNative;
|
|
55
|
+
nativeLock = new Mutex();
|
|
56
|
+
hardware;
|
|
57
|
+
flags;
|
|
58
|
+
maxCacheSize;
|
|
59
|
+
graphs = new Map();
|
|
60
|
+
jpegPool;
|
|
61
|
+
pngPool;
|
|
62
|
+
disposed = false;
|
|
63
|
+
/**
|
|
64
|
+
* Create a new scaler.
|
|
65
|
+
*
|
|
66
|
+
* @param options - Scaler options
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* using scaler = new Scaler({ hardware: HardwareContext.auto() });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
constructor(options = {}) {
|
|
74
|
+
this.flags = options.flags ?? SWS_BILINEAR;
|
|
75
|
+
this.hardware = options.hardware;
|
|
76
|
+
this.maxCacheSize = options.maxCacheSize ?? 16;
|
|
77
|
+
this.native = new bindings.Scaler(this.flags);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Scale, crop, and/or convert a frame and return the raw pixel data.
|
|
81
|
+
*
|
|
82
|
+
* The returned buffer is tightly packed (no row padding). For planar formats
|
|
83
|
+
* (`nv12`, `yuv420p`) planes are concatenated.
|
|
84
|
+
*
|
|
85
|
+
* @param frame - Source frame (software or hardware; hardware frames are processed on the GPU and downloaded)
|
|
86
|
+
*
|
|
87
|
+
* @param options - Crop, resize, format, and output buffer options
|
|
88
|
+
*
|
|
89
|
+
* @returns Tightly packed pixel data (a view of `options.output` when given)
|
|
90
|
+
*
|
|
91
|
+
* @throws {FFmpegError} If scaling fails
|
|
92
|
+
*
|
|
93
|
+
* @throws {TypeError} If `options.output` is not a Buffer or is smaller than the result
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const gray = await scaler.toBuffer(frame, { resize: { width: 320, height: 180 }, format: 'gray' });
|
|
98
|
+
*
|
|
99
|
+
* // Reuse one buffer across frames
|
|
100
|
+
* const reused = Buffer.allocUnsafe(320 * 180);
|
|
101
|
+
* await scaler.toBuffer(frame, { resize: { width: 320, height: 180 }, format: 'gray', output: reused });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @see {@link toBufferSync} For synchronous version
|
|
105
|
+
*/
|
|
106
|
+
async toBuffer(frame, options = {}) {
|
|
107
|
+
if (this.disposed) {
|
|
108
|
+
throw new Error('Scaler has been disposed');
|
|
109
|
+
}
|
|
110
|
+
// The native side accepts any Uint8Array, but the result would then not be a Buffer
|
|
111
|
+
if (options.output !== undefined && !Buffer.isBuffer(options.output)) {
|
|
112
|
+
throw new TypeError('output must be a Buffer');
|
|
113
|
+
}
|
|
114
|
+
if (frame.isHwFrame()) {
|
|
115
|
+
// With a hardware context, crop/scale/convert on the GPU and download only
|
|
116
|
+
// the small result. Without one, or where the GPU cannot do it, fall back to
|
|
117
|
+
// downloading the full frame and processing it in software.
|
|
118
|
+
if (!this.needsDownload(frame, options.crop)) {
|
|
119
|
+
const out = await this.toFrame(frame, options.crop, options.resize, options.format ?? 'nv12');
|
|
120
|
+
try {
|
|
121
|
+
return out.toBuffer(options.output);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
out.free();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const sw = await this.downloadToSoftware(frame);
|
|
128
|
+
try {
|
|
129
|
+
return await this.processNative(sw, options);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
sw.free();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return await this.processNative(frame, options);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Scale, crop, and/or convert a frame and return the raw pixel data.
|
|
139
|
+
* Synchronous version of toBuffer.
|
|
140
|
+
*
|
|
141
|
+
* @param frame - Source frame (software or hardware; hardware frames are processed on the GPU and downloaded)
|
|
142
|
+
*
|
|
143
|
+
* @param options - Crop, resize, format, and output buffer options
|
|
144
|
+
*
|
|
145
|
+
* @returns Tightly packed pixel data (a view of `options.output` when given)
|
|
146
|
+
*
|
|
147
|
+
* @throws {FFmpegError} If scaling fails
|
|
148
|
+
*
|
|
149
|
+
* @throws {TypeError} If `options.output` is not a Buffer or is smaller than the result
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const gray = scaler.toBufferSync(frame, { resize: { width: 320, height: 180 }, format: 'gray' });
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @see {@link toBuffer} For async version
|
|
157
|
+
*/
|
|
158
|
+
toBufferSync(frame, options = {}) {
|
|
159
|
+
if (this.disposed) {
|
|
160
|
+
throw new Error('Scaler has been disposed');
|
|
161
|
+
}
|
|
162
|
+
if (options.output !== undefined && !Buffer.isBuffer(options.output)) {
|
|
163
|
+
throw new TypeError('output must be a Buffer');
|
|
164
|
+
}
|
|
165
|
+
if (frame.isHwFrame()) {
|
|
166
|
+
if (!this.needsDownload(frame, options.crop)) {
|
|
167
|
+
const out = this.toFrameSync(frame, options.crop, options.resize, options.format ?? 'nv12');
|
|
168
|
+
try {
|
|
169
|
+
return out.toBuffer(options.output);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
out.free();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const sw = this.downloadToSoftwareSync(frame);
|
|
176
|
+
try {
|
|
177
|
+
return this.getSyncNative().processSync(sw.getNative(), options);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
sw.free();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return this.getSyncNative().processSync(frame.getNative(), options);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Scale/crop a frame and encode it to a JPEG image.
|
|
187
|
+
*
|
|
188
|
+
* The encoder is pooled per output size and reused across calls - no encoder is
|
|
189
|
+
* rebuilt per snapshot. Hardware frames are scaled on the GPU and downloaded;
|
|
190
|
+
* software frames are scaled with swscale.
|
|
191
|
+
*
|
|
192
|
+
* @param frame - Source frame (software or hardware)
|
|
193
|
+
*
|
|
194
|
+
* @param options - Crop, resize, and quality options
|
|
195
|
+
*
|
|
196
|
+
* @returns Encoded JPEG bytes
|
|
197
|
+
*
|
|
198
|
+
* @throws {FFmpegError} If scaling or encoding fails
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const jpeg = await scaler.toJpeg(frame, { resize: { width: 1280, height: 720 }, quality: 85 });
|
|
203
|
+
* await writeFile('snapshot.jpg', jpeg);
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @see {@link toJpegSync} For synchronous version
|
|
207
|
+
*/
|
|
208
|
+
async toJpeg(frame, options = {}) {
|
|
209
|
+
if (this.disposed) {
|
|
210
|
+
throw new Error('Scaler has been disposed');
|
|
211
|
+
}
|
|
212
|
+
const quality = options.quality ?? 90;
|
|
213
|
+
const downloaded = this.needsDownload(frame, options.crop) ? await this.downloadToSoftware(frame) : null;
|
|
214
|
+
try {
|
|
215
|
+
// mjpeg wants planar YUV; tag it full-range so colors are correct without the
|
|
216
|
+
// deprecated yuvj* formats.
|
|
217
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
218
|
+
try {
|
|
219
|
+
this.prepareJpegFrame(out, quality);
|
|
220
|
+
return await this.getJpegPool().encode(out);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
out.free();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
downloaded?.free();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Scale/crop a frame and encode it to a JPEG image.
|
|
232
|
+
* Synchronous version of toJpeg.
|
|
233
|
+
*
|
|
234
|
+
* @param frame - Source frame (software or hardware)
|
|
235
|
+
*
|
|
236
|
+
* @param options - Crop, resize, and quality options
|
|
237
|
+
*
|
|
238
|
+
* @returns Encoded JPEG bytes
|
|
239
|
+
*
|
|
240
|
+
* @throws {FFmpegError} If scaling or encoding fails
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const jpeg = scaler.toJpegSync(frame, { resize: { width: 1280, height: 720 }, quality: 85 });
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* @see {@link toJpeg} For async version
|
|
248
|
+
*/
|
|
249
|
+
toJpegSync(frame, options = {}) {
|
|
250
|
+
if (this.disposed) {
|
|
251
|
+
throw new Error('Scaler has been disposed');
|
|
252
|
+
}
|
|
253
|
+
const quality = options.quality ?? 90;
|
|
254
|
+
const downloaded = this.needsDownload(frame, options.crop) ? this.downloadToSoftwareSync(frame) : null;
|
|
255
|
+
try {
|
|
256
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, 'yuv420p');
|
|
257
|
+
try {
|
|
258
|
+
this.prepareJpegFrame(out, quality);
|
|
259
|
+
return this.getJpegPool().encodeSync(out);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
out.free();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
downloaded?.free();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Scale/crop a frame and encode it to a PNG image.
|
|
271
|
+
*
|
|
272
|
+
* The encoder is pooled per output size/format and reused across calls. Hardware
|
|
273
|
+
* frames are scaled on the GPU and downloaded; software frames use swscale.
|
|
274
|
+
*
|
|
275
|
+
* @param frame - Source frame (software or hardware)
|
|
276
|
+
*
|
|
277
|
+
* @param options - Crop, resize, and color-format options
|
|
278
|
+
*
|
|
279
|
+
* @returns Encoded PNG bytes
|
|
280
|
+
*
|
|
281
|
+
* @throws {FFmpegError} If scaling or encoding fails
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const png = await scaler.toPng(frame, { crop: { x, y, width: w, height: h } });
|
|
286
|
+
* await writeFile('region.png', png);
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* @see {@link toPngSync} For synchronous version
|
|
290
|
+
*/
|
|
291
|
+
async toPng(frame, options = {}) {
|
|
292
|
+
if (this.disposed) {
|
|
293
|
+
throw new Error('Scaler has been disposed');
|
|
294
|
+
}
|
|
295
|
+
const format = options.format ?? 'rgb';
|
|
296
|
+
const downloaded = this.needsDownload(frame, options.crop) ? await this.downloadToSoftware(frame) : null;
|
|
297
|
+
try {
|
|
298
|
+
const out = await this.toFrame(downloaded ?? frame, options.crop, options.resize, format);
|
|
299
|
+
try {
|
|
300
|
+
return await this.getPngPool().encode(out);
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
out.free();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
downloaded?.free();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Scale/crop a frame and encode it to a PNG image.
|
|
312
|
+
* Synchronous version of toPng.
|
|
313
|
+
*
|
|
314
|
+
* @param frame - Source frame (software or hardware)
|
|
315
|
+
*
|
|
316
|
+
* @param options - Crop, resize, and color-format options
|
|
317
|
+
*
|
|
318
|
+
* @returns Encoded PNG bytes
|
|
319
|
+
*
|
|
320
|
+
* @throws {FFmpegError} If scaling or encoding fails
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const png = scaler.toPngSync(frame, { crop: { x, y, width: w, height: h } });
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* @see {@link toPng} For async version
|
|
328
|
+
*/
|
|
329
|
+
toPngSync(frame, options = {}) {
|
|
330
|
+
if (this.disposed) {
|
|
331
|
+
throw new Error('Scaler has been disposed');
|
|
332
|
+
}
|
|
333
|
+
const format = options.format ?? 'rgb';
|
|
334
|
+
const downloaded = this.needsDownload(frame, options.crop) ? this.downloadToSoftwareSync(frame) : null;
|
|
335
|
+
try {
|
|
336
|
+
const out = this.toFrameSync(downloaded ?? frame, options.crop, options.resize, format);
|
|
337
|
+
try {
|
|
338
|
+
return this.getPngPool().encodeSync(out);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
out.free();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
downloaded?.free();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Release all pooled scaling contexts, graphs, and frames.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* scaler.close();
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
close() {
|
|
357
|
+
if (this.disposed) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.disposed = true;
|
|
361
|
+
this.native.close();
|
|
362
|
+
this.syncNative?.close();
|
|
363
|
+
this.syncNative = undefined;
|
|
364
|
+
// In-use graphs are retired, not disposed: the last in-flight scale disposes
|
|
365
|
+
// them on release, so close() during concurrent calls never crashes them.
|
|
366
|
+
for (const entry of this.graphs.values()) {
|
|
367
|
+
this.retireEntry(entry);
|
|
368
|
+
}
|
|
369
|
+
this.graphs.clear();
|
|
370
|
+
this.jpegPool?.close();
|
|
371
|
+
this.jpegPool = undefined;
|
|
372
|
+
this.pngPool?.close();
|
|
373
|
+
this.pngPool = undefined;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Whether a frame has to be downloaded before it is cropped and scaled.
|
|
377
|
+
*
|
|
378
|
+
* Without a hardware context, or on a device without a GPU scaler, hardware
|
|
379
|
+
* frames are processed in software. The D3D11VA and D3D12VA scalers run on
|
|
380
|
+
* the GPU but ignore the crop, so only uncropped frames stay there.
|
|
381
|
+
*
|
|
382
|
+
* @param frame - Source frame
|
|
383
|
+
*
|
|
384
|
+
* @param crop - Requested crop region
|
|
385
|
+
*
|
|
386
|
+
* @returns True for a hardware frame the GPU graph cannot handle
|
|
387
|
+
*
|
|
388
|
+
* @internal
|
|
389
|
+
*/
|
|
390
|
+
needsDownload(frame, crop) {
|
|
391
|
+
if (!frame.isHwFrame()) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (!this.hardware || !FilterPreset.chain(this.hardware).supports('scale')) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
const cropped = crop !== undefined && (crop.x !== 0 || crop.y !== 0 || crop.width !== frame.width || crop.height !== frame.height);
|
|
398
|
+
return cropped && CROP_BLIND_DEVICES.includes(this.hardware.deviceType);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Download a hardware frame to system memory for the software path.
|
|
402
|
+
*
|
|
403
|
+
* Allocates a fresh target per call - concurrent downloads must not share a
|
|
404
|
+
* staging frame. The frame's own hwframe context selects a supported software
|
|
405
|
+
* format. The caller frees the returned frame.
|
|
406
|
+
*
|
|
407
|
+
* @param frame - Hardware source frame
|
|
408
|
+
*
|
|
409
|
+
* @returns Software frame (caller frees)
|
|
410
|
+
*
|
|
411
|
+
* @throws {FFmpegError} If the transfer fails
|
|
412
|
+
*
|
|
413
|
+
* @internal
|
|
414
|
+
*/
|
|
415
|
+
async downloadToSoftware(frame) {
|
|
416
|
+
const dst = new Frame();
|
|
417
|
+
dst.alloc();
|
|
418
|
+
const ret = await frame.hwframeTransferData(dst, 0);
|
|
419
|
+
if (ret < 0) {
|
|
420
|
+
dst.free();
|
|
421
|
+
}
|
|
422
|
+
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
423
|
+
dst.copyProps(frame);
|
|
424
|
+
return dst;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Synchronous version of downloadToSoftware.
|
|
428
|
+
*
|
|
429
|
+
* @param frame - Hardware source frame
|
|
430
|
+
*
|
|
431
|
+
* @returns Software frame (caller frees)
|
|
432
|
+
*
|
|
433
|
+
* @throws {FFmpegError} If the transfer fails
|
|
434
|
+
*
|
|
435
|
+
* @internal
|
|
436
|
+
*/
|
|
437
|
+
downloadToSoftwareSync(frame) {
|
|
438
|
+
const dst = new Frame();
|
|
439
|
+
dst.alloc();
|
|
440
|
+
const ret = frame.hwframeTransferDataSync(dst, 0);
|
|
441
|
+
if (ret < 0) {
|
|
442
|
+
dst.free();
|
|
443
|
+
}
|
|
444
|
+
FFmpegError.throwIfError(ret, 'Failed to download hardware frame');
|
|
445
|
+
dst.copyProps(frame);
|
|
446
|
+
return dst;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Run the pooled native software scaler, serialized per instance.
|
|
450
|
+
*
|
|
451
|
+
* The native scaler reuses staging frames and sws contexts per configuration;
|
|
452
|
+
* overlapping async calls would race on them in the worker threads.
|
|
453
|
+
*
|
|
454
|
+
* @param frame - Software source frame
|
|
455
|
+
*
|
|
456
|
+
* @param options - Crop, resize, and format options
|
|
457
|
+
*
|
|
458
|
+
* @returns Tightly packed pixel data
|
|
459
|
+
*
|
|
460
|
+
* @throws {FFmpegError} If scaling fails
|
|
461
|
+
*
|
|
462
|
+
* @internal
|
|
463
|
+
*/
|
|
464
|
+
async processNative(frame, options) {
|
|
465
|
+
return await this.nativeLock.run(() => this.native.process(frame.getNative(), options));
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The native scaler used by the sync path (lazily created).
|
|
469
|
+
*
|
|
470
|
+
* Sync calls block the event loop but can land while an async call's worker is
|
|
471
|
+
* scaling in the background; a separate native instance keeps them off the async
|
|
472
|
+
* scaler's pooled contexts, which are not safe to share across threads.
|
|
473
|
+
*
|
|
474
|
+
* @returns The sync-path native scaler
|
|
475
|
+
*
|
|
476
|
+
* @internal
|
|
477
|
+
*/
|
|
478
|
+
getSyncNative() {
|
|
479
|
+
this.syncNative ??= new bindings.Scaler(this.flags);
|
|
480
|
+
return this.syncNative;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Scale/crop/convert a frame through a cached filter graph and return the
|
|
484
|
+
* resulting frame (caller must free it).
|
|
485
|
+
*
|
|
486
|
+
* Software frames are scaled with swscale filters; hardware frames are
|
|
487
|
+
* cropped/scaled/converted on the GPU and downloaded. The crop region is
|
|
488
|
+
* reconfigured per frame via a runtime command on a graph shared per output
|
|
489
|
+
* size (except OpenCL, where the crop is baked in).
|
|
490
|
+
*
|
|
491
|
+
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
492
|
+
*
|
|
493
|
+
* @param crop - Crop region, or undefined for the full frame
|
|
494
|
+
*
|
|
495
|
+
* @param resize - Target dimensions, or undefined to keep the cropped size
|
|
496
|
+
*
|
|
497
|
+
* @param format - Output pixel format
|
|
498
|
+
*
|
|
499
|
+
* @returns The scaled output frame (caller frees)
|
|
500
|
+
*
|
|
501
|
+
* @throws {Error} If the crop is out of bounds or the graph produces no frame
|
|
502
|
+
*
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
async toFrame(frame, crop, resize, format) {
|
|
506
|
+
// Retry loop: an entry can be retired while we queue on its lock (QSV flush
|
|
507
|
+
// eviction, mid-stream failure, or LRU pressure from concurrent calls). The
|
|
508
|
+
// next attempt then resolves a freshly built graph.
|
|
509
|
+
for (;;) {
|
|
510
|
+
if (this.disposed) {
|
|
511
|
+
throw new Error('Scaler has been disposed');
|
|
512
|
+
}
|
|
513
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
514
|
+
const { entry, key } = acq;
|
|
515
|
+
// Hold the entry across the awaits below: eviction of an in-use graph is
|
|
516
|
+
// deferred to releaseGraph(), never disposing it mid-scale.
|
|
517
|
+
entry.refs++;
|
|
518
|
+
try {
|
|
519
|
+
// Serialize crop command + process + receive per graph: concurrent scales
|
|
520
|
+
// on the same output config would otherwise interleave on one FilterAPI
|
|
521
|
+
// and hand frames to the wrong caller.
|
|
522
|
+
const out = await entry.lock.run(async () => {
|
|
523
|
+
if (entry.evicted) {
|
|
524
|
+
return null; // Retired while we queued - retry with a fresh graph
|
|
525
|
+
}
|
|
526
|
+
this.prepareGraph(acq, frame);
|
|
527
|
+
try {
|
|
528
|
+
await entry.filter.process(frame);
|
|
529
|
+
let received = await entry.filter.receive();
|
|
530
|
+
if (!(received instanceof Frame)) {
|
|
531
|
+
// Some hardware scalers (notably QSV's vpp_qsv) buffer internally and don't
|
|
532
|
+
// emit a frame synchronously after a single input - the buffersink returns
|
|
533
|
+
// EAGAIN. Flush to force the pending frame out, then evict the now-EOF'd
|
|
534
|
+
// graph so the next call rebuilds it. Zero-latency scalers (scale_vaapi,
|
|
535
|
+
// scale_vt) deliver on the first receive and never hit this path.
|
|
536
|
+
await entry.filter.flush();
|
|
537
|
+
received = await entry.filter.receive();
|
|
538
|
+
this.evictGraph(key, entry);
|
|
539
|
+
}
|
|
540
|
+
if (!(received instanceof Frame)) {
|
|
541
|
+
throw new Error('Scaler: filter produced no frame');
|
|
542
|
+
}
|
|
543
|
+
return received;
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
// The graph is in an unknown mid-stream state; retire it so the next
|
|
547
|
+
// call rebuilds instead of inheriting the wreckage.
|
|
548
|
+
this.evictGraph(key, entry);
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
if (out) {
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
this.releaseGraph(entry);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Synchronous version of toFrame.
|
|
563
|
+
*
|
|
564
|
+
* Sync scales cannot interleave with each other (they block the event loop),
|
|
565
|
+
* but they can land between the awaits of an in-flight async scale; a busy
|
|
566
|
+
* cached graph is bypassed with a throwaway graph in that case.
|
|
567
|
+
*
|
|
568
|
+
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
569
|
+
*
|
|
570
|
+
* @param crop - Crop region, or undefined for the full frame
|
|
571
|
+
*
|
|
572
|
+
* @param resize - Target dimensions, or undefined to keep the cropped size
|
|
573
|
+
*
|
|
574
|
+
* @param format - Output pixel format
|
|
575
|
+
*
|
|
576
|
+
* @returns The scaled output frame (caller frees)
|
|
577
|
+
*
|
|
578
|
+
* @internal
|
|
579
|
+
*/
|
|
580
|
+
toFrameSync(frame, crop, resize, format) {
|
|
581
|
+
const acq = this.resolveGraph(frame, crop, resize, format);
|
|
582
|
+
const { entry, key } = acq;
|
|
583
|
+
if (entry.refs > 0) {
|
|
584
|
+
return this.scaleOneShotSync(frame, acq);
|
|
585
|
+
}
|
|
586
|
+
this.prepareGraph(acq, frame);
|
|
587
|
+
try {
|
|
588
|
+
entry.filter.processSync(frame);
|
|
589
|
+
let out = entry.filter.receiveSync();
|
|
590
|
+
if (!(out instanceof Frame)) {
|
|
591
|
+
// See toFrame: QSV's vpp_qsv buffers a frame; flush to drain it and evict
|
|
592
|
+
// the now-EOF'd graph so the next call rebuilds.
|
|
593
|
+
entry.filter.flushSync();
|
|
594
|
+
out = entry.filter.receiveSync();
|
|
595
|
+
this.evictGraph(key, entry);
|
|
596
|
+
}
|
|
597
|
+
if (!(out instanceof Frame)) {
|
|
598
|
+
throw new Error('Scaler: filter produced no frame');
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
this.evictGraph(key, entry);
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Scale through a throwaway graph with the crop baked in, bypassing the cache.
|
|
609
|
+
*
|
|
610
|
+
* @param frame - Source frame
|
|
611
|
+
*
|
|
612
|
+
* @param acq - Resolved operation configuration
|
|
613
|
+
*
|
|
614
|
+
* @returns The scaled output frame (caller frees)
|
|
615
|
+
*
|
|
616
|
+
* @throws {Error} If the graph produces no frame
|
|
617
|
+
*
|
|
618
|
+
* @internal
|
|
619
|
+
*/
|
|
620
|
+
scaleOneShotSync(frame, acq) {
|
|
621
|
+
const filter = FilterAPI.create(this.buildGraph(acq.isHw, acq.outW, acq.outH, acq.format, acq.crop, false), acq.isHw ? { hardware: this.hardware } : {});
|
|
622
|
+
try {
|
|
623
|
+
filter.processSync(frame);
|
|
624
|
+
let out = filter.receiveSync();
|
|
625
|
+
if (!(out instanceof Frame)) {
|
|
626
|
+
filter.flushSync();
|
|
627
|
+
out = filter.receiveSync();
|
|
628
|
+
}
|
|
629
|
+
if (!(out instanceof Frame)) {
|
|
630
|
+
throw new Error('Scaler: filter produced no frame');
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
filter[Symbol.dispose]();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Resolve (and, on demand, build) the cached filter graph for a frame. Shared
|
|
640
|
+
* by the sync and async scaling paths; all of this work is synchronous, so a
|
|
641
|
+
* resolved entry cannot be evicted before the caller marks it in use.
|
|
642
|
+
*
|
|
643
|
+
* @param frame - Source frame (software, or hardware with a hardware context)
|
|
644
|
+
*
|
|
645
|
+
* @param crop - Crop region, or undefined for the full frame
|
|
646
|
+
*
|
|
647
|
+
* @param resize - Target dimensions, or undefined to keep the cropped size
|
|
648
|
+
*
|
|
649
|
+
* @param format - Output pixel format
|
|
650
|
+
*
|
|
651
|
+
* @returns The cached graph entry and the resolved operation configuration
|
|
652
|
+
*
|
|
653
|
+
* @throws {Error} If the crop is out of bounds
|
|
654
|
+
*
|
|
655
|
+
* @internal
|
|
656
|
+
*/
|
|
657
|
+
resolveGraph(frame, crop, resize, format) {
|
|
658
|
+
const isHw = frame.isHwFrame();
|
|
659
|
+
if (isHw && !this.hardware) {
|
|
660
|
+
throw new Error('Scaler received a hardware frame but was created without a HardwareContext');
|
|
661
|
+
}
|
|
662
|
+
const cropW = crop?.width ?? frame.width;
|
|
663
|
+
const cropH = crop?.height ?? frame.height;
|
|
664
|
+
const cropX = crop?.x ?? 0;
|
|
665
|
+
const cropY = crop?.y ?? 0;
|
|
666
|
+
if (cropX < 0 || cropY < 0 || cropW <= 0 || cropH <= 0 || cropX + cropW > frame.width || cropY + cropH > frame.height) {
|
|
667
|
+
throw new Error(`Scaler: crop region {x:${cropX}, y:${cropY}, width:${cropW}, height:${cropH}} is out of bounds for ${frame.width}x${frame.height} frame`);
|
|
668
|
+
}
|
|
669
|
+
const outW = resize?.width ?? cropW;
|
|
670
|
+
const outH = resize?.height ?? cropH;
|
|
671
|
+
// The standard `crop` filter is runtime-commandable for software frames and on
|
|
672
|
+
// every hardware backend except OpenCL (where crop is folded into scale_opencl).
|
|
673
|
+
// When commandable we share one graph per output size and re-aim the crop per
|
|
674
|
+
// frame; otherwise the crop is baked in and the key must include it.
|
|
675
|
+
const commandable = !isHw || this.hardware.deviceType !== AV_HWDEVICE_TYPE_OPENCL;
|
|
676
|
+
const prefix = isHw ? 'hw' : 'sw';
|
|
677
|
+
const key = commandable ? `${prefix}_${outW}x${outH}_${format}` : `${prefix}_${cropX},${cropY},${cropW},${cropH}->${outW}x${outH}_${format}`;
|
|
678
|
+
const cropRegion = { x: cropX, y: cropY, width: cropW, height: cropH };
|
|
679
|
+
let entry = this.graphs.get(key);
|
|
680
|
+
if (!entry) {
|
|
681
|
+
// Bake the first crop in as the graph default: the graph initializes lazily
|
|
682
|
+
// on the first processed frame, before any command can be sent, so the
|
|
683
|
+
// initial crop must already be in place. Later crops use sendCommand.
|
|
684
|
+
const filter = FilterAPI.create(this.buildGraph(isHw, outW, outH, format, cropRegion, commandable), isHw ? { hardware: this.hardware } : {});
|
|
685
|
+
entry = { filter, lastCrop: commandable ? { x: cropX, y: cropY, w: cropW, h: cropH } : null, lastSrc: null, lock: new Mutex(), refs: 0, evicted: false };
|
|
686
|
+
this.cacheSet(key, entry);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// Bump LRU recency.
|
|
690
|
+
this.graphs.delete(key);
|
|
691
|
+
this.graphs.set(key, entry);
|
|
692
|
+
}
|
|
693
|
+
return { entry, key, isHw, commandable, crop: cropRegion, outW, outH, format };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Bring a cached graph in line with the current call. Must run under the entry
|
|
697
|
+
* lock (async path) or with no async call in flight (sync path).
|
|
698
|
+
*
|
|
699
|
+
* Rebuilds the graph when the source parameters changed: FilterAPI would
|
|
700
|
+
* otherwise reinitialize itself from the original graph description, silently
|
|
701
|
+
* resurrecting the crop baked in at build time instead of the current one. A
|
|
702
|
+
* graph that has not seen a frame yet is likewise rebuilt when the crop
|
|
703
|
+
* differs, because commands cannot be sent before initialization. Otherwise a
|
|
704
|
+
* changed crop is re-aimed with runtime commands - identical crops reuse the
|
|
705
|
+
* graph untouched (no config_input/config_output re-run).
|
|
706
|
+
*
|
|
707
|
+
* @param acq - Resolved graph and operation configuration
|
|
708
|
+
*
|
|
709
|
+
* @param frame - Frame about to be processed
|
|
710
|
+
*
|
|
711
|
+
* @internal
|
|
712
|
+
*/
|
|
713
|
+
prepareGraph(acq, frame) {
|
|
714
|
+
const { entry, isHw, commandable, crop, outW, outH, format } = acq;
|
|
715
|
+
const src = entry.lastSrc;
|
|
716
|
+
const srcChanged = src !== null && (src.width !== frame.width || src.height !== frame.height || src.format !== frame.format);
|
|
717
|
+
const c = entry.lastCrop;
|
|
718
|
+
const cropChanged = commandable && (c?.x !== crop.x || c?.y !== crop.y || c?.w !== crop.width || c?.h !== crop.height);
|
|
719
|
+
if (srcChanged || (cropChanged && src === null)) {
|
|
720
|
+
entry.filter[Symbol.dispose]();
|
|
721
|
+
entry.filter = FilterAPI.create(this.buildGraph(isHw, outW, outH, format, crop, commandable), isHw ? { hardware: this.hardware } : {});
|
|
722
|
+
entry.lastCrop = commandable ? { x: crop.x, y: crop.y, w: crop.width, h: crop.height } : null;
|
|
723
|
+
}
|
|
724
|
+
else if (cropChanged) {
|
|
725
|
+
// w/h are validated against the input dims (in resolveGraph); x/y are clamped
|
|
726
|
+
// per frame by the crop filter, so this order never produces a transient error.
|
|
727
|
+
entry.filter.sendCommand('crop@sc', 'w', String(crop.width));
|
|
728
|
+
entry.filter.sendCommand('crop@sc', 'h', String(crop.height));
|
|
729
|
+
entry.filter.sendCommand('crop@sc', 'x', String(crop.x));
|
|
730
|
+
entry.filter.sendCommand('crop@sc', 'y', String(crop.y));
|
|
731
|
+
entry.lastCrop = { x: crop.x, y: crop.y, w: crop.width, h: crop.height };
|
|
732
|
+
}
|
|
733
|
+
entry.lastSrc = { width: frame.width, height: frame.height, format: frame.format };
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Remove a graph from the cache and retire it.
|
|
737
|
+
*
|
|
738
|
+
* Identity-checked so a newer graph cached under the same key is left alone.
|
|
739
|
+
* Used after a flush (the graph is at EOF and can no longer accept frames) and
|
|
740
|
+
* after mid-stream failures, so the next operation rebuilds it.
|
|
741
|
+
*
|
|
742
|
+
* @param key - Cache key of the graph
|
|
743
|
+
*
|
|
744
|
+
* @param entry - The exact entry to evict
|
|
745
|
+
*
|
|
746
|
+
* @internal
|
|
747
|
+
*/
|
|
748
|
+
evictGraph(key, entry) {
|
|
749
|
+
if (this.graphs.get(key) === entry) {
|
|
750
|
+
this.graphs.delete(key);
|
|
751
|
+
}
|
|
752
|
+
this.retireEntry(entry);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Take a graph out of service, disposing it now or on last release.
|
|
756
|
+
*
|
|
757
|
+
* An entry with in-flight scales is only marked; the final {@link releaseGraph}
|
|
758
|
+
* disposes the filter once nothing uses it anymore.
|
|
759
|
+
*
|
|
760
|
+
* @param entry - Entry removed from the cache
|
|
761
|
+
*
|
|
762
|
+
* @internal
|
|
763
|
+
*/
|
|
764
|
+
retireEntry(entry) {
|
|
765
|
+
entry.evicted = true;
|
|
766
|
+
if (entry.refs === 0) {
|
|
767
|
+
entry.filter[Symbol.dispose]();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Drop an in-flight reference, disposing the graph if it was retired meanwhile.
|
|
772
|
+
*
|
|
773
|
+
* @param entry - Entry acquired by {@link toFrame}
|
|
774
|
+
*
|
|
775
|
+
* @internal
|
|
776
|
+
*/
|
|
777
|
+
releaseGraph(entry) {
|
|
778
|
+
entry.refs--;
|
|
779
|
+
if (entry.refs === 0 && entry.evicted) {
|
|
780
|
+
entry.filter[Symbol.dispose]();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Tag a frame for MJPEG encoding: full color range and a per-frame quality.
|
|
785
|
+
*
|
|
786
|
+
* MJPEG quality is carried on the frame (the encoder pool sets the QSCALE flag),
|
|
787
|
+
* so one pooled encoder serves every quality for a given output size.
|
|
788
|
+
*
|
|
789
|
+
* @param frame - Frame about to be JPEG-encoded
|
|
790
|
+
*
|
|
791
|
+
* @param quality - Quality from 1 (worst) to 100 (best)
|
|
792
|
+
*
|
|
793
|
+
* @internal
|
|
794
|
+
*/
|
|
795
|
+
prepareJpegFrame(frame, quality) {
|
|
796
|
+
frame.colorRange = AVCOL_RANGE_JPEG;
|
|
797
|
+
const q = Math.max(1, Math.min(100, quality));
|
|
798
|
+
const qscale = Math.max(2, Math.min(31, Math.round(2 + ((100 - q) / 99) * 29)));
|
|
799
|
+
frame.quality = qscale * FF_QP2LAMBDA;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* The lazily created MJPEG encoder pool (QSCALE flag set for per-frame quality).
|
|
803
|
+
*
|
|
804
|
+
* @returns The JPEG encoder pool
|
|
805
|
+
*
|
|
806
|
+
* @internal
|
|
807
|
+
*/
|
|
808
|
+
getJpegPool() {
|
|
809
|
+
this.jpegPool ??= new EncoderPool(FF_ENCODER_MJPEG, { maxSize: this.maxCacheSize, flags: AV_CODEC_FLAG_QSCALE });
|
|
810
|
+
return this.jpegPool;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* The lazily created PNG encoder pool.
|
|
814
|
+
*
|
|
815
|
+
* @returns The PNG encoder pool
|
|
816
|
+
*
|
|
817
|
+
* @internal
|
|
818
|
+
*/
|
|
819
|
+
getPngPool() {
|
|
820
|
+
this.pngPool ??= new EncoderPool(FF_ENCODER_PNG, { maxSize: this.maxCacheSize });
|
|
821
|
+
return this.pngPool;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Build a crop+scale+convert graph.
|
|
825
|
+
*
|
|
826
|
+
* For software frames this is `crop,scale,format`. For hardware frames the crop
|
|
827
|
+
* filter sets crop metadata that the hardware scaler (scale_vt/scale_cuda/
|
|
828
|
+
* scale_vaapi) applies on the GPU; the result is kept in NV12, downloaded, and
|
|
829
|
+
* converted on the CPU. Scalers with a same-size/same-format passthrough mode
|
|
830
|
+
* (scale_vaapi/scale_cuda/vpp_qsv) get `passthrough=0`, since skipping the scaler
|
|
831
|
+
* would drop the crop metadata unapplied. When `commandable` the crop is a labeled instance
|
|
832
|
+
* (`crop@sc`) whose initial region is `crop` but which is re-aimed per frame via
|
|
833
|
+
* `sendCommand`. Otherwise (OpenCL) the crop is fixed in the graph.
|
|
834
|
+
*
|
|
835
|
+
* @param isHw - Whether the input is a hardware frame
|
|
836
|
+
*
|
|
837
|
+
* @param outW - Output width
|
|
838
|
+
*
|
|
839
|
+
* @param outH - Output height
|
|
840
|
+
*
|
|
841
|
+
* @param format - Output pixel format
|
|
842
|
+
*
|
|
843
|
+
* @param crop - Initial crop region
|
|
844
|
+
*
|
|
845
|
+
* @param commandable - Whether the crop can be reconfigured at runtime
|
|
846
|
+
*
|
|
847
|
+
* @returns Filtergraph description
|
|
848
|
+
*
|
|
849
|
+
* @internal
|
|
850
|
+
*/
|
|
851
|
+
buildGraph(isHw, outW, outH, format, crop, commandable) {
|
|
852
|
+
let chain = FilterPreset.chain(isHw ? this.hardware : undefined);
|
|
853
|
+
if (commandable) {
|
|
854
|
+
// Labeled crop so sendCommand('crop@sc', ...) can re-aim it per frame.
|
|
855
|
+
chain = chain.custom(`crop@sc=${crop.width}:${crop.height}:${crop.x}:${crop.y}`);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
chain = chain.crop(crop.width, crop.height, crop.x, crop.y);
|
|
859
|
+
}
|
|
860
|
+
// The crop filter only sets crop metadata on hardware frames; the hw scaler
|
|
861
|
+
// materializes it. scale_vaapi/scale_cuda/vpp_qsv skip themselves entirely when
|
|
862
|
+
// input and output size/format match (decided at config time, before any crop
|
|
863
|
+
// command), which would silently drop the crop - so their passthrough is disabled.
|
|
864
|
+
const passthroughCapable = [AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_QSV];
|
|
865
|
+
const noPassthrough = isHw && passthroughCapable.includes(this.hardware.deviceType);
|
|
866
|
+
chain = chain.scale(outW, outH, noPassthrough ? { passthrough: 0 } : undefined);
|
|
867
|
+
if (isHw) {
|
|
868
|
+
// Keep the GPU result in NV12, download the small frame, then convert on CPU.
|
|
869
|
+
chain = chain.scaleFormat(AV_PIX_FMT_NV12).hwdownload().format(AV_PIX_FMT_NV12);
|
|
870
|
+
if (format !== 'nv12') {
|
|
871
|
+
chain = chain.format(FORMAT_TO_PIXFMT[format]);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
chain = chain.format(FORMAT_TO_PIXFMT[format]);
|
|
876
|
+
}
|
|
877
|
+
return chain.build();
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Insert a graph, evicting the least-recently-used entry if over capacity.
|
|
881
|
+
*
|
|
882
|
+
* @param key - Operation-configuration key
|
|
883
|
+
*
|
|
884
|
+
* @param entry - Graph entry to cache
|
|
885
|
+
*
|
|
886
|
+
* @internal
|
|
887
|
+
*/
|
|
888
|
+
cacheSet(key, entry) {
|
|
889
|
+
this.graphs.set(key, entry);
|
|
890
|
+
while (this.graphs.size > this.maxCacheSize) {
|
|
891
|
+
const oldest = this.graphs.keys().next().value;
|
|
892
|
+
if (oldest === undefined) {
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
const evictee = this.graphs.get(oldest);
|
|
896
|
+
this.graphs.delete(oldest);
|
|
897
|
+
if (evictee) {
|
|
898
|
+
// In-use graphs are retired, not disposed - the last in-flight scale
|
|
899
|
+
// disposes them on release.
|
|
900
|
+
this.retireEntry(evictee);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Dispose of the scaler (for `using` statements).
|
|
906
|
+
*
|
|
907
|
+
* @example
|
|
908
|
+
* ```typescript
|
|
909
|
+
* using scaler = new Scaler();
|
|
910
|
+
* // ... use scaler ...
|
|
911
|
+
* // automatically disposed at end of scope
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
[Symbol.dispose]() {
|
|
915
|
+
this.close();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
//# sourceMappingURL=scaler.js.map
|