node-av-light 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,1661 @@
1
+ import { AV_BUFFERSRC_FLAG_KEEP_REF, AV_BUFFERSRC_FLAG_PUSH, AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, AVMEDIA_TYPE_VIDEO, EOF, } from '../constants/constants.js';
2
+ import { FFmpegError } from '../lib/error.js';
3
+ import { FilterGraph } from '../lib/filter-graph.js';
4
+ import { FilterInOut } from '../lib/filter-inout.js';
5
+ import { Filter } from '../lib/filter.js';
6
+ import { Frame } from '../lib/frame.js';
7
+ import { Rational } from '../lib/rational.js';
8
+ import { avGetSampleFmtName, avInvQ, avRescaleQ } from '../lib/utilities.js';
9
+ import { applyContextOptions } from './utilities/context-options.js';
10
+ /**
11
+ * High-level filter_complex API for multi-input/output filtering.
12
+ *
13
+ * Provides simplified interface for complex FFmpeg filter graphs with multiple inputs and outputs.
14
+ * Supports both high-level generator API and low-level manual control.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // High-level API: Simple overlay with frames() generator
19
+ * using complex = FilterComplexAPI.create('[0:v][1:v]overlay=x=100:y=50[out]', {
20
+ * inputs: [{ label: '0:v' }, { label: '1:v' }],
21
+ * outputs: [{ label: 'out' }]
22
+ * });
23
+ *
24
+ * // Process multiple input streams automatically
25
+ * for await (using frame of complex.frames('out', {
26
+ * '0:v': decoder1.frames(packets1),
27
+ * '1:v': decoder2.frames(packets2)
28
+ * })) {
29
+ * await encoder.encode(frame);
30
+ * }
31
+ * ```
32
+ *
33
+ * @see {@link FilterAPI} For simple single-input/output filtering
34
+ * @see {@link FilterGraph} For low-level filter graph API
35
+ * @see {@link frames} For high-level stream processing
36
+ * @see {@link process} For low-level manual frame sending
37
+ * @see {@link receive} For low-level manual frame receiving
38
+ */
39
+ export class FilterComplexAPI {
40
+ graph;
41
+ description;
42
+ options;
43
+ // Input/Output state
44
+ inputs = new Map();
45
+ outputs = new Map();
46
+ // Initialization state
47
+ initialized = false;
48
+ isClosed = false;
49
+ initializePromise = null;
50
+ // Reusable frame for receive operations
51
+ frame = new Frame();
52
+ /**
53
+ * @param graph - Filter graph instance
54
+ *
55
+ * @param description - Filter description string
56
+ *
57
+ * @param options - Filter complex options
58
+ *
59
+ * @internal
60
+ */
61
+ constructor(graph, description, options) {
62
+ this.graph = graph;
63
+ this.description = description;
64
+ this.options = options;
65
+ }
66
+ /**
67
+ * Create a complex filter with specified configuration.
68
+ *
69
+ * Direct mapping to avfilter_graph_segment_parse() and avfilter_graph_config().
70
+ *
71
+ * @param description - Filter description string (e.g., '[0:v][1:v]overlay[out]')
72
+ *
73
+ * @param options - Filter complex configuration including inputs and outputs
74
+ *
75
+ * @returns Filter complex instance ready to process frames
76
+ *
77
+ * @throws {Error} If configuration is invalid (duplicate labels, no inputs/outputs)
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Simple overlay example
82
+ * using complex = FilterComplexAPI.create(
83
+ * '[0:v][1:v]overlay=x=100:y=50[out]',
84
+ * {
85
+ * inputs: [
86
+ * { label: '0:v' }, // Base video
87
+ * { label: '1:v' } // Overlay video
88
+ * ],
89
+ * outputs: [{ label: 'out' }]
90
+ * }
91
+ * );
92
+ *
93
+ * // Send frames manually
94
+ * await complex.process('0:v', baseFrame);
95
+ * await complex.process('1:v', overlayFrame);
96
+ * using outFrame = await complex.receive('out');
97
+ * ```
98
+ *
99
+ * @see {@link process} For sending frames to inputs
100
+ * @see {@link receive} For getting frames from outputs
101
+ */
102
+ static create(description, options) {
103
+ // Accept a type-safe FilterComplexGraph builder or a raw description string.
104
+ const descriptionString = typeof description === 'string' ? description : description.build();
105
+ // Validate inputs and outputs
106
+ if (!options.inputs || options.inputs.length === 0) {
107
+ throw new Error('At least one input is required');
108
+ }
109
+ if (!options.outputs || options.outputs.length === 0) {
110
+ throw new Error('At least one output is required');
111
+ }
112
+ // Check for duplicate input labels
113
+ const inputLabels = new Set();
114
+ for (const input of options.inputs) {
115
+ if (inputLabels.has(input.label)) {
116
+ throw new Error(`Duplicate input label: ${input.label}`);
117
+ }
118
+ inputLabels.add(input.label);
119
+ }
120
+ // Check for duplicate output labels
121
+ const outputLabels = new Set();
122
+ for (const output of options.outputs) {
123
+ if (outputLabels.has(output.label)) {
124
+ throw new Error(`Duplicate output label: ${output.label}`);
125
+ }
126
+ outputLabels.add(output.label);
127
+ }
128
+ // Create graph
129
+ const graph = new FilterGraph();
130
+ graph.alloc();
131
+ // Configure threading
132
+ if (options.threads !== undefined) {
133
+ graph.nbThreads = options.threads;
134
+ }
135
+ const instance = new FilterComplexAPI(graph, descriptionString, options);
136
+ // Initialize input states
137
+ for (const input of options.inputs) {
138
+ instance.inputs.set(input.label, {
139
+ label: input.label,
140
+ buffersrc: null,
141
+ queuedFrames: [],
142
+ });
143
+ }
144
+ // Initialize output states
145
+ for (const output of options.outputs) {
146
+ instance.outputs.set(output.label, {
147
+ label: output.label,
148
+ buffersink: null,
149
+ });
150
+ }
151
+ return instance;
152
+ }
153
+ /**
154
+ * Check if filter complex is open.
155
+ *
156
+ * @returns true if not closed
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * if (complex.isOpen) {
161
+ * // Can still consume frames
162
+ * }
163
+ * ```
164
+ */
165
+ get isOpen() {
166
+ return !this.isClosed;
167
+ }
168
+ /**
169
+ * Check if filter complex has been initialized.
170
+ *
171
+ * Returns true after first frame set has been processed from all inputs.
172
+ *
173
+ * @returns true if filter graph has been configured
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * if (!complex.isInitialized) {
178
+ * console.log('Filter will initialize on first frame set');
179
+ * }
180
+ * ```
181
+ */
182
+ get isInitialized() {
183
+ return this.initialized;
184
+ }
185
+ /**
186
+ * Get output frame rate.
187
+ *
188
+ * Returns frame rate from the first output's buffersink.
189
+ * Returns null if not initialized or frame rate is not set.
190
+ *
191
+ * @returns Frame rate as rational number or null
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const frameRate = complex.frameRate;
196
+ * if (frameRate) {
197
+ * console.log(`Output: ${frameRate.num}/${frameRate.den} fps`);
198
+ * }
199
+ * ```
200
+ *
201
+ * @see {@link FilterAPI.frameRate} For single-output filter frame rate
202
+ */
203
+ get frameRate() {
204
+ if (!this.initialized || this.outputs.size === 0) {
205
+ return null;
206
+ }
207
+ // Get frame rate from first output
208
+ const firstOutput = this.outputs.values().next().value;
209
+ if (!firstOutput?.buffersink) {
210
+ return null;
211
+ }
212
+ const fr = firstOutput.buffersink.buffersinkGetFrameRate();
213
+ // Return null if frame rate is not set (0/0 or 0/1)
214
+ if (fr.num <= 0 || fr.den <= 0) {
215
+ return null;
216
+ }
217
+ return fr;
218
+ }
219
+ /**
220
+ * Get output time base.
221
+ *
222
+ * Returns time base from the first output's buffersink.
223
+ * Returns null if not initialized.
224
+ *
225
+ * @returns Time base as rational number or null
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const timeBase = complex.timeBase;
230
+ * if (timeBase) {
231
+ * console.log(`Output timeBase: ${timeBase.num}/${timeBase.den}`);
232
+ * }
233
+ * ```
234
+ *
235
+ * @see {@link FilterAPI.timeBase} For single-output filter time base
236
+ */
237
+ get timeBase() {
238
+ if (!this.initialized || this.outputs.size === 0) {
239
+ return null;
240
+ }
241
+ // Get time base from first output
242
+ const firstOutput = this.outputs.values().next().value;
243
+ if (!firstOutput?.buffersink) {
244
+ return null;
245
+ }
246
+ return firstOutput.buffersink.buffersinkGetTimeBase();
247
+ }
248
+ /**
249
+ * Process frame by sending to specified input.
250
+ *
251
+ * Sends a frame to the buffersrc of the specified input label.
252
+ * Automatically rescales timestamps to the input's calculated timeBase (CFR/VFR).
253
+ * Pass null to signal end-of-stream for that input.
254
+ *
255
+ * Direct mapping to av_buffersrc_add_frame().
256
+ *
257
+ * @param inLabel - Input label to send frame to
258
+ *
259
+ * @param frame - Frame to process
260
+ *
261
+ * @throws {Error} If input label not found or filter closed
262
+ *
263
+ * @throws {FFmpegError} If processing fails
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * // Process frames one at a time
268
+ * await complex.process('0:v', frame1);
269
+ * await complex.process('1:v', frame2);
270
+ * const outFrame = await complex.receive('out');
271
+ * ```
272
+ *
273
+ * @see {@link receive} For receiving output frames
274
+ * @see {@link flush} For flushing inputs
275
+ * @see {@link processSync} For synchronous version
276
+ */
277
+ async process(inLabel, frame) {
278
+ if (this.isClosed) {
279
+ throw new Error('FilterComplexAPI is already closed');
280
+ }
281
+ // Get input state
282
+ const inputState = this.inputs.get(inLabel);
283
+ if (!inputState) {
284
+ throw new Error(`Input '${inLabel}' not found`);
285
+ }
286
+ // If not initialized, queue frame and try to initialize
287
+ if (!this.initialized) {
288
+ const cloned = frame.clone();
289
+ if (!cloned) {
290
+ throw new Error('Failed to clone frame for queuing');
291
+ }
292
+ inputState.queuedFrames.push(cloned);
293
+ // Check if all inputs have at least one frame
294
+ if (this.hasAllInputFormats()) {
295
+ // All inputs ready → initialize graph and process queued frames
296
+ this.initializePromise ??= this.initializeFromQueuedFrames();
297
+ await this.initializePromise;
298
+ }
299
+ return;
300
+ }
301
+ // Already initialized → send frame directly
302
+ if (!inputState.buffersrc || !inputState.calculatedTimeBase) {
303
+ throw new Error(`Input '${inLabel}' buffersrc not initialized`);
304
+ }
305
+ // Rescale timestamps using helper
306
+ this.rescaleFrameTimestamps(frame, inputState.calculatedTimeBase);
307
+ // Send frame to buffersrc
308
+ // KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
309
+ const ret = await inputState.buffersrc.buffersrcAddFrame(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
310
+ FFmpegError.throwIfError(ret, `Failed to send frame to input ${inLabel}`);
311
+ }
312
+ /**
313
+ * Process frame by sending to specified input synchronously.
314
+ * Synchronous version of process.
315
+ *
316
+ * Sends a frame to the buffersrc of the specified input label.
317
+ * Automatically rescales timestamps to the input's calculated timeBase (CFR/VFR).
318
+ * Pass null to signal end-of-stream for that input.
319
+ *
320
+ * Direct mapping to av_buffersrc_add_frame().
321
+ *
322
+ * @param inLabel - Input label to send frame to
323
+ *
324
+ * @param frame - Frame to process
325
+ *
326
+ * @throws {Error} If input label not found or filter closed
327
+ *
328
+ * @throws {FFmpegError} If processing fails
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * // Process frames one at a time
333
+ * complex.processSync('0:v', frame1);
334
+ * complex.processSync('1:v', frame2);
335
+ * const outFrame = complex.receiveSync('out');
336
+ * ```
337
+ *
338
+ * @see {@link receiveSync} For receiving output frames
339
+ * @see {@link flushSync} For flushing inputs
340
+ * @see {@link process} For async version
341
+ */
342
+ processSync(inLabel, frame) {
343
+ if (this.isClosed) {
344
+ throw new Error('FilterComplexAPI is already closed');
345
+ }
346
+ // Get input state
347
+ const inputState = this.inputs.get(inLabel);
348
+ if (!inputState) {
349
+ throw new Error(`Input '${inLabel}' not found`);
350
+ }
351
+ // If not initialized, queue frame and try to initialize
352
+ if (!this.initialized) {
353
+ const cloned = frame.clone();
354
+ if (!cloned) {
355
+ throw new Error('Failed to clone frame for queuing');
356
+ }
357
+ inputState.queuedFrames.push(cloned);
358
+ // Check if all inputs have at least one frame
359
+ if (this.hasAllInputFormats()) {
360
+ // All inputs ready → initialize graph and process queued frames synchronously
361
+ this.initializeFromQueuedFramesSync();
362
+ }
363
+ return;
364
+ }
365
+ // Already initialized → send frame directly
366
+ if (!inputState.buffersrc || !inputState.calculatedTimeBase) {
367
+ throw new Error(`Input '${inLabel}' buffersrc not initialized`);
368
+ }
369
+ // Rescale timestamps using helper
370
+ this.rescaleFrameTimestamps(frame, inputState.calculatedTimeBase);
371
+ // Send frame to buffersrc
372
+ // KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
373
+ const ret = inputState.buffersrc.buffersrcAddFrameSync(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
374
+ FFmpegError.throwIfError(ret, `Failed to send frame to input ${inLabel}`);
375
+ }
376
+ /**
377
+ * Process frame streams from multiple inputs and yield frames from specified output.
378
+ *
379
+ * High-level async generator for multi-input filtering.
380
+ * Inputs may end at different times - shorter inputs are flushed individually
381
+ * while the remaining inputs keep flowing, matching ffmpeg CLI behaviour.
382
+ *
383
+ * **EOF Handling:**
384
+ * - EOF (null) from one input flushes only that input; the others continue.
385
+ * The filter decides what happens (e.g. overlay's `eof_action`, default
386
+ * `repeat`, keeps compositing the last overlay frame)
387
+ * - When ALL inputs have ended and at least one signaled EOF, the graph is
388
+ * finalized and the generator yields null
389
+ * - Iterator completion without null does not trigger a flush - the filter
390
+ * stays open until EOF or close()
391
+ *
392
+ * @param outLabel - Output label to receive frames from
393
+ *
394
+ * @param inputs - Record mapping input labels to frame sources (AsyncIterable, single Frame, or null)
395
+ *
396
+ * @yields {Frame | null} Filtered frames from output, followed by null when flushed
397
+ *
398
+ * @throws {Error} If input label not found
399
+ *
400
+ * @throws {FFmpegError} If processing fails
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * // Stream processing: 2 inputs, 1 output
405
+ * using complex = FilterComplexAPI.create('[0:v][1:v]overlay[out]', {
406
+ * inputs: [{ label: '0:v' }, { label: '1:v' }],
407
+ * outputs: [{ label: 'out' }]
408
+ * });
409
+ *
410
+ * for await (using frame of complex.frames('out', {
411
+ * '0:v': decoder1.frames(packets1),
412
+ * '1:v': decoder2.frames(packets2)
413
+ * })) {
414
+ * await encoder.encode(frame);
415
+ * }
416
+ * ```
417
+ *
418
+ * @example
419
+ * ```typescript
420
+ * // Single frames - no automatic flush
421
+ * for await (using frame of complex.frames('out', {
422
+ * '0:v': frame1,
423
+ * '1:v': frame2
424
+ * })) {
425
+ * await encoder.encode(frame);
426
+ * }
427
+ * // Filter remains open, buffered frames not flushed
428
+ * ```
429
+ *
430
+ * @example
431
+ * ```typescript
432
+ * // Explicit flush with null
433
+ * for await (using frame of complex.frames('out', {
434
+ * '0:v': null,
435
+ * '1:v': null
436
+ * })) {
437
+ * await encoder.encode(frame);
438
+ * }
439
+ * ```
440
+ *
441
+ * @example
442
+ * ```typescript
443
+ * // Mixed: stream + single frame
444
+ * for await (using frame of complex.frames('out', {
445
+ * '0:v': decoder.frames(packets), // Stream
446
+ * '1:v': watermarkFrame // Single frame (used for all)
447
+ * })) {
448
+ * await encoder.encode(frame);
449
+ * }
450
+ * ```
451
+ *
452
+ * @see {@link process} For manual frame sending
453
+ * @see {@link receive} For manual frame receiving
454
+ * @see {@link framesSync} For sync version
455
+ */
456
+ async *frames(outLabel, inputs) {
457
+ // Validate output label
458
+ if (!this.outputs.has(outLabel)) {
459
+ throw new Error(`Output '${outLabel}' not found`);
460
+ }
461
+ // Validate all input labels exist
462
+ for (const label of Object.keys(inputs)) {
463
+ if (!this.inputs.has(label)) {
464
+ throw new Error(`Input '${label}' not found in filter complex configuration`);
465
+ }
466
+ }
467
+ // Helper to drain all currently available output frames
468
+ const drainOutput = async function* () {
469
+ while (true) {
470
+ const outFrame = await this.receive(outLabel);
471
+ if (!outFrame || outFrame === EOF) {
472
+ break;
473
+ }
474
+ yield outFrame;
475
+ }
476
+ }.bind(this);
477
+ // Helper to process a single frame and yield output
478
+ const processFrame = async function* (label, frame) {
479
+ await this.process(label, frame);
480
+ yield* drainOutput();
481
+ }.bind(this);
482
+ // Helper to finalize (flush all inputs and yield remaining frames)
483
+ const finalize = async function* () {
484
+ for await (const frame of this.flushFrames(outLabel)) {
485
+ yield frame;
486
+ }
487
+ yield null;
488
+ }.bind(this);
489
+ // Separate inputs by type
490
+ const iterableInputs = new Map();
491
+ const singleFrameInputs = [];
492
+ const flushInputs = new Set(); // Track which inputs to flush
493
+ for (const [label, source] of Object.entries(inputs)) {
494
+ if (source === null) {
495
+ // null - flush this input
496
+ flushInputs.add(label);
497
+ }
498
+ else if (source instanceof Frame) {
499
+ // Single frame
500
+ singleFrameInputs.push({ label, frame: source });
501
+ }
502
+ else {
503
+ // AsyncIterable
504
+ iterableInputs.set(label, source[Symbol.asyncIterator]());
505
+ }
506
+ }
507
+ // If only single frames/nulls and no iterables
508
+ if (iterableInputs.size === 0) {
509
+ // Process single frames
510
+ for (const { label, frame } of singleFrameInputs) {
511
+ yield* processFrame(label, frame);
512
+ }
513
+ // Flush inputs that were null
514
+ for (const label of flushInputs) {
515
+ await this.flush(label);
516
+ }
517
+ // Only finalize if we flushed any inputs
518
+ if (flushInputs.size > 0) {
519
+ yield* finalize();
520
+ }
521
+ return;
522
+ }
523
+ // Process single frames first
524
+ for (const { label, frame } of singleFrameInputs) {
525
+ yield* processFrame(label, frame);
526
+ }
527
+ // Track which inputs have finished
528
+ const finishedInputs = new Set();
529
+ let sawEof = flushInputs.size > 0; // True once any input signaled EOF (null)
530
+ // Inputs given as explicit null: signal EOF on that leg right away.
531
+ for (const label of flushInputs) {
532
+ await this.flush(label);
533
+ }
534
+ try {
535
+ // Process frames from iterable inputs in parallel
536
+ while (finishedInputs.size < iterableInputs.size) {
537
+ // Read one frame from each active input
538
+ const readPromises = [];
539
+ for (const [label, iterator] of iterableInputs) {
540
+ if (!finishedInputs.has(label)) {
541
+ readPromises.push(iterator.next().then((result) => ({
542
+ label,
543
+ result,
544
+ })));
545
+ }
546
+ }
547
+ // Wait for all reads to complete
548
+ const results = await Promise.all(readPromises);
549
+ // Process each result
550
+ for (const { label, result } of results) {
551
+ if (result.done) {
552
+ // Iterator finished without explicit null - no automatic flush
553
+ finishedInputs.add(label);
554
+ continue;
555
+ }
556
+ const frame = result.value;
557
+ if (frame === null) {
558
+ // EOF for THIS input only: signal it downstream and keep the other
559
+ // inputs flowing. What happens next is the filter's decision - e.g.
560
+ // overlay's eof_action (default `repeat`) keeps compositing the last
561
+ // overlay frame while the main input continues (matches ffmpeg CLI).
562
+ await this.flush(label);
563
+ sawEof = true;
564
+ finishedInputs.add(label);
565
+ yield* drainOutput();
566
+ }
567
+ else {
568
+ // Send frame to input
569
+ yield* processFrame(label, frame);
570
+ }
571
+ }
572
+ }
573
+ // All inputs ended. If any signaled EOF explicitly, finalize the graph
574
+ // (flush remaining legs, drain buffered output) and yield null. Iterators
575
+ // that merely completed without a null do not trigger a flush.
576
+ if (sawEof) {
577
+ yield* finalize();
578
+ }
579
+ }
580
+ finally {
581
+ // Close input iterators that are still open. The inputs are driven via
582
+ // manual iterators (not for-await), so an early consumer stop
583
+ // (generator.return) would otherwise leave the upstream generators
584
+ // (decoders, demuxer packet queues) registered and running - a full
585
+ // demuxer queue then blocks the shared read loop and starves the other
586
+ // stream's consumers (teardown deadlock).
587
+ for (const [label, iterator] of iterableInputs) {
588
+ if (!finishedInputs.has(label)) {
589
+ await iterator.return?.(undefined);
590
+ }
591
+ }
592
+ }
593
+ }
594
+ /**
595
+ * Process frame streams from multiple inputs and yield frames from specified output synchronously.
596
+ * Synchronous version of frames.
597
+ *
598
+ * High-level sync generator for multi-input filtering.
599
+ * Inputs may end at different times - shorter inputs are flushed individually
600
+ * while the remaining inputs keep flowing, matching ffmpeg CLI behaviour.
601
+ *
602
+ * **EOF Handling:**
603
+ * - EOF (null) from one input flushes only that input; the others continue
604
+ * - When ALL inputs have ended and at least one signaled EOF, the graph is
605
+ * finalized and the generator yields null
606
+ * - Iterator completion without null does not trigger a flush - the filter
607
+ * stays open until EOF or close()
608
+ *
609
+ * @param outLabel - Output label to receive frames from
610
+ *
611
+ * @param inputs - Record mapping input labels to frame sources (Iterable, single Frame, or null)
612
+ *
613
+ * @yields {Frame | null} Filtered frames from output, followed by null when flushed
614
+ *
615
+ * @throws {Error} If input label not found or filter not initialized
616
+ *
617
+ * @throws {FFmpegError} If processing fails
618
+ *
619
+ * @example
620
+ * ```typescript
621
+ * // Stream processing: 2 inputs, 1 output
622
+ * using complex = FilterComplexAPI.create('[0:v][1:v]overlay[out]', {
623
+ * inputs: [{ label: '0:v' }, { label: '1:v' }],
624
+ * outputs: [{ label: 'out' }]
625
+ * });
626
+ *
627
+ * // Note: Sync version requires async initialization first
628
+ * await complex.process('0:v', firstFrame1);
629
+ * await complex.process('1:v', firstFrame2);
630
+ *
631
+ * for (using frame of complex.framesSync('out', {
632
+ * '0:v': decoder1.framesSync(packets1),
633
+ * '1:v': decoder2.framesSync(packets2)
634
+ * })) {
635
+ * encoder.encodeSync(frame);
636
+ * }
637
+ * ```
638
+ *
639
+ * @example
640
+ * ```typescript
641
+ * // Single frames
642
+ * for (using frame of complex.framesSync('out', {
643
+ * '0:v': frame1,
644
+ * '1:v': frame2
645
+ * })) {
646
+ * encoder.encodeSync(frame);
647
+ * }
648
+ * ```
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * // Explicit flush
653
+ * for (using frame of complex.framesSync('out', {
654
+ * '0:v': null,
655
+ * '1:v': null
656
+ * })) {
657
+ * encoder.encodeSync(frame);
658
+ * }
659
+ * ```
660
+ *
661
+ * @see {@link processSync} For manual frame sending
662
+ * @see {@link receiveSync} For manual frame receiving
663
+ * @see {@link frames} For async version with lazy initialization
664
+ */
665
+ *framesSync(outLabel, inputs) {
666
+ // Validate output label
667
+ if (!this.outputs.has(outLabel)) {
668
+ throw new Error(`Output '${outLabel}' not found`);
669
+ }
670
+ // Validate all input labels exist
671
+ for (const label of Object.keys(inputs)) {
672
+ if (!this.inputs.has(label)) {
673
+ throw new Error(`Input '${label}' not found in filter complex configuration`);
674
+ }
675
+ }
676
+ // Sync version requires filter to be initialized already
677
+ if (!this.initialized) {
678
+ throw new Error('FilterComplexAPI not initialized. Use async frames() method for lazy initialization.');
679
+ }
680
+ // Helper to drain all currently available output frames
681
+ const drainOutput = function* () {
682
+ while (true) {
683
+ const outFrame = this.receiveSync(outLabel);
684
+ if (!outFrame || outFrame === EOF) {
685
+ break;
686
+ }
687
+ yield outFrame;
688
+ }
689
+ }.bind(this);
690
+ // Helper to process a single frame and yield output
691
+ const processFrame = function* (label, frame) {
692
+ this.processSync(label, frame);
693
+ yield* drainOutput();
694
+ }.bind(this);
695
+ // Helper to finalize (flush all inputs and yield remaining frames)
696
+ const finalize = function* () {
697
+ for (const frame of this.flushFramesSync(outLabel)) {
698
+ yield frame;
699
+ }
700
+ yield null;
701
+ }.bind(this);
702
+ // Separate inputs by type
703
+ const iterableInputs = new Map();
704
+ const singleFrameInputs = [];
705
+ const flushInputs = new Set(); // Track which inputs to flush
706
+ for (const [label, source] of Object.entries(inputs)) {
707
+ if (source === null) {
708
+ // null - flush this input
709
+ flushInputs.add(label);
710
+ }
711
+ else if (source instanceof Frame) {
712
+ // Single frame
713
+ singleFrameInputs.push({ label, frame: source });
714
+ }
715
+ else {
716
+ // Iterable
717
+ iterableInputs.set(label, source[Symbol.iterator]());
718
+ }
719
+ }
720
+ // If only single frames/nulls and no iterables
721
+ if (iterableInputs.size === 0) {
722
+ // Process single frames
723
+ for (const { label, frame } of singleFrameInputs) {
724
+ yield* processFrame(label, frame);
725
+ }
726
+ // Flush inputs that were null
727
+ for (const label of flushInputs) {
728
+ this.flushSync(label);
729
+ }
730
+ // Only finalize if we flushed any inputs
731
+ if (flushInputs.size > 0) {
732
+ yield* finalize();
733
+ }
734
+ return;
735
+ }
736
+ // Process single frames first
737
+ for (const { label, frame } of singleFrameInputs) {
738
+ yield* processFrame(label, frame);
739
+ }
740
+ // Track which inputs have finished
741
+ const finishedInputs = new Set();
742
+ let sawEof = flushInputs.size > 0; // True once any input signaled EOF (null)
743
+ // Inputs given as explicit null: signal EOF on that leg right away.
744
+ for (const label of flushInputs) {
745
+ this.flushSync(label);
746
+ }
747
+ try {
748
+ // Process frames from iterable inputs in round-robin fashion
749
+ while (finishedInputs.size < iterableInputs.size) {
750
+ // Read one frame from each active input
751
+ for (const [label, iterator] of iterableInputs) {
752
+ if (finishedInputs.has(label)) {
753
+ continue;
754
+ }
755
+ const result = iterator.next();
756
+ if (result.done) {
757
+ // Iterator finished without explicit null - no automatic flush
758
+ finishedInputs.add(label);
759
+ continue;
760
+ }
761
+ const frame = result.value;
762
+ if (frame === null) {
763
+ // EOF for THIS input only - see frames() for the semantics.
764
+ this.flushSync(label);
765
+ sawEof = true;
766
+ finishedInputs.add(label);
767
+ yield* drainOutput();
768
+ }
769
+ else {
770
+ // Send frame to input
771
+ yield* processFrame(label, frame);
772
+ }
773
+ }
774
+ }
775
+ // All inputs ended - finalize only if any input signaled EOF explicitly.
776
+ if (sawEof) {
777
+ yield* finalize();
778
+ }
779
+ }
780
+ finally {
781
+ // Close input iterators that are still open - see frames() for why.
782
+ for (const [label, iterator] of iterableInputs) {
783
+ if (!finishedInputs.has(label)) {
784
+ iterator.return?.(undefined);
785
+ }
786
+ }
787
+ }
788
+ }
789
+ /**
790
+ * Flush input(s) and signal end-of-stream.
791
+ *
792
+ * Sends null frame to buffersrc filter(s) to flush buffered data.
793
+ * Must call receive() on outputs to get flushed frames.
794
+ * Does nothing if filter is closed or was never initialized.
795
+ *
796
+ * Direct mapping to av_buffersrc_add_frame(NULL).
797
+ *
798
+ * @param inLabel - Input label to flush. If not specified, flushes all inputs.
799
+ *
800
+ * @throws {Error} If input label not found
801
+ *
802
+ * @throws {FFmpegError} If flush fails
803
+ *
804
+ * @example
805
+ * ```typescript
806
+ * // Flush specific input
807
+ * await complex.flush('0:v');
808
+ *
809
+ * // Flush all inputs
810
+ * await complex.flush();
811
+ *
812
+ * // Get remaining frames from output
813
+ * let frame;
814
+ * while ((frame = await complex.receive('out')) !== null) {
815
+ * frame.free();
816
+ * }
817
+ * ```
818
+ *
819
+ * @see {@link flushFrames} For async iteration
820
+ * @see {@link receive} For getting flushed frames
821
+ * @see {@link flushSync} For synchronous version
822
+ */
823
+ async flush(inLabel) {
824
+ if (this.isClosed || !this.initialized) {
825
+ return;
826
+ }
827
+ if (inLabel) {
828
+ // Flush specific input
829
+ const inputState = this.inputs.get(inLabel);
830
+ if (!inputState) {
831
+ throw new Error(`Input '${inLabel}' not found`);
832
+ }
833
+ if (inputState.buffersrc) {
834
+ const ret = await inputState.buffersrc.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
835
+ if (ret < 0 && ret !== AVERROR_EOF) {
836
+ FFmpegError.throwIfError(ret, `Failed to flush input ${inLabel}`);
837
+ }
838
+ }
839
+ }
840
+ else {
841
+ // Flush all inputs
842
+ for (const inputState of this.inputs.values()) {
843
+ if (!inputState.buffersrc)
844
+ continue;
845
+ const ret = await inputState.buffersrc.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
846
+ if (ret < 0 && ret !== AVERROR_EOF) {
847
+ FFmpegError.throwIfError(ret, `Failed to flush input ${inputState.label}`);
848
+ }
849
+ }
850
+ }
851
+ }
852
+ /**
853
+ * Flush input(s) and signal end-of-stream synchronously.
854
+ * Synchronous version of flush.
855
+ *
856
+ * Sends null frame to buffersrc filter(s) to flush buffered data.
857
+ * Must call receiveSync() on outputs to get flushed frames.
858
+ * Does nothing if filter is closed or was never initialized.
859
+ *
860
+ * Direct mapping to av_buffersrc_add_frame(NULL).
861
+ *
862
+ * @param inLabel - Input label to flush. If not specified, flushes all inputs.
863
+ *
864
+ * @throws {Error} If input label not found
865
+ *
866
+ * @throws {FFmpegError} If flush fails
867
+ *
868
+ * @example
869
+ * ```typescript
870
+ * // Flush specific input
871
+ * complex.flushSync('0:v');
872
+ *
873
+ * // Flush all inputs
874
+ * complex.flushSync();
875
+ *
876
+ * // Get remaining frames from output
877
+ * let frame;
878
+ * while ((frame = complex.receiveSync('out')) !== null) {
879
+ * frame.free();
880
+ * }
881
+ * ```
882
+ *
883
+ * @see {@link flushFramesSync} For sync iteration
884
+ * @see {@link receiveSync} For getting flushed frames
885
+ * @see {@link flush} For async version
886
+ */
887
+ flushSync(inLabel) {
888
+ if (this.isClosed || !this.initialized) {
889
+ return;
890
+ }
891
+ if (inLabel) {
892
+ // Flush specific input
893
+ const inputState = this.inputs.get(inLabel);
894
+ if (!inputState) {
895
+ throw new Error(`Input '${inLabel}' not found`);
896
+ }
897
+ if (inputState.buffersrc) {
898
+ const ret = inputState.buffersrc.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
899
+ if (ret < 0 && ret !== AVERROR_EOF) {
900
+ FFmpegError.throwIfError(ret, `Failed to flush input ${inLabel}`);
901
+ }
902
+ }
903
+ }
904
+ else {
905
+ // Flush all inputs
906
+ for (const inputState of this.inputs.values()) {
907
+ if (!inputState.buffersrc)
908
+ continue;
909
+ const ret = inputState.buffersrc.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
910
+ if (ret < 0 && ret !== AVERROR_EOF) {
911
+ FFmpegError.throwIfError(ret, `Failed to flush input ${inputState.label}`);
912
+ }
913
+ }
914
+ }
915
+ }
916
+ /**
917
+ * Flush all inputs and yield remaining frames from specified output.
918
+ *
919
+ * Convenience method that:
920
+ * 1. Calls flush() to send EOF to all inputs
921
+ * 2. Yields all remaining frames from the specified output
922
+ * 3. Continues until EOF is reached
923
+ *
924
+ * Automatically frees yielded frames after use (using declaration).
925
+ *
926
+ * @param outLabel - Output label to receive flushed frames from
927
+ *
928
+ * @yields {Frame} Remaining frames from filter after flush
929
+ *
930
+ * @throws {Error} If output label not found or filter not initialized
931
+ *
932
+ * @throws {FFmpegError} If flushing or receiving fails
933
+ *
934
+ * @example
935
+ * ```typescript
936
+ * // Process all frames, then flush
937
+ * for await (using frame of inputFrames) {
938
+ * await complex.process('0:v', frame);
939
+ * }
940
+ *
941
+ * // Get all remaining frames
942
+ * for await (using frame of complex.flushFrames('out')) {
943
+ * await encoder.encode(frame);
944
+ * }
945
+ * ```
946
+ *
947
+ * @see {@link flush} For flushing without iteration
948
+ * @see {@link receive} For manual frame retrieval
949
+ * @see {@link flushFramesSync} For synchronous version
950
+ */
951
+ async *flushFrames(outLabel) {
952
+ // Flush all inputs
953
+ await this.flush();
954
+ // Yield all remaining frames from output
955
+ while (true) {
956
+ const frame = await this.receive(outLabel);
957
+ if (!frame || frame === EOF) {
958
+ break;
959
+ }
960
+ yield frame;
961
+ }
962
+ }
963
+ /**
964
+ * Flush all inputs and yield remaining frames from specified output synchronously.
965
+ * Synchronous version of flushFrames.
966
+ *
967
+ * Convenience method that:
968
+ * 1. Calls flushSync() to send EOF to all inputs
969
+ * 2. Yields all remaining frames from the specified output
970
+ * 3. Continues until EOF is reached
971
+ *
972
+ * Automatically frees yielded frames after use (using declaration).
973
+ *
974
+ * @param outLabel - Output label to receive flushed frames from
975
+ *
976
+ * @yields {Frame} Remaining frames from filter after flush
977
+ *
978
+ * @throws {Error} If output label not found or filter not initialized
979
+ *
980
+ * @throws {FFmpegError} If flushing or receiving fails
981
+ *
982
+ * @example
983
+ * ```typescript
984
+ * // Process all frames, then flush
985
+ * for (using frame of inputFrames) {
986
+ * complex.processSync('0:v', frame);
987
+ * }
988
+ *
989
+ * // Get all remaining frames
990
+ * for (using frame of complex.flushFramesSync('out')) {
991
+ * encoder.encodeSync(frame);
992
+ * }
993
+ * ```
994
+ *
995
+ * @see {@link flushSync} For flushing without iteration
996
+ * @see {@link receiveSync} For manual frame retrieval
997
+ * @see {@link flushFrames} For async version
998
+ */
999
+ *flushFramesSync(outLabel) {
1000
+ // Flush all inputs
1001
+ this.flushSync();
1002
+ // Yield all remaining frames from output
1003
+ while (true) {
1004
+ const frame = this.receiveSync(outLabel);
1005
+ if (!frame || frame === EOF) {
1006
+ break;
1007
+ }
1008
+ yield frame;
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Receive filtered frame from specified output.
1013
+ *
1014
+ * Pulls a single frame from the buffersink of the specified output.
1015
+ * Automatically post-processes frame (sets timeBase, calculates duration).
1016
+ * Returns cloned frame - caller must free it.
1017
+ *
1018
+ * Return values:
1019
+ * - Frame: Successfully received frame (caller must free)
1020
+ * - null: Need more input (AVERROR_EAGAIN) - call process() to send more frames
1021
+ * - EOF: End of stream reached
1022
+ *
1023
+ * Direct mapping to av_buffersink_get_frame().
1024
+ *
1025
+ * @param outLabel - Output label to receive from
1026
+ *
1027
+ * @returns Frame on success, null if need more input, EOF if finished
1028
+ *
1029
+ * @throws {Error} If output label not found or filter not initialized
1030
+ *
1031
+ * @throws {FFmpegError} If receive fails with unexpected error
1032
+ *
1033
+ * @example
1034
+ * ```typescript
1035
+ * // Process frames one at a time
1036
+ * await complex.process('0:v', frame1);
1037
+ * const outFrame = await complex.receive('out');
1038
+ * if (outFrame && outFrame !== EOF) {
1039
+ * // Use frame
1040
+ * outFrame.free();
1041
+ * }
1042
+ * ```
1043
+ *
1044
+ * @see {@link process} For sending input frames
1045
+ * @see {@link flush} For flushing after all input
1046
+ * @see {@link receiveSync} For synchronous version
1047
+ */
1048
+ async receive(outLabel) {
1049
+ if (this.isClosed || !this.initialized) {
1050
+ return null;
1051
+ }
1052
+ // Get output state
1053
+ const outputState = this.outputs.get(outLabel);
1054
+ if (!outputState?.buffersink) {
1055
+ throw new Error(`Output '${outLabel}' not found or not initialized`);
1056
+ }
1057
+ // Allocate frame for receiving
1058
+ this.frame.alloc();
1059
+ const ret = await outputState.buffersink.buffersinkGetFrame(this.frame);
1060
+ if (ret >= 0) {
1061
+ // Success - post-process and clone for user
1062
+ this.postProcessOutputFrame(this.frame, outputState.buffersink);
1063
+ const cloned = this.frame.clone();
1064
+ if (!cloned) {
1065
+ throw new Error('Failed to clone output frame');
1066
+ }
1067
+ return cloned;
1068
+ }
1069
+ else if (ret === AVERROR_EAGAIN) {
1070
+ // Need more input
1071
+ return null;
1072
+ }
1073
+ else if (ret === AVERROR_EOF) {
1074
+ // End of stream
1075
+ return EOF;
1076
+ }
1077
+ else {
1078
+ // Unexpected error
1079
+ FFmpegError.throwIfError(ret, `Failed to receive frame from ${outLabel}`);
1080
+ return null;
1081
+ }
1082
+ }
1083
+ /**
1084
+ * Receive filtered frame from specified output synchronously.
1085
+ * Synchronous version of receive.
1086
+ *
1087
+ * Pulls a single frame from the buffersink of the specified output.
1088
+ * Automatically post-processes frame (sets timeBase, calculates duration).
1089
+ * Returns cloned frame - caller must free it.
1090
+ *
1091
+ * Return values:
1092
+ * - Frame: Successfully received frame (caller must free)
1093
+ * - null: Need more input (AVERROR_EAGAIN) - call processSync() to send more frames
1094
+ * - EOF: End of stream reached
1095
+ *
1096
+ * Direct mapping to av_buffersink_get_frame().
1097
+ *
1098
+ * @param outLabel - Output label to receive from
1099
+ *
1100
+ * @returns Frame on success, null if need more input, EOF if finished
1101
+ *
1102
+ * @throws {Error} If output label not found or filter not initialized
1103
+ *
1104
+ * @throws {FFmpegError} If receive fails with unexpected error
1105
+ *
1106
+ * @example
1107
+ * ```typescript
1108
+ * // Process frames one at a time
1109
+ * complex.processSync('0:v', frame1);
1110
+ * const outFrame = complex.receiveSync('out');
1111
+ * if (outFrame && outFrame !== EOF) {
1112
+ * // Use frame
1113
+ * outFrame.free();
1114
+ * }
1115
+ * ```
1116
+ *
1117
+ * @see {@link processSync} For sending input frames
1118
+ * @see {@link flushSync} For flushing after all input
1119
+ * @see {@link receive} For async version
1120
+ */
1121
+ receiveSync(outLabel) {
1122
+ if (this.isClosed || !this.initialized) {
1123
+ return null;
1124
+ }
1125
+ // Get output state
1126
+ const outputState = this.outputs.get(outLabel);
1127
+ if (!outputState?.buffersink) {
1128
+ throw new Error(`Output '${outLabel}' not found or not initialized`);
1129
+ }
1130
+ // Allocate frame for receiving
1131
+ this.frame.alloc();
1132
+ const ret = outputState.buffersink.buffersinkGetFrameSync(this.frame);
1133
+ if (ret >= 0) {
1134
+ // Success - post-process and clone for user
1135
+ this.postProcessOutputFrame(this.frame, outputState.buffersink);
1136
+ const cloned = this.frame.clone();
1137
+ if (!cloned) {
1138
+ throw new Error('Failed to clone output frame');
1139
+ }
1140
+ return cloned;
1141
+ }
1142
+ else if (ret === AVERROR_EAGAIN) {
1143
+ // Need more input
1144
+ return null;
1145
+ }
1146
+ else if (ret === AVERROR_EOF) {
1147
+ // End of stream
1148
+ return EOF;
1149
+ }
1150
+ else {
1151
+ // Unexpected error
1152
+ FFmpegError.throwIfError(ret, `Failed to receive frame from ${outLabel}`);
1153
+ return null;
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Check if all inputs have received at least one frame (have format information).
1158
+ *
1159
+ * @returns true if all inputs have format info (first frame received)
1160
+ *
1161
+ * @internal
1162
+ */
1163
+ hasAllInputFormats() {
1164
+ for (const inputState of this.inputs.values()) {
1165
+ // Input has format if it has at least one queued frame OR is already initialized
1166
+ if (inputState.queuedFrames.length === 0 && !inputState.buffersrc) {
1167
+ return false;
1168
+ }
1169
+ }
1170
+ return true;
1171
+ }
1172
+ /**
1173
+ * Initialize filter graph from queued frames.
1174
+ *
1175
+ * Implements FFmpeg's configure_filtergraph() logic:
1176
+ * 1. Create buffersrc filters from first queued frame of each input
1177
+ * 2. Parse filter description
1178
+ * 3. Create buffersink filters
1179
+ * 4. Configure graph with avfilter_graph_config()
1180
+ * 5. Send all queued frames to buffersrc
1181
+ *
1182
+ * @throws {Error} If initialization fails
1183
+ *
1184
+ * @throws {FFmpegError} If configuration fails
1185
+ *
1186
+ * @internal
1187
+ */
1188
+ async initializeFromQueuedFrames() {
1189
+ if (this.isClosed) {
1190
+ throw new Error('FilterComplexAPI is already closed');
1191
+ }
1192
+ // Step 1: Create buffersrc filters from first queued frame
1193
+ for (const [label, inputState] of this.inputs) {
1194
+ if (inputState.queuedFrames.length === 0) {
1195
+ throw new Error(`Input '${label}' has no queued frames for initialization`);
1196
+ }
1197
+ const firstFrame = inputState.queuedFrames[0];
1198
+ // Calculate timeBase from first frame (CFR/VFR mode)
1199
+ inputState.calculatedTimeBase = this.calculateTimeBase(firstFrame);
1200
+ // Track initial frame properties for change detection
1201
+ inputState.lastFrameProps = {
1202
+ format: firstFrame.format,
1203
+ width: firstFrame.width,
1204
+ height: firstFrame.height,
1205
+ sampleRate: firstFrame.sampleRate,
1206
+ channels: firstFrame.channelLayout?.nbChannels ?? 0,
1207
+ };
1208
+ // Create buffersrc filter
1209
+ const buffersrc = this.createBufferSource(label, firstFrame, inputState.calculatedTimeBase);
1210
+ inputState.buffersrc = buffersrc;
1211
+ }
1212
+ // Step 2: Set graph options before parsing
1213
+ if (this.options.scaleSwsOpts) {
1214
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1215
+ }
1216
+ if (this.options.audioResampleOpts) {
1217
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1218
+ }
1219
+ // Step 3: Parse filter description and create buffersink filters
1220
+ this.parseFilterDescription();
1221
+ applyContextOptions(this.graph, this.options.graph);
1222
+ this.options.configure?.(this.graph);
1223
+ // Step 4: Configure the graph
1224
+ const ret = await this.graph.config();
1225
+ FFmpegError.throwIfError(ret, 'Failed to configure filter complex graph');
1226
+ // Step 5: Send all queued frames to buffersrc
1227
+ for (const [label, inputState] of this.inputs) {
1228
+ if (!inputState.buffersrc || !inputState.calculatedTimeBase) {
1229
+ continue;
1230
+ }
1231
+ // Process all queued frames for this input
1232
+ for (const frame of inputState.queuedFrames) {
1233
+ // Rescale timestamps using helper
1234
+ this.rescaleFrameTimestamps(frame, inputState.calculatedTimeBase);
1235
+ // Send to buffersrc
1236
+ // KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
1237
+ const ret = await inputState.buffersrc.buffersrcAddFrame(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
1238
+ FFmpegError.throwIfError(ret, `Failed to send queued frame to ${label}`);
1239
+ // Free the frame
1240
+ frame.free();
1241
+ }
1242
+ // Clear the queue after processing
1243
+ inputState.queuedFrames = [];
1244
+ }
1245
+ this.initialized = true;
1246
+ }
1247
+ /**
1248
+ * Initialize filter graph from queued frames synchronously.
1249
+ * Synchronous version of initializeFromQueuedFrames.
1250
+ *
1251
+ * @throws {Error} If closed or inputs have no queued frames
1252
+ *
1253
+ * @throws {FFmpegError} If graph configuration or frame processing fails
1254
+ *
1255
+ * @internal
1256
+ */
1257
+ initializeFromQueuedFramesSync() {
1258
+ if (this.isClosed) {
1259
+ throw new Error('FilterComplexAPI is already closed');
1260
+ }
1261
+ // Step 1: Create buffersrc filters from first queued frame
1262
+ for (const [label, inputState] of this.inputs) {
1263
+ if (inputState.queuedFrames.length === 0) {
1264
+ throw new Error(`Input '${label}' has no queued frames for initialization`);
1265
+ }
1266
+ const firstFrame = inputState.queuedFrames[0];
1267
+ // Calculate timeBase from first frame (CFR/VFR mode)
1268
+ inputState.calculatedTimeBase = this.calculateTimeBase(firstFrame);
1269
+ // Track initial frame properties for change detection
1270
+ inputState.lastFrameProps = {
1271
+ format: firstFrame.format,
1272
+ width: firstFrame.width,
1273
+ height: firstFrame.height,
1274
+ sampleRate: firstFrame.sampleRate,
1275
+ channels: firstFrame.channelLayout?.nbChannels ?? 0,
1276
+ };
1277
+ // Create buffersrc filter
1278
+ const buffersrc = this.createBufferSource(label, firstFrame, inputState.calculatedTimeBase);
1279
+ inputState.buffersrc = buffersrc;
1280
+ }
1281
+ // Step 2: Set graph options before parsing
1282
+ if (this.options.scaleSwsOpts) {
1283
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1284
+ }
1285
+ if (this.options.audioResampleOpts) {
1286
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1287
+ }
1288
+ // Step 3: Parse filter description and create buffersink filters
1289
+ this.parseFilterDescription();
1290
+ applyContextOptions(this.graph, this.options.graph);
1291
+ this.options.configure?.(this.graph);
1292
+ // Step 4: Configure the graph
1293
+ const ret = this.graph.configSync();
1294
+ FFmpegError.throwIfError(ret, 'Failed to configure filter complex graph');
1295
+ // Step 5: Send all queued frames to buffersrc
1296
+ for (const [label, inputState] of this.inputs) {
1297
+ if (!inputState.buffersrc || !inputState.calculatedTimeBase) {
1298
+ continue;
1299
+ }
1300
+ // Process all queued frames for this input
1301
+ for (const frame of inputState.queuedFrames) {
1302
+ // Rescale timestamps using helper
1303
+ this.rescaleFrameTimestamps(frame, inputState.calculatedTimeBase);
1304
+ // Send to buffersrc
1305
+ // KEEP_REF preserves the input frame's hw_frames_ctx for reuse across multiple filters
1306
+ const ret = inputState.buffersrc.buffersrcAddFrameSync(frame, (AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF));
1307
+ FFmpegError.throwIfError(ret, `Failed to send queued frame to ${label}`);
1308
+ // Free the frame
1309
+ frame.free();
1310
+ }
1311
+ // Clear the queue after processing
1312
+ inputState.queuedFrames = [];
1313
+ }
1314
+ this.initialized = true;
1315
+ }
1316
+ /**
1317
+ * Calculate timeBase from frame based on media type and CFR option.
1318
+ *
1319
+ * Implements FFmpeg's ifilter_parameters_from_frame logic:
1320
+ * - Audio: Always { 1, sample_rate }
1321
+ * - Video CFR: 1/framerate (inverse of framerate)
1322
+ * - Video VFR: Use frame.timeBase
1323
+ *
1324
+ * @param frame - Input frame
1325
+ *
1326
+ * @returns Calculated timeBase
1327
+ *
1328
+ * @internal
1329
+ */
1330
+ calculateTimeBase(frame) {
1331
+ if (frame.isAudio()) {
1332
+ // Audio: Always { 1, sample_rate }
1333
+ return { num: 1, den: frame.sampleRate };
1334
+ }
1335
+ else {
1336
+ // Video: Check CFR flag
1337
+ if (this.options.cfr) {
1338
+ // CFR mode: timeBase = 1/framerate = inverse(framerate)
1339
+ // Note: framerate is guaranteed to be set (validated in create())
1340
+ return avInvQ(this.options.framerate);
1341
+ }
1342
+ else {
1343
+ // VFR mode: Use frame's timeBase
1344
+ return frame.timeBase;
1345
+ }
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Rescale frame timestamps to calculated timeBase.
1350
+ *
1351
+ * Helper to avoid code duplication when rescaling timestamps.
1352
+ * Modifies the frame in-place.
1353
+ *
1354
+ * @param frame - Frame to rescale
1355
+ *
1356
+ * @param calculatedTimeBase - Target timeBase
1357
+ *
1358
+ * @internal
1359
+ */
1360
+ rescaleFrameTimestamps(frame, calculatedTimeBase) {
1361
+ const originalTimeBase = frame.timeBase;
1362
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, calculatedTimeBase);
1363
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, calculatedTimeBase);
1364
+ frame.timeBase = new Rational(calculatedTimeBase.num, calculatedTimeBase.den);
1365
+ }
1366
+ /**
1367
+ * Create buffer source for an input.
1368
+ *
1369
+ * @param label - Input label
1370
+ *
1371
+ * @param frame - First frame from this input
1372
+ *
1373
+ * @param timeBase - Calculated timeBase for this input (from calculateTimeBase)
1374
+ *
1375
+ * @returns BufferSrc filter context
1376
+ *
1377
+ * @throws {Error} If creation fails
1378
+ *
1379
+ * @internal
1380
+ */
1381
+ createBufferSource(label, frame, timeBase) {
1382
+ const filterName = frame.isVideo() ? 'buffer' : 'abuffer';
1383
+ const bufferFilter = Filter.getByName(filterName);
1384
+ if (!bufferFilter) {
1385
+ throw new Error(`${filterName} filter not found`);
1386
+ }
1387
+ let buffersrcCtx;
1388
+ if (frame.isVideo()) {
1389
+ // Video: allocate + set parameters
1390
+ buffersrcCtx = this.graph.allocFilter(bufferFilter, `in_${label}`);
1391
+ if (!buffersrcCtx) {
1392
+ throw new Error(`Failed to allocate buffer source for ${label}`);
1393
+ }
1394
+ const ret = buffersrcCtx.buffersrcParametersSet({
1395
+ width: frame.width,
1396
+ height: frame.height,
1397
+ format: frame.format,
1398
+ timeBase: timeBase,
1399
+ frameRate: this.options.framerate,
1400
+ sampleAspectRatio: frame.sampleAspectRatio,
1401
+ colorRange: frame.colorRange,
1402
+ colorSpace: frame.colorSpace,
1403
+ hwFramesCtx: frame.hwFramesCtx,
1404
+ });
1405
+ FFmpegError.throwIfError(ret, `Failed to set buffer source parameters for ${label}`);
1406
+ const initRet = buffersrcCtx.init(null);
1407
+ FFmpegError.throwIfError(initRet, `Failed to initialize buffer source for ${label}`);
1408
+ }
1409
+ else {
1410
+ // Audio: create with args string
1411
+ const formatName = avGetSampleFmtName(frame.format);
1412
+ const channelLayout = frame.channelLayout.mask === 0n ? `${frame.channelLayout.nbChannels}c` : frame.channelLayout.mask.toString();
1413
+ const args = `time_base=${timeBase.num}/${timeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
1414
+ buffersrcCtx = this.graph.createFilter(bufferFilter, `in_${label}`, args);
1415
+ if (!buffersrcCtx) {
1416
+ throw new Error(`Failed to create audio buffer source for ${label}`);
1417
+ }
1418
+ }
1419
+ return buffersrcCtx;
1420
+ }
1421
+ /**
1422
+ * Create buffer sink for an output.
1423
+ *
1424
+ * @param label - Output label
1425
+ *
1426
+ * @param isVideo - Whether this is a video output
1427
+ *
1428
+ * @returns BufferSink filter context
1429
+ *
1430
+ * @throws {Error} If creation fails
1431
+ *
1432
+ * @internal
1433
+ */
1434
+ createBufferSink(label, isVideo) {
1435
+ const filterName = isVideo ? 'buffersink' : 'abuffersink';
1436
+ const sinkFilter = Filter.getByName(filterName);
1437
+ if (!sinkFilter) {
1438
+ throw new Error(`${filterName} filter not found`);
1439
+ }
1440
+ const buffersinkCtx = this.graph.createFilter(sinkFilter, `out_${label}`, null);
1441
+ if (!buffersinkCtx) {
1442
+ throw new Error(`Failed to create buffer sink for ${label}`);
1443
+ }
1444
+ return buffersinkCtx;
1445
+ }
1446
+ /**
1447
+ * Parse filter description and build graph using segment API.
1448
+ *
1449
+ * @throws {Error} If parsing fails
1450
+ *
1451
+ * @throws {FFmpegError} If graph construction fails
1452
+ *
1453
+ * @internal
1454
+ */
1455
+ parseFilterDescription() {
1456
+ // Step 1: Parse the filter description into a segment
1457
+ const segment = this.graph.segmentParse(this.description);
1458
+ if (!segment) {
1459
+ throw new Error('Failed to parse filter segment');
1460
+ }
1461
+ let inputs;
1462
+ let outputs;
1463
+ try {
1464
+ // Step 2: Create filter instances (but don't initialize yet)
1465
+ let ret = segment.createFilters();
1466
+ FFmpegError.throwIfError(ret, 'Failed to create filters in segment');
1467
+ // Step 3: Set hw_device_ctx on filters that need it BEFORE initialization
1468
+ const filters = this.graph.filters;
1469
+ if (filters && this.options.hardware) {
1470
+ for (const filterCtx of filters) {
1471
+ const filter = filterCtx.filter;
1472
+ if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
1473
+ filterCtx.hwDeviceCtx = this.options.hardware.deviceContext;
1474
+ // Set extra_hw_frames if specified
1475
+ if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
1476
+ filterCtx.extraHWFrames = this.options.extraHWFrames;
1477
+ }
1478
+ }
1479
+ }
1480
+ }
1481
+ // Step 4: Apply options to filters
1482
+ ret = segment.applyOpts();
1483
+ FFmpegError.throwIfError(ret, 'Failed to apply options to segment');
1484
+ // Step 5: Initialize and link filters in the segment
1485
+ inputs = new FilterInOut();
1486
+ outputs = new FilterInOut();
1487
+ ret = segment.apply(inputs, outputs);
1488
+ FFmpegError.throwIfError(ret, 'Failed to apply segment');
1489
+ // Step 6: Link buffersrc filters to segment inputs
1490
+ this.linkBufferSources(inputs);
1491
+ // Step 7: Link segment outputs to buffersink filters
1492
+ this.linkBufferSinks(outputs);
1493
+ }
1494
+ finally {
1495
+ // Always free the FilterInOut structures and the segment, including on the
1496
+ // error paths above (apply/link failures) where they would otherwise leak.
1497
+ inputs?.free();
1498
+ outputs?.free();
1499
+ segment.free();
1500
+ }
1501
+ }
1502
+ /**
1503
+ * Link buffersrc filters to segment inputs.
1504
+ *
1505
+ * Iterates through FilterInOut chain and links by label.
1506
+ *
1507
+ * @param inputs - FilterInOut chain of segment inputs
1508
+ *
1509
+ * @throws {Error} If linking fails
1510
+ *
1511
+ * @internal
1512
+ */
1513
+ linkBufferSources(inputs) {
1514
+ let current = inputs;
1515
+ while (current?.name) {
1516
+ const label = current.name;
1517
+ const inputState = this.inputs.get(label);
1518
+ if (!inputState?.buffersrc) {
1519
+ throw new Error(`No buffersrc found for input label '${label}'`);
1520
+ }
1521
+ if (!current.filterCtx) {
1522
+ throw new Error(`FilterContext is null for input label '${label}'`);
1523
+ }
1524
+ // Link buffersrc → segment input
1525
+ const ret = inputState.buffersrc.link(0, current.filterCtx, current.padIdx);
1526
+ FFmpegError.throwIfError(ret, `Failed to link buffersrc '${label}' to segment`);
1527
+ current = current.next;
1528
+ }
1529
+ }
1530
+ /**
1531
+ * Link segment outputs to buffersink filters.
1532
+ *
1533
+ * Iterates through FilterInOut chain and links by label.
1534
+ *
1535
+ * @param outputs - FilterInOut chain of segment outputs
1536
+ *
1537
+ * @throws {Error} If linking fails
1538
+ *
1539
+ * @internal
1540
+ */
1541
+ linkBufferSinks(outputs) {
1542
+ let current = outputs;
1543
+ // Get media type from first input as default
1544
+ const firstInput = this.inputs.values().next().value;
1545
+ const defaultIsVideo = firstInput?.buffersrc?.filter?.name === 'buffer'; // 'buffer' = video, 'abuffer' = audio
1546
+ while (current?.name) {
1547
+ const label = current.name;
1548
+ const outputState = this.outputs.get(label);
1549
+ if (!outputState) {
1550
+ throw new Error(`No output state found for label '${label}'`);
1551
+ }
1552
+ if (!current.filterCtx) {
1553
+ throw new Error(`FilterContext is null for output label '${label}'`);
1554
+ }
1555
+ // Determine media type: use configured value or default from first input
1556
+ const outputConfig = this.options.outputs.find((o) => o.label === label);
1557
+ let isVideo = defaultIsVideo;
1558
+ if (outputConfig?.mediaType !== undefined) {
1559
+ isVideo = outputConfig.mediaType === AVMEDIA_TYPE_VIDEO;
1560
+ }
1561
+ // Create buffersink filter
1562
+ const buffersink = this.createBufferSink(label, isVideo);
1563
+ outputState.buffersink = buffersink;
1564
+ // Link segment output → buffersink
1565
+ const ret = current.filterCtx.link(current.padIdx, buffersink, 0);
1566
+ FFmpegError.throwIfError(ret, `Failed to link segment to buffersink '${label}'`);
1567
+ current = current.next;
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Post-process output frame from buffersink.
1572
+ *
1573
+ * Applies FFmpeg's fg_output_step() behavior:
1574
+ * 1. Sets frame.timeBase from buffersink (filters can change timeBase)
1575
+ * 2. Calculates video frame duration from frame rate if not set
1576
+ *
1577
+ * This must be called AFTER buffersinkGetFrame() for every output frame.
1578
+ *
1579
+ * @param frame - Output frame from buffersink
1580
+ *
1581
+ * @param buffersink - The buffersink context
1582
+ *
1583
+ * @internal
1584
+ */
1585
+ postProcessOutputFrame(frame, buffersink) {
1586
+ // Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
1587
+ // Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
1588
+ frame.timeBase = buffersink.buffersinkGetTimeBase();
1589
+ if (frame.isVideo() && !frame.duration) {
1590
+ const frameRate = buffersink.buffersinkGetFrameRate();
1591
+ if (frameRate.num > 0 && frameRate.den > 0) {
1592
+ frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
1593
+ }
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Close filter complex and release resources.
1598
+ *
1599
+ * Frees queued frames, filter graph and all filter contexts.
1600
+ * Safe to call multiple times.
1601
+ *
1602
+ * @example
1603
+ * ```typescript
1604
+ * complex.close();
1605
+ * ```
1606
+ *
1607
+ * @example
1608
+ * ```typescript
1609
+ * // Automatic cleanup with using
1610
+ * {
1611
+ * using complex = FilterComplexAPI.create('[0:v]scale=640:480[out]', { ... });
1612
+ * // Use complex...
1613
+ * } // Automatically freed
1614
+ * ```
1615
+ *
1616
+ * @see {@link Symbol.dispose} For automatic cleanup
1617
+ */
1618
+ close() {
1619
+ if (this.isClosed) {
1620
+ return;
1621
+ }
1622
+ this.isClosed = true;
1623
+ for (const inputState of this.inputs.values()) {
1624
+ for (const frame of inputState.queuedFrames) {
1625
+ frame.free();
1626
+ }
1627
+ inputState.queuedFrames = [];
1628
+ inputState.buffersrc?.free();
1629
+ inputState.buffersrc = null;
1630
+ }
1631
+ for (const outputState of this.outputs.values()) {
1632
+ outputState.buffersink?.free();
1633
+ outputState.buffersink = null;
1634
+ }
1635
+ this.inputs.clear();
1636
+ this.outputs.clear();
1637
+ this.frame.free();
1638
+ this.graph.free();
1639
+ this.initialized = false;
1640
+ this.initializePromise = null;
1641
+ }
1642
+ /**
1643
+ * Dispose of filter complex.
1644
+ *
1645
+ * Implements Disposable interface for automatic cleanup.
1646
+ *
1647
+ * @example
1648
+ * ```typescript
1649
+ * {
1650
+ * using complex = FilterComplexAPI.create('[0:v]scale=640:480[out]', { ... });
1651
+ * // Use complex...
1652
+ * } // Automatically freed
1653
+ * ```
1654
+ *
1655
+ * @see {@link close} For manual cleanup
1656
+ */
1657
+ [Symbol.dispose]() {
1658
+ this.close();
1659
+ }
1660
+ }
1661
+ //# sourceMappingURL=filter-complex.js.map