node-av-lgpl 0.0.0 → 7.1.0-beta.1

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.
Files changed (311) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +711 -0
  3. package/dist/api/audio-frame-buffer.d.ts +636 -0
  4. package/dist/api/audio-frame-buffer.js +1027 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +938 -0
  7. package/dist/api/bitstream-filter.js +1427 -0
  8. package/dist/api/bitstream-filter.js.map +1 -0
  9. package/dist/api/constants.d.ts +44 -0
  10. package/dist/api/constants.js +45 -0
  11. package/dist/api/constants.js.map +1 -0
  12. package/dist/api/data/test_av1.ivf +0 -0
  13. package/dist/api/data/test_h264.h264 +0 -0
  14. package/dist/api/data/test_hevc.h265 +0 -0
  15. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  16. package/dist/api/data/test_vp8.ivf +0 -0
  17. package/dist/api/data/test_vp9.ivf +0 -0
  18. package/dist/api/decoder.d.ts +1413 -0
  19. package/dist/api/decoder.js +2367 -0
  20. package/dist/api/decoder.js.map +1 -0
  21. package/dist/api/demuxer.d.ts +1295 -0
  22. package/dist/api/demuxer.js +2344 -0
  23. package/dist/api/demuxer.js.map +1 -0
  24. package/dist/api/device.d.ts +589 -0
  25. package/dist/api/device.js +964 -0
  26. package/dist/api/device.js.map +1 -0
  27. package/dist/api/encoder-pool.d.ts +262 -0
  28. package/dist/api/encoder-pool.js +397 -0
  29. package/dist/api/encoder-pool.js.map +1 -0
  30. package/dist/api/encoder.d.ts +1509 -0
  31. package/dist/api/encoder.js +2619 -0
  32. package/dist/api/encoder.js.map +1 -0
  33. package/dist/api/filter-complex.d.ts +828 -0
  34. package/dist/api/filter-complex.js +1661 -0
  35. package/dist/api/filter-complex.js.map +1 -0
  36. package/dist/api/filter-presets.d.ts +800 -0
  37. package/dist/api/filter-presets.js +1554 -0
  38. package/dist/api/filter-presets.js.map +1 -0
  39. package/dist/api/filter.d.ts +1339 -0
  40. package/dist/api/filter.js +2075 -0
  41. package/dist/api/filter.js.map +1 -0
  42. package/dist/api/fmp4-stream.d.ts +797 -0
  43. package/dist/api/fmp4-stream.js +1428 -0
  44. package/dist/api/fmp4-stream.js.map +1 -0
  45. package/dist/api/hardware.d.ts +677 -0
  46. package/dist/api/hardware.js +1300 -0
  47. package/dist/api/hardware.js.map +1 -0
  48. package/dist/api/index.d.ts +21 -0
  49. package/dist/api/index.js +38 -0
  50. package/dist/api/index.js.map +1 -0
  51. package/dist/api/io-stream.d.ts +308 -0
  52. package/dist/api/io-stream.js +358 -0
  53. package/dist/api/io-stream.js.map +1 -0
  54. package/dist/api/muxer.d.ts +1311 -0
  55. package/dist/api/muxer.js +2672 -0
  56. package/dist/api/muxer.js.map +1 -0
  57. package/dist/api/pipeline.d.ts +801 -0
  58. package/dist/api/pipeline.js +1344 -0
  59. package/dist/api/pipeline.js.map +1 -0
  60. package/dist/api/probe.d.ts +128 -0
  61. package/dist/api/probe.js +227 -0
  62. package/dist/api/probe.js.map +1 -0
  63. package/dist/api/rtp-stream.d.ts +497 -0
  64. package/dist/api/rtp-stream.js +1131 -0
  65. package/dist/api/rtp-stream.js.map +1 -0
  66. package/dist/api/scaler.d.ts +561 -0
  67. package/dist/api/scaler.js +918 -0
  68. package/dist/api/scaler.js.map +1 -0
  69. package/dist/api/types.d.ts +17 -0
  70. package/dist/api/types.js +2 -0
  71. package/dist/api/types.js.map +1 -0
  72. package/dist/api/utilities/async-queue.d.ts +149 -0
  73. package/dist/api/utilities/async-queue.js +254 -0
  74. package/dist/api/utilities/async-queue.js.map +1 -0
  75. package/dist/api/utilities/audio-sample.d.ts +117 -0
  76. package/dist/api/utilities/audio-sample.js +112 -0
  77. package/dist/api/utilities/audio-sample.js.map +1 -0
  78. package/dist/api/utilities/audio-transcode.d.ts +20 -0
  79. package/dist/api/utilities/audio-transcode.js +27 -0
  80. package/dist/api/utilities/audio-transcode.js.map +1 -0
  81. package/dist/api/utilities/channel-layout.d.ts +76 -0
  82. package/dist/api/utilities/channel-layout.js +80 -0
  83. package/dist/api/utilities/channel-layout.js.map +1 -0
  84. package/dist/api/utilities/codec-format.d.ts +87 -0
  85. package/dist/api/utilities/codec-format.js +117 -0
  86. package/dist/api/utilities/codec-format.js.map +1 -0
  87. package/dist/api/utilities/context-options.d.ts +25 -0
  88. package/dist/api/utilities/context-options.js +23 -0
  89. package/dist/api/utilities/context-options.js.map +1 -0
  90. package/dist/api/utilities/electron-shared-texture.d.ts +368 -0
  91. package/dist/api/utilities/electron-shared-texture.js +540 -0
  92. package/dist/api/utilities/electron-shared-texture.js.map +1 -0
  93. package/dist/api/utilities/image.d.ts +207 -0
  94. package/dist/api/utilities/image.js +213 -0
  95. package/dist/api/utilities/image.js.map +1 -0
  96. package/dist/api/utilities/index.d.ts +14 -0
  97. package/dist/api/utilities/index.js +27 -0
  98. package/dist/api/utilities/index.js.map +1 -0
  99. package/dist/api/utilities/live-decoding.d.ts +15 -0
  100. package/dist/api/utilities/live-decoding.js +32 -0
  101. package/dist/api/utilities/live-decoding.js.map +1 -0
  102. package/dist/api/utilities/media-type.d.ts +49 -0
  103. package/dist/api/utilities/media-type.js +53 -0
  104. package/dist/api/utilities/media-type.js.map +1 -0
  105. package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
  106. package/dist/api/utilities/packet-age-watchdog.js +594 -0
  107. package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
  108. package/dist/api/utilities/pixel-format.d.ts +89 -0
  109. package/dist/api/utilities/pixel-format.js +97 -0
  110. package/dist/api/utilities/pixel-format.js.map +1 -0
  111. package/dist/api/utilities/sample-format.d.ts +129 -0
  112. package/dist/api/utilities/sample-format.js +141 -0
  113. package/dist/api/utilities/sample-format.js.map +1 -0
  114. package/dist/api/utilities/scale-size.d.ts +51 -0
  115. package/dist/api/utilities/scale-size.js +55 -0
  116. package/dist/api/utilities/scale-size.js.map +1 -0
  117. package/dist/api/utilities/scheduler.d.ts +138 -0
  118. package/dist/api/utilities/scheduler.js +98 -0
  119. package/dist/api/utilities/scheduler.js.map +1 -0
  120. package/dist/api/utilities/streaming.d.ts +186 -0
  121. package/dist/api/utilities/streaming.js +309 -0
  122. package/dist/api/utilities/streaming.js.map +1 -0
  123. package/dist/api/utilities/timestamp.d.ts +193 -0
  124. package/dist/api/utilities/timestamp.js +206 -0
  125. package/dist/api/utilities/timestamp.js.map +1 -0
  126. package/dist/api/utilities/video-transcode.d.ts +218 -0
  127. package/dist/api/utilities/video-transcode.js +624 -0
  128. package/dist/api/utilities/video-transcode.js.map +1 -0
  129. package/dist/api/utilities/whisper-model.d.ts +330 -0
  130. package/dist/api/utilities/whisper-model.js +587 -0
  131. package/dist/api/utilities/whisper-model.js.map +1 -0
  132. package/dist/api/utils.d.ts +19 -0
  133. package/dist/api/utils.js +39 -0
  134. package/dist/api/utils.js.map +1 -0
  135. package/dist/api/webrtc-stream.d.ts +294 -0
  136. package/dist/api/webrtc-stream.js +546 -0
  137. package/dist/api/webrtc-stream.js.map +1 -0
  138. package/dist/api/whisper.d.ts +326 -0
  139. package/dist/api/whisper.js +402 -0
  140. package/dist/api/whisper.js.map +1 -0
  141. package/dist/constants/bsf-options.d.ts +364 -0
  142. package/dist/constants/bsf-options.js +7 -0
  143. package/dist/constants/bsf-options.js.map +1 -0
  144. package/dist/constants/channel-layouts.d.ts +53 -0
  145. package/dist/constants/channel-layouts.js +57 -0
  146. package/dist/constants/channel-layouts.js.map +1 -0
  147. package/dist/constants/constants.d.ts +2435 -0
  148. package/dist/constants/constants.js +1989 -0
  149. package/dist/constants/constants.js.map +1 -0
  150. package/dist/constants/decoders.d.ts +651 -0
  151. package/dist/constants/decoders.js +641 -0
  152. package/dist/constants/decoders.js.map +1 -0
  153. package/dist/constants/encoders.d.ts +314 -0
  154. package/dist/constants/encoders.js +309 -0
  155. package/dist/constants/encoders.js.map +1 -0
  156. package/dist/constants/filter-options.d.ts +12099 -0
  157. package/dist/constants/filter-options.js +7 -0
  158. package/dist/constants/filter-options.js.map +1 -0
  159. package/dist/constants/format-options.d.ts +4319 -0
  160. package/dist/constants/format-options.js +7 -0
  161. package/dist/constants/format-options.js.map +1 -0
  162. package/dist/constants/formats.d.ts +18 -0
  163. package/dist/constants/formats.js +7 -0
  164. package/dist/constants/formats.js.map +1 -0
  165. package/dist/constants/hardware.d.ts +26 -0
  166. package/dist/constants/hardware.js +27 -0
  167. package/dist/constants/hardware.js.map +1 -0
  168. package/dist/constants/index.d.ts +10 -0
  169. package/dist/constants/index.js +6 -0
  170. package/dist/constants/index.js.map +1 -0
  171. package/dist/constants/options.d.ts +6217 -0
  172. package/dist/constants/options.js +7 -0
  173. package/dist/constants/options.js.map +1 -0
  174. package/dist/ffmpeg/index.d.ts +101 -0
  175. package/dist/ffmpeg/index.js +161 -0
  176. package/dist/ffmpeg/index.js.map +1 -0
  177. package/dist/ffmpeg/version.d.ts +6 -0
  178. package/dist/ffmpeg/version.js +7 -0
  179. package/dist/ffmpeg/version.js.map +1 -0
  180. package/dist/index.d.ts +4 -0
  181. package/dist/index.js +9 -0
  182. package/dist/index.js.map +1 -0
  183. package/dist/lib/audio-fifo.d.ts +399 -0
  184. package/dist/lib/audio-fifo.js +431 -0
  185. package/dist/lib/audio-fifo.js.map +1 -0
  186. package/dist/lib/binding.d.ts +242 -0
  187. package/dist/lib/binding.js +102 -0
  188. package/dist/lib/binding.js.map +1 -0
  189. package/dist/lib/bitstream-filter-context.d.ts +381 -0
  190. package/dist/lib/bitstream-filter-context.js +443 -0
  191. package/dist/lib/bitstream-filter-context.js.map +1 -0
  192. package/dist/lib/bitstream-filter.d.ts +140 -0
  193. package/dist/lib/bitstream-filter.js +154 -0
  194. package/dist/lib/bitstream-filter.js.map +1 -0
  195. package/dist/lib/codec-context.d.ts +1069 -0
  196. package/dist/lib/codec-context.js +1336 -0
  197. package/dist/lib/codec-context.js.map +1 -0
  198. package/dist/lib/codec-parameters.d.ts +664 -0
  199. package/dist/lib/codec-parameters.js +813 -0
  200. package/dist/lib/codec-parameters.js.map +1 -0
  201. package/dist/lib/codec-parser.d.ts +201 -0
  202. package/dist/lib/codec-parser.js +213 -0
  203. package/dist/lib/codec-parser.js.map +1 -0
  204. package/dist/lib/codec.d.ts +617 -0
  205. package/dist/lib/codec.js +746 -0
  206. package/dist/lib/codec.js.map +1 -0
  207. package/dist/lib/device.d.ts +294 -0
  208. package/dist/lib/device.js +326 -0
  209. package/dist/lib/device.js.map +1 -0
  210. package/dist/lib/dictionary.d.ts +333 -0
  211. package/dist/lib/dictionary.js +372 -0
  212. package/dist/lib/dictionary.js.map +1 -0
  213. package/dist/lib/error.d.ts +311 -0
  214. package/dist/lib/error.js +395 -0
  215. package/dist/lib/error.js.map +1 -0
  216. package/dist/lib/fifo.d.ts +416 -0
  217. package/dist/lib/fifo.js +453 -0
  218. package/dist/lib/fifo.js.map +1 -0
  219. package/dist/lib/filter-context.d.ts +714 -0
  220. package/dist/lib/filter-context.js +783 -0
  221. package/dist/lib/filter-context.js.map +1 -0
  222. package/dist/lib/filter-graph-segment.d.ts +161 -0
  223. package/dist/lib/filter-graph-segment.js +172 -0
  224. package/dist/lib/filter-graph-segment.js.map +1 -0
  225. package/dist/lib/filter-graph.d.ts +641 -0
  226. package/dist/lib/filter-graph.js +704 -0
  227. package/dist/lib/filter-graph.js.map +1 -0
  228. package/dist/lib/filter-inout.d.ts +202 -0
  229. package/dist/lib/filter-inout.js +261 -0
  230. package/dist/lib/filter-inout.js.map +1 -0
  231. package/dist/lib/filter.d.ts +243 -0
  232. package/dist/lib/filter.js +272 -0
  233. package/dist/lib/filter.js.map +1 -0
  234. package/dist/lib/format-context.d.ts +1335 -0
  235. package/dist/lib/format-context.js +1455 -0
  236. package/dist/lib/format-context.js.map +1 -0
  237. package/dist/lib/frame-utils.d.ts +116 -0
  238. package/dist/lib/frame-utils.js +98 -0
  239. package/dist/lib/frame-utils.js.map +1 -0
  240. package/dist/lib/frame.d.ts +1315 -0
  241. package/dist/lib/frame.js +1535 -0
  242. package/dist/lib/frame.js.map +1 -0
  243. package/dist/lib/hardware-device-context.d.ts +362 -0
  244. package/dist/lib/hardware-device-context.js +383 -0
  245. package/dist/lib/hardware-device-context.js.map +1 -0
  246. package/dist/lib/hardware-frames-context.d.ts +418 -0
  247. package/dist/lib/hardware-frames-context.js +471 -0
  248. package/dist/lib/hardware-frames-context.js.map +1 -0
  249. package/dist/lib/index.d.ts +35 -0
  250. package/dist/lib/index.js +60 -0
  251. package/dist/lib/index.js.map +1 -0
  252. package/dist/lib/input-format.d.ts +249 -0
  253. package/dist/lib/input-format.js +306 -0
  254. package/dist/lib/input-format.js.map +1 -0
  255. package/dist/lib/io-context.d.ts +698 -0
  256. package/dist/lib/io-context.js +771 -0
  257. package/dist/lib/io-context.js.map +1 -0
  258. package/dist/lib/log.d.ts +174 -0
  259. package/dist/lib/log.js +184 -0
  260. package/dist/lib/log.js.map +1 -0
  261. package/dist/lib/native-types.d.ts +1024 -0
  262. package/dist/lib/native-types.js +2 -0
  263. package/dist/lib/native-types.js.map +1 -0
  264. package/dist/lib/option.d.ts +953 -0
  265. package/dist/lib/option.js +1607 -0
  266. package/dist/lib/option.js.map +1 -0
  267. package/dist/lib/output-format.d.ts +222 -0
  268. package/dist/lib/output-format.js +259 -0
  269. package/dist/lib/output-format.js.map +1 -0
  270. package/dist/lib/packet.d.ts +521 -0
  271. package/dist/lib/packet.js +613 -0
  272. package/dist/lib/packet.js.map +1 -0
  273. package/dist/lib/rational.d.ts +251 -0
  274. package/dist/lib/rational.js +278 -0
  275. package/dist/lib/rational.js.map +1 -0
  276. package/dist/lib/software-resample-context.d.ts +552 -0
  277. package/dist/lib/software-resample-context.js +592 -0
  278. package/dist/lib/software-resample-context.js.map +1 -0
  279. package/dist/lib/software-scale-context.d.ts +344 -0
  280. package/dist/lib/software-scale-context.js +366 -0
  281. package/dist/lib/software-scale-context.js.map +1 -0
  282. package/dist/lib/stream.d.ts +383 -0
  283. package/dist/lib/stream.js +524 -0
  284. package/dist/lib/stream.js.map +1 -0
  285. package/dist/lib/sync-queue.d.ts +179 -0
  286. package/dist/lib/sync-queue.js +197 -0
  287. package/dist/lib/sync-queue.js.map +1 -0
  288. package/dist/lib/types.d.ts +34 -0
  289. package/dist/lib/types.js +2 -0
  290. package/dist/lib/types.js.map +1 -0
  291. package/dist/lib/utilities.d.ts +1198 -0
  292. package/dist/lib/utilities.js +1302 -0
  293. package/dist/lib/utilities.js.map +1 -0
  294. package/dist/utils/electron.d.ts +49 -0
  295. package/dist/utils/electron.js +63 -0
  296. package/dist/utils/electron.js.map +1 -0
  297. package/dist/utils/index.d.ts +6 -0
  298. package/dist/utils/index.js +7 -0
  299. package/dist/utils/index.js.map +1 -0
  300. package/dist/utils/libc.d.ts +11 -0
  301. package/dist/utils/libc.js +35 -0
  302. package/dist/utils/libc.js.map +1 -0
  303. package/dist/utils/variant.d.ts +26 -0
  304. package/dist/utils/variant.js +31 -0
  305. package/dist/utils/variant.js.map +1 -0
  306. package/dist/webrtc/index.d.ts +4 -0
  307. package/dist/webrtc/index.js +7 -0
  308. package/dist/webrtc/index.js.map +1 -0
  309. package/install/check.js +124 -0
  310. package/install/ffmpeg.js +66 -0
  311. 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