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