node-av 3.1.3 → 5.0.0

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 (192) hide show
  1. package/README.md +88 -52
  2. package/binding.gyp +23 -11
  3. package/dist/api/audio-frame-buffer.d.ts +201 -0
  4. package/dist/api/audio-frame-buffer.js +275 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +320 -78
  7. package/dist/api/bitstream-filter.js +684 -151
  8. package/dist/api/bitstream-filter.js.map +1 -1
  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_mjpeg.mjpeg +0 -0
  14. package/dist/api/data/test_vp8.ivf +0 -0
  15. package/dist/api/data/test_vp9.ivf +0 -0
  16. package/dist/api/decoder.d.ts +454 -77
  17. package/dist/api/decoder.js +1081 -271
  18. package/dist/api/decoder.js.map +1 -1
  19. package/dist/api/{media-input.d.ts → demuxer.d.ts} +295 -45
  20. package/dist/api/demuxer.js +1965 -0
  21. package/dist/api/demuxer.js.map +1 -0
  22. package/dist/api/encoder.d.ts +423 -132
  23. package/dist/api/encoder.js +1089 -240
  24. package/dist/api/encoder.js.map +1 -1
  25. package/dist/api/filter-complex.d.ts +769 -0
  26. package/dist/api/filter-complex.js +1596 -0
  27. package/dist/api/filter-complex.js.map +1 -0
  28. package/dist/api/filter-presets.d.ts +80 -5
  29. package/dist/api/filter-presets.js +117 -7
  30. package/dist/api/filter-presets.js.map +1 -1
  31. package/dist/api/filter.d.ts +561 -125
  32. package/dist/api/filter.js +1083 -274
  33. package/dist/api/filter.js.map +1 -1
  34. package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
  35. package/dist/api/fmp4-stream.js +539 -0
  36. package/dist/api/fmp4-stream.js.map +1 -0
  37. package/dist/api/hardware.d.ts +58 -6
  38. package/dist/api/hardware.js +127 -11
  39. package/dist/api/hardware.js.map +1 -1
  40. package/dist/api/index.d.ts +8 -4
  41. package/dist/api/index.js +17 -8
  42. package/dist/api/index.js.map +1 -1
  43. package/dist/api/io-stream.d.ts +6 -6
  44. package/dist/api/io-stream.js +5 -4
  45. package/dist/api/io-stream.js.map +1 -1
  46. package/dist/api/{media-output.d.ts → muxer.d.ts} +280 -66
  47. package/dist/api/muxer.js +1934 -0
  48. package/dist/api/muxer.js.map +1 -0
  49. package/dist/api/pipeline.d.ts +77 -29
  50. package/dist/api/pipeline.js +449 -439
  51. package/dist/api/pipeline.js.map +1 -1
  52. package/dist/api/rtp-stream.d.ts +312 -0
  53. package/dist/api/rtp-stream.js +630 -0
  54. package/dist/api/rtp-stream.js.map +1 -0
  55. package/dist/api/types.d.ts +533 -56
  56. package/dist/api/utilities/async-queue.d.ts +91 -0
  57. package/dist/api/utilities/async-queue.js +162 -0
  58. package/dist/api/utilities/async-queue.js.map +1 -0
  59. package/dist/api/utilities/audio-sample.d.ts +11 -1
  60. package/dist/api/utilities/audio-sample.js +10 -0
  61. package/dist/api/utilities/audio-sample.js.map +1 -1
  62. package/dist/api/utilities/channel-layout.d.ts +1 -0
  63. package/dist/api/utilities/channel-layout.js +1 -0
  64. package/dist/api/utilities/channel-layout.js.map +1 -1
  65. package/dist/api/utilities/image.d.ts +39 -1
  66. package/dist/api/utilities/image.js +38 -0
  67. package/dist/api/utilities/image.js.map +1 -1
  68. package/dist/api/utilities/index.d.ts +3 -0
  69. package/dist/api/utilities/index.js +6 -0
  70. package/dist/api/utilities/index.js.map +1 -1
  71. package/dist/api/utilities/media-type.d.ts +2 -1
  72. package/dist/api/utilities/media-type.js +1 -0
  73. package/dist/api/utilities/media-type.js.map +1 -1
  74. package/dist/api/utilities/pixel-format.d.ts +4 -1
  75. package/dist/api/utilities/pixel-format.js +3 -0
  76. package/dist/api/utilities/pixel-format.js.map +1 -1
  77. package/dist/api/utilities/sample-format.d.ts +6 -1
  78. package/dist/api/utilities/sample-format.js +5 -0
  79. package/dist/api/utilities/sample-format.js.map +1 -1
  80. package/dist/api/utilities/scheduler.d.ts +138 -0
  81. package/dist/api/utilities/scheduler.js +98 -0
  82. package/dist/api/utilities/scheduler.js.map +1 -0
  83. package/dist/api/utilities/streaming.d.ts +105 -15
  84. package/dist/api/utilities/streaming.js +201 -12
  85. package/dist/api/utilities/streaming.js.map +1 -1
  86. package/dist/api/utilities/timestamp.d.ts +15 -1
  87. package/dist/api/utilities/timestamp.js +14 -0
  88. package/dist/api/utilities/timestamp.js.map +1 -1
  89. package/dist/api/utilities/whisper-model.d.ts +310 -0
  90. package/dist/api/utilities/whisper-model.js +528 -0
  91. package/dist/api/utilities/whisper-model.js.map +1 -0
  92. package/dist/api/webrtc-stream.d.ts +288 -0
  93. package/dist/api/webrtc-stream.js +440 -0
  94. package/dist/api/webrtc-stream.js.map +1 -0
  95. package/dist/api/whisper.d.ts +324 -0
  96. package/dist/api/whisper.js +362 -0
  97. package/dist/api/whisper.js.map +1 -0
  98. package/dist/constants/constants.d.ts +54 -2
  99. package/dist/constants/constants.js +48 -1
  100. package/dist/constants/constants.js.map +1 -1
  101. package/dist/constants/encoders.d.ts +2 -1
  102. package/dist/constants/encoders.js +4 -3
  103. package/dist/constants/encoders.js.map +1 -1
  104. package/dist/constants/hardware.d.ts +26 -0
  105. package/dist/constants/hardware.js +27 -0
  106. package/dist/constants/hardware.js.map +1 -0
  107. package/dist/constants/index.d.ts +1 -0
  108. package/dist/constants/index.js +1 -0
  109. package/dist/constants/index.js.map +1 -1
  110. package/dist/ffmpeg/index.d.ts +3 -3
  111. package/dist/ffmpeg/index.js +3 -3
  112. package/dist/ffmpeg/utils.d.ts +27 -0
  113. package/dist/ffmpeg/utils.js +28 -16
  114. package/dist/ffmpeg/utils.js.map +1 -1
  115. package/dist/lib/binding.d.ts +22 -11
  116. package/dist/lib/binding.js.map +1 -1
  117. package/dist/lib/codec-context.d.ts +87 -0
  118. package/dist/lib/codec-context.js +125 -4
  119. package/dist/lib/codec-context.js.map +1 -1
  120. package/dist/lib/codec-parameters.d.ts +229 -1
  121. package/dist/lib/codec-parameters.js +264 -0
  122. package/dist/lib/codec-parameters.js.map +1 -1
  123. package/dist/lib/codec-parser.d.ts +23 -0
  124. package/dist/lib/codec-parser.js +25 -0
  125. package/dist/lib/codec-parser.js.map +1 -1
  126. package/dist/lib/codec.d.ts +26 -4
  127. package/dist/lib/codec.js +35 -0
  128. package/dist/lib/codec.js.map +1 -1
  129. package/dist/lib/dictionary.js +1 -0
  130. package/dist/lib/dictionary.js.map +1 -1
  131. package/dist/lib/error.js +1 -1
  132. package/dist/lib/error.js.map +1 -1
  133. package/dist/lib/fifo.d.ts +416 -0
  134. package/dist/lib/fifo.js +453 -0
  135. package/dist/lib/fifo.js.map +1 -0
  136. package/dist/lib/filter-context.d.ts +52 -11
  137. package/dist/lib/filter-context.js +56 -12
  138. package/dist/lib/filter-context.js.map +1 -1
  139. package/dist/lib/filter-graph.d.ts +9 -0
  140. package/dist/lib/filter-graph.js +13 -0
  141. package/dist/lib/filter-graph.js.map +1 -1
  142. package/dist/lib/filter.d.ts +21 -0
  143. package/dist/lib/filter.js +28 -0
  144. package/dist/lib/filter.js.map +1 -1
  145. package/dist/lib/format-context.d.ts +48 -14
  146. package/dist/lib/format-context.js +76 -7
  147. package/dist/lib/format-context.js.map +1 -1
  148. package/dist/lib/frame.d.ts +264 -1
  149. package/dist/lib/frame.js +351 -1
  150. package/dist/lib/frame.js.map +1 -1
  151. package/dist/lib/hardware-device-context.d.ts +3 -2
  152. package/dist/lib/hardware-device-context.js.map +1 -1
  153. package/dist/lib/index.d.ts +2 -0
  154. package/dist/lib/index.js +4 -0
  155. package/dist/lib/index.js.map +1 -1
  156. package/dist/lib/input-format.d.ts +21 -0
  157. package/dist/lib/input-format.js +42 -2
  158. package/dist/lib/input-format.js.map +1 -1
  159. package/dist/lib/native-types.d.ts +76 -27
  160. package/dist/lib/option.d.ts +25 -13
  161. package/dist/lib/option.js +28 -0
  162. package/dist/lib/option.js.map +1 -1
  163. package/dist/lib/output-format.d.ts +22 -1
  164. package/dist/lib/output-format.js +28 -0
  165. package/dist/lib/output-format.js.map +1 -1
  166. package/dist/lib/packet.d.ts +35 -0
  167. package/dist/lib/packet.js +52 -2
  168. package/dist/lib/packet.js.map +1 -1
  169. package/dist/lib/rational.d.ts +18 -0
  170. package/dist/lib/rational.js +19 -0
  171. package/dist/lib/rational.js.map +1 -1
  172. package/dist/lib/stream.d.ts +126 -0
  173. package/dist/lib/stream.js +188 -5
  174. package/dist/lib/stream.js.map +1 -1
  175. package/dist/lib/sync-queue.d.ts +179 -0
  176. package/dist/lib/sync-queue.js +197 -0
  177. package/dist/lib/sync-queue.js.map +1 -0
  178. package/dist/lib/types.d.ts +49 -1
  179. package/dist/lib/utilities.d.ts +281 -53
  180. package/dist/lib/utilities.js +298 -55
  181. package/dist/lib/utilities.js.map +1 -1
  182. package/install/check.js +2 -2
  183. package/package.json +37 -26
  184. package/dist/api/fmp4.js +0 -710
  185. package/dist/api/fmp4.js.map +0 -1
  186. package/dist/api/media-input.js +0 -1075
  187. package/dist/api/media-input.js.map +0 -1
  188. package/dist/api/media-output.js +0 -1040
  189. package/dist/api/media-output.js.map +0 -1
  190. package/dist/api/webrtc.d.ts +0 -664
  191. package/dist/api/webrtc.js +0 -1132
  192. package/dist/api/webrtc.js.map +0 -1
@@ -1,6 +1,67 @@
1
- import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE } from '../constants/constants.js';
2
- import { FFmpegError, Filter, FilterGraph, FilterInOut, Frame } from '../lib/index.js';
3
- import { avGetSampleFmtName } from '../lib/utilities.js';
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ /* eslint-disable @stylistic/indent-binary-ops */
54
+ import { AV_BUFFERSRC_FLAG_PUSH, AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, EOF } from '../constants/constants.js';
55
+ import { FFmpegError } from '../lib/error.js';
56
+ import { FilterGraph } from '../lib/filter-graph.js';
57
+ import { FilterInOut } from '../lib/filter-inout.js';
58
+ import { Filter } from '../lib/filter.js';
59
+ import { Frame } from '../lib/frame.js';
60
+ import { Rational } from '../lib/rational.js';
61
+ import { avGetSampleFmtName, avInvQ, avRescaleQ } from '../lib/utilities.js';
62
+ import { FRAME_THREAD_QUEUE_SIZE } from './constants.js';
63
+ import { AsyncQueue } from './utilities/async-queue.js';
64
+ import { Scheduler } from './utilities/scheduler.js';
4
65
  /**
5
66
  * High-level filter API for audio and video processing.
6
67
  *
@@ -44,8 +105,20 @@ export class FilterAPI {
44
105
  options;
45
106
  buffersrcCtx = null;
46
107
  buffersinkCtx = null;
108
+ frame = new Frame(); // Reusable frame for receive operations
109
+ initializePromise = null;
47
110
  initialized = false;
48
111
  isClosed = false;
112
+ // Auto-calculated timeBase from first frame
113
+ calculatedTimeBase = null;
114
+ // Track last frame properties for change detection (for dropOnChange/allowReinit)
115
+ lastFrameProps = null;
116
+ // Worker pattern for push-based processing
117
+ inputQueue;
118
+ outputQueue;
119
+ workerPromise = null;
120
+ nextComponent = null;
121
+ pipeToPromise = null;
49
122
  /**
50
123
  * @param graph - Filter graph instance
51
124
  *
@@ -59,50 +132,53 @@ export class FilterAPI {
59
132
  this.graph = graph;
60
133
  this.description = description;
61
134
  this.options = options;
135
+ this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
136
+ this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
62
137
  }
63
138
  /**
64
139
  * Create a filter with specified description and configuration.
65
140
  *
66
- * Creates and allocates filter graph immediately.
67
- * Filter configuration is completed on first frame with frame properties.
68
- * Hardware frames context is automatically detected from input frames.
69
- *
70
141
  * Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
71
142
  *
72
143
  * @param description - Filter graph description
73
144
  *
74
- * @param options - Filter options including required timeBase
145
+ * @param options - Filter options
75
146
  *
76
147
  * @returns Configured filter instance
77
148
  *
149
+ * @throws {Error} If cfr=true but framerate is not set
150
+ *
78
151
  * @example
79
152
  * ```typescript
80
- * // Simple video filter
81
- * const filter = FilterAPI.create('scale=640:480', {
82
- * timeBase: video.timeBase
83
- * });
153
+ * // Simple video filter (VFR mode, auto timeBase)
154
+ * const filter = FilterAPI.create('scale=640:480');
84
155
  * ```
85
156
  *
86
157
  * @example
87
158
  * ```typescript
88
- * // Complex filter chain
89
- * const filter = FilterAPI.create('crop=640:480:0:0,rotate=PI/4', {
90
- * timeBase: video.timeBase
159
+ * // CFR mode with constant framerate
160
+ * const filter = FilterAPI.create('scale=1920:1080', {
161
+ * cfr: true,
162
+ * framerate: { num: 25, den: 1 }
91
163
  * });
92
164
  * ```
93
165
  *
94
166
  * @example
95
167
  * ```typescript
96
- * // Audio filter
97
- * const filter = FilterAPI.create('volume=0.5,aecho=0.8:0.9:1000:0.3', {
98
- * timeBase: audio.timeBase
168
+ * // Audio filter with resampling
169
+ * const filter = FilterAPI.create('aformat=sample_fmts=s16:sample_rates=44100', {
170
+ * audioResampleOpts: 'async=1'
99
171
  * });
100
172
  * ```
101
173
  *
102
174
  * @see {@link process} For frame processing
103
175
  * @see {@link FilterOptions} For configuration options
104
176
  */
105
- static create(description, options) {
177
+ static create(description, options = {}) {
178
+ // Validate options: CFR requires framerate
179
+ if (options.cfr && !options.framerate) {
180
+ throw new Error('cfr=true requires framerate to be set');
181
+ }
106
182
  // Create graph
107
183
  const graph = new FilterGraph();
108
184
  graph.alloc();
@@ -147,6 +223,260 @@ export class FilterAPI {
147
223
  get isFilterInitialized() {
148
224
  return this.initialized;
149
225
  }
226
+ /**
227
+ * Get buffersink filter context.
228
+ *
229
+ * Provides access to the buffersink filter context for advanced operations.
230
+ * Returns null if filter is not initialized.
231
+ *
232
+ * @returns Buffersink context or null
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const sink = filter.buffersink;
237
+ * if (sink) {
238
+ * const fr = sink.buffersinkGetFrameRate();
239
+ * console.log(`Output frame rate: ${fr.num}/${fr.den}`);
240
+ * }
241
+ * ```
242
+ */
243
+ get buffersink() {
244
+ return this.buffersinkCtx;
245
+ }
246
+ /**
247
+ * Output frame rate from filter graph.
248
+ *
249
+ * Returns the frame rate determined by the filter graph output.
250
+ * Returns null if filter is not initialized or frame rate is not set.
251
+ *
252
+ * Direct mapping to av_buffersink_get_frame_rate().
253
+ *
254
+ * @returns Frame rate or null if not available
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const frameRate = filter.frameRate;
259
+ * if (frameRate) {
260
+ * console.log(`Filter output: ${frameRate.num}/${frameRate.den} fps`);
261
+ * }
262
+ * ```
263
+ *
264
+ * @see {@link timeBase} For output timebase
265
+ */
266
+ get frameRate() {
267
+ if (!this.initialized || !this.buffersinkCtx) {
268
+ return null;
269
+ }
270
+ const fr = this.buffersinkCtx.buffersinkGetFrameRate();
271
+ // Return null if frame rate is not set (0/0 or 0/1)
272
+ if (fr.num <= 0 || fr.den <= 0) {
273
+ return null;
274
+ }
275
+ return fr;
276
+ }
277
+ /**
278
+ * Output time base from filter graph.
279
+ *
280
+ * Returns the time base of the buffersink output.
281
+ * Matches FFmpeg CLI's av_buffersink_get_time_base() behavior.
282
+ *
283
+ * Direct mapping to av_buffersink_get_time_base().
284
+ *
285
+ * @returns Time base or null if not initialized
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const timeBase = filter.timeBase;
290
+ * if (timeBase) {
291
+ * console.log(`Filter timebase: ${timeBase.num}/${timeBase.den}`);
292
+ * }
293
+ * ```
294
+ *
295
+ * @see {@link frameRate} For output frame rate
296
+ */
297
+ get timeBase() {
298
+ if (!this.initialized || !this.buffersinkCtx) {
299
+ return null;
300
+ }
301
+ return this.buffersinkCtx.buffersinkGetTimeBase();
302
+ }
303
+ /**
304
+ * Output format from filter graph.
305
+ *
306
+ * Returns the pixel format (video) or sample format (audio) of the buffersink output.
307
+ * Matches FFmpeg CLI's av_buffersink_get_format() behavior.
308
+ *
309
+ * Direct mapping to av_buffersink_get_format().
310
+ *
311
+ * @returns Pixel format or sample format, or null if not initialized
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * const format = filter.format;
316
+ * if (format !== null) {
317
+ * console.log(`Filter output format: ${format}`);
318
+ * }
319
+ * ```
320
+ */
321
+ get format() {
322
+ if (!this.initialized || !this.buffersinkCtx) {
323
+ return null;
324
+ }
325
+ return this.buffersinkCtx.buffersinkGetFormat();
326
+ }
327
+ /**
328
+ * Output dimensions from filter graph (video only).
329
+ *
330
+ * Returns the width and height of the buffersink output.
331
+ * Matches FFmpeg CLI's av_buffersink_get_w() and av_buffersink_get_h() behavior.
332
+ * Only meaningful for video filters.
333
+ *
334
+ * Direct mapping to av_buffersink_get_w() and av_buffersink_get_h().
335
+ *
336
+ * @returns Dimensions object or null if not initialized
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * const dims = filter.dimensions;
341
+ * if (dims) {
342
+ * console.log(`Filter output: ${dims.width}x${dims.height}`);
343
+ * }
344
+ * ```
345
+ */
346
+ get dimensions() {
347
+ if (!this.initialized || !this.buffersinkCtx) {
348
+ return null;
349
+ }
350
+ return {
351
+ width: this.buffersinkCtx.buffersinkGetWidth(),
352
+ height: this.buffersinkCtx.buffersinkGetHeight(),
353
+ };
354
+ }
355
+ /**
356
+ * Output sample rate from filter graph (audio only).
357
+ *
358
+ * Returns the sample rate of the buffersink output.
359
+ * Matches FFmpeg CLI's av_buffersink_get_sample_rate() behavior.
360
+ * Only meaningful for audio filters.
361
+ *
362
+ * Direct mapping to av_buffersink_get_sample_rate().
363
+ *
364
+ * @returns Sample rate or null if not initialized
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const sampleRate = filter.sampleRate;
369
+ * if (sampleRate) {
370
+ * console.log(`Filter output sample rate: ${sampleRate} Hz`);
371
+ * }
372
+ * ```
373
+ */
374
+ get sampleRate() {
375
+ if (!this.initialized || !this.buffersinkCtx) {
376
+ return null;
377
+ }
378
+ return this.buffersinkCtx.buffersinkGetSampleRate();
379
+ }
380
+ /**
381
+ * Output channel layout from filter graph (audio only).
382
+ *
383
+ * Returns the channel layout of the buffersink output.
384
+ * Matches FFmpeg CLI's av_buffersink_get_ch_layout() behavior.
385
+ * Only meaningful for audio filters.
386
+ *
387
+ * Direct mapping to av_buffersink_get_ch_layout().
388
+ *
389
+ * @returns Channel layout or null if not initialized
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * const layout = filter.channelLayout;
394
+ * if (layout) {
395
+ * console.log(`Filter output channels: ${layout.nbChannels}`);
396
+ * }
397
+ * ```
398
+ */
399
+ get channelLayout() {
400
+ if (!this.initialized || !this.buffersinkCtx) {
401
+ return null;
402
+ }
403
+ return this.buffersinkCtx.buffersinkGetChannelLayout();
404
+ }
405
+ /**
406
+ * Output color space from filter graph (video only).
407
+ *
408
+ * Returns the color space of the buffersink output.
409
+ * Matches FFmpeg CLI's av_buffersink_get_colorspace() behavior.
410
+ * Only meaningful for video filters.
411
+ *
412
+ * Direct mapping to av_buffersink_get_colorspace().
413
+ *
414
+ * @returns Color space or null if not initialized
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * const colorSpace = filter.colorSpace;
419
+ * if (colorSpace !== null) {
420
+ * console.log(`Filter output color space: ${colorSpace}`);
421
+ * }
422
+ * ```
423
+ */
424
+ get colorSpace() {
425
+ if (!this.initialized || !this.buffersinkCtx) {
426
+ return null;
427
+ }
428
+ return this.buffersinkCtx.buffersinkGetColorspace();
429
+ }
430
+ /**
431
+ * Output color range from filter graph (video only).
432
+ *
433
+ * Returns the color range of the buffersink output.
434
+ * Matches FFmpeg CLI's av_buffersink_get_color_range() behavior.
435
+ * Only meaningful for video filters.
436
+ *
437
+ * Direct mapping to av_buffersink_get_color_range().
438
+ *
439
+ * @returns Color range or null if not initialized
440
+ *
441
+ * @example
442
+ * ```typescript
443
+ * const colorRange = filter.colorRange;
444
+ * if (colorRange !== null) {
445
+ * console.log(`Filter output color range: ${colorRange}`);
446
+ * }
447
+ * ```
448
+ */
449
+ get colorRange() {
450
+ if (!this.initialized || !this.buffersinkCtx) {
451
+ return null;
452
+ }
453
+ return this.buffersinkCtx.buffersinkGetColorRange();
454
+ }
455
+ /**
456
+ * Output sample aspect ratio from filter graph (video only).
457
+ *
458
+ * Returns the sample aspect ratio of the buffersink output.
459
+ * Matches FFmpeg CLI's av_buffersink_get_sample_aspect_ratio() behavior.
460
+ * Only meaningful for video filters.
461
+ *
462
+ * Direct mapping to av_buffersink_get_sample_aspect_ratio().
463
+ *
464
+ * @returns Sample aspect ratio or null if not initialized
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * const sar = filter.sampleAspectRatio;
469
+ * if (sar) {
470
+ * console.log(`Filter output SAR: ${sar.num}:${sar.den}`);
471
+ * }
472
+ * ```
473
+ */
474
+ get sampleAspectRatio() {
475
+ if (!this.initialized || !this.buffersinkCtx) {
476
+ return null;
477
+ }
478
+ return this.buffersinkCtx.buffersinkGetSampleAspectRatio();
479
+ }
150
480
  /**
151
481
  * Check if filter is ready for processing.
152
482
  *
@@ -182,28 +512,33 @@ export class FilterAPI {
182
512
  return !this.isClosed && this.initialized ? this.graph.dump() : null;
183
513
  }
184
514
  /**
185
- * Process a frame through the filter.
515
+ * Send a frame to the filter.
186
516
  *
187
- * Applies filter operations to input frame.
517
+ * Sends a frame to the filter for processing.
518
+ * Does not return filtered frames - use {@link receive} to retrieve frames.
188
519
  * On first frame, automatically builds filter graph with frame properties.
189
- * May buffer frames internally before producing output.
190
- * Hardware frames context is automatically detected from frame.
191
- * Returns null if filter is closed and frame is null.
520
+ * A single input frame can produce zero, one, or multiple output frames.
192
521
  *
193
- * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
522
+ * **Important**: This method only SENDS the frame to the filter.
523
+ * You must call {@link receive} separately (potentially multiple times) to get filtered frames.
194
524
  *
195
- * @param frame - Input frame to process (or null to flush)
525
+ * Direct mapping to av_buffersrc_add_frame().
196
526
  *
197
- * @returns Filtered frame or null if buffered
527
+ * @param frame - Input frame to send to filter
198
528
  *
199
529
  * @throws {Error} If filter could not be initialized
200
530
  *
201
- * @throws {FFmpegError} If processing fails
531
+ * @throws {FFmpegError} If sending frame fails
202
532
  *
203
533
  * @example
204
534
  * ```typescript
205
- * const output = await filter.process(inputFrame);
206
- * if (output) {
535
+ * // Send frame and receive filtered frames
536
+ * await filter.process(inputFrame);
537
+ *
538
+ * // Receive all available filtered frames
539
+ * while (true) {
540
+ * const output = await filter.receive();
541
+ * if (!output) break;
207
542
  * console.log(`Got filtered frame: pts=${output.pts}`);
208
543
  * output.free();
209
544
  * }
@@ -211,228 +546,267 @@ export class FilterAPI {
211
546
  *
212
547
  * @example
213
548
  * ```typescript
214
- * // Process frame - may buffer internally
215
- * const output = await filter.process(frame);
216
- * if (output) {
217
- * // Got output immediately
218
- * yield output;
549
+ * for await (const frame of decoder.frames(input.packets())) {
550
+ * // Send frame
551
+ * await filter.process(frame);
552
+ *
553
+ * // Receive available filtered frames
554
+ * let output;
555
+ * while ((output = await filter.receive())) {
556
+ * await encoder.encode(output);
557
+ * output.free();
558
+ * }
559
+ * frame.free();
219
560
  * }
220
- * // For buffered frames, use the frames() async generator
221
561
  * ```
222
562
  *
563
+ * @see {@link receive} For receiving filtered frames
564
+ * @see {@link processAll} For combined send+receive operation
223
565
  * @see {@link frames} For processing frame streams
224
566
  * @see {@link flush} For end-of-stream handling
567
+ * @see {@link processSync} For synchronous version
225
568
  */
226
569
  async process(frame) {
227
570
  if (this.isClosed) {
228
- return null;
571
+ return;
229
572
  }
230
573
  // Open filter if not already done
231
- if (!this.initialized) {
232
- if (!frame) {
233
- return null;
234
- }
235
- await this.initialize(frame);
574
+ this.initializePromise ??= this.initialize(frame);
575
+ await this.initializePromise;
576
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
577
+ throw new Error('Could not initialize filter contexts');
236
578
  }
579
+ // Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
580
+ if (!this.checkFramePropertiesChanged(frame)) {
581
+ // Frame dropped due to property change
582
+ return;
583
+ }
584
+ // If reinitialized, reinitialize now
237
585
  if (!this.initialized) {
238
- return null;
586
+ this.initializePromise = this.initialize(frame);
587
+ await this.initializePromise;
239
588
  }
240
589
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
241
- throw new Error('Could not initialize filter contexts');
242
- }
243
- // Send frame to filter
244
- const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame);
245
- FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
246
- // Try to get filtered frame
247
- const outputFrame = new Frame();
248
- outputFrame.alloc();
249
- const getRet = await this.buffersinkCtx.buffersinkGetFrame(outputFrame);
250
- if (getRet >= 0) {
251
- return outputFrame;
252
- }
253
- else if (getRet === AVERROR_EAGAIN) {
254
- // Need more input
255
- outputFrame.free();
256
- return null;
590
+ throw new Error('Could not reinitialize filter contexts');
257
591
  }
258
- else {
259
- outputFrame.free();
260
- FFmpegError.throwIfError(getRet, 'Failed to get frame from filter');
261
- return null;
592
+ // Rescale timestamps to filter's timeBase
593
+ if (this.calculatedTimeBase) {
594
+ const originalTimeBase = frame.timeBase;
595
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
596
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
597
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
262
598
  }
599
+ // Send frame to filter with PUSH flag for immediate processing
600
+ const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
601
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
263
602
  }
264
603
  /**
265
- * Process a frame through the filter synchronously.
604
+ * Send a frame to the filter synchronously.
266
605
  * Synchronous version of process.
267
606
  *
268
- * Applies filter operations to input frame.
607
+ * Sends a frame to the filter for processing.
608
+ * Does not return filtered frames - use {@link receiveSync} to retrieve frames.
269
609
  * On first frame, automatically builds filter graph with frame properties.
270
- * May buffer frames internally before producing output.
271
- * Hardware frames context is automatically detected from frame.
272
- * Returns null if filter is closed and frame is null.
610
+ * A single input frame can produce zero, one, or multiple output frames.
273
611
  *
274
- * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
612
+ * **Important**: This method only SENDS the frame to the filter.
613
+ * You must call {@link receiveSync} separately (potentially multiple times) to get filtered frames.
275
614
  *
276
- * @param frame - Input frame to process (or null to flush)
615
+ * Direct mapping to av_buffersrc_add_frame().
277
616
  *
278
- * @returns Filtered frame or null if buffered
617
+ * @param frame - Input frame to send to filter
279
618
  *
280
619
  * @throws {Error} If filter could not be initialized
281
620
  *
282
- * @throws {FFmpegError} If processing fails
621
+ * @throws {FFmpegError} If sending frame fails
283
622
  *
284
623
  * @example
285
624
  * ```typescript
286
- * const output = filter.processSync(inputFrame);
287
- * if (output) {
625
+ * // Send frame and receive filtered frames
626
+ * filter.processSync(inputFrame);
627
+ *
628
+ * // Receive all available filtered frames
629
+ * let output;
630
+ * while ((output = filter.receiveSync())) {
288
631
  * console.log(`Got filtered frame: pts=${output.pts}`);
289
632
  * output.free();
290
633
  * }
291
634
  * ```
292
635
  *
293
- * @example
294
- * ```typescript
295
- * // Process frame - may buffer internally
296
- * const output = filter.processSync(frame);
297
- * if (output) {
298
- * // Got output immediately
299
- * yield output;
300
- * }
301
- * // For buffered frames, use the framesSync() generator
302
- * ```
303
- *
636
+ * @see {@link receiveSync} For receiving filtered frames
637
+ * @see {@link processAllSync} For combined send+receive operation
638
+ * @see {@link framesSync} For processing frame streams
639
+ * @see {@link flushSync} For end-of-stream handling
304
640
  * @see {@link process} For async version
305
641
  */
306
642
  processSync(frame) {
307
643
  if (this.isClosed) {
308
- return null;
644
+ return;
309
645
  }
310
646
  // Open filter if not already done
311
647
  if (!this.initialized) {
312
- if (!frame) {
313
- return null;
314
- }
315
648
  this.initializeSync(frame);
316
649
  }
317
650
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
318
651
  throw new Error('Could not initialize filter contexts');
319
652
  }
320
- // Send frame to filter
321
- const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame);
322
- FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
323
- // Try to get filtered frame
324
- const outputFrame = new Frame();
325
- outputFrame.alloc();
326
- const getRet = this.buffersinkCtx.buffersinkGetFrameSync(outputFrame);
327
- if (getRet >= 0) {
328
- return outputFrame;
329
- }
330
- else if (getRet === AVERROR_EAGAIN) {
331
- // Need more input
332
- outputFrame.free();
333
- return null;
653
+ // Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
654
+ if (!this.checkFramePropertiesChanged(frame)) {
655
+ // Frame dropped due to property change
656
+ return;
334
657
  }
335
- else {
336
- outputFrame.free();
337
- FFmpegError.throwIfError(getRet, 'Failed to get frame from filter');
338
- return null;
658
+ // If reinitialized, reinitialize now
659
+ if (!this.initialized) {
660
+ this.initializeSync(frame);
661
+ }
662
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
663
+ throw new Error('Could not reinitialize filter contexts');
339
664
  }
665
+ // Rescale timestamps to filter's timeBase
666
+ if (this.calculatedTimeBase) {
667
+ const originalTimeBase = frame.timeBase;
668
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
669
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
670
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
671
+ }
672
+ // Send frame to filter with PUSH flag for immediate processing
673
+ const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
674
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
340
675
  }
341
676
  /**
342
- * Process multiple frames at once.
677
+ * Process a frame through the filter.
678
+ *
679
+ * Applies filter operations to input frame and receives all available output frames.
680
+ * Returns array of frames - may be empty if filter needs more input.
681
+ * On first frame, automatically builds filter graph with frame properties.
682
+ * One input frame can produce zero, one, or multiple output frames depending on filter.
683
+ * Hardware frames context is automatically detected from frame.
343
684
  *
344
- * Processes batch of frames and drains all output.
345
- * Useful for filters that buffer multiple frames.
685
+ * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
346
686
  *
347
- * @param frames - Array of input frames
687
+ * @param frame - Input frame to process
348
688
  *
349
- * @returns Array of all output frames (empty if filter closed)
689
+ * @returns Array of filtered frames (empty if buffered or filter closed)
350
690
  *
351
- * @throws {Error} If filter not ready
691
+ * @throws {Error} If filter could not be initialized
352
692
  *
353
693
  * @throws {FFmpegError} If processing fails
354
694
  *
355
695
  * @example
356
696
  * ```typescript
357
- * const outputs = await filter.processMultiple([frame1, frame2, frame3]);
358
- * for (const output of outputs) {
359
- * console.log(`Output frame: pts=${output.pts}`);
697
+ * const frames = await filter.processAll(inputFrame);
698
+ * for (const output of frames) {
699
+ * console.log(`Got filtered frame: pts=${output.pts}`);
360
700
  * output.free();
361
701
  * }
362
702
  * ```
363
703
  *
704
+ * @example
705
+ * ```typescript
706
+ * // Process frame - may return multiple frames (e.g. fps filter)
707
+ * const frames = await filter.processAll(frame);
708
+ * for (const output of frames) {
709
+ * yield output;
710
+ * }
711
+ * ```
712
+ *
364
713
  * @see {@link process} For single frame processing
714
+ * @see {@link frames} For processing frame streams
715
+ * @see {@link flush} For end-of-stream handling
716
+ * @see {@link processAllSync} For synchronous version
365
717
  */
366
- async processMultiple(frames) {
367
- const outputFrames = [];
368
- for (const frame of frames) {
369
- const output = await this.process(frame);
370
- if (output) {
371
- outputFrames.push(output);
372
- }
373
- // Drain any additional frames
374
- while (!this.isClosed) {
375
- const additional = await this.receive();
376
- if (!additional)
377
- break;
378
- outputFrames.push(additional);
379
- }
718
+ async processAll(frame) {
719
+ if (frame) {
720
+ await this.process(frame);
721
+ }
722
+ else {
723
+ await this.flush();
724
+ }
725
+ // Receive all available frames
726
+ const frames = [];
727
+ while (true) {
728
+ const outputFrame = await this.receive();
729
+ if (!outputFrame)
730
+ break; // Stop on EAGAIN or EOF
731
+ frames.push(outputFrame); // Only push actual frames
380
732
  }
381
- return outputFrames;
733
+ return frames;
382
734
  }
383
735
  /**
384
- * Process multiple frames at once synchronously.
385
- * Synchronous version of processMultiple.
736
+ * Process a frame through the filter synchronously.
737
+ * Synchronous version of processAll.
386
738
  *
387
- * Processes batch of frames and drains all output.
388
- * Useful for filters that buffer multiple frames.
739
+ * Applies filter operations to input frame and receives all available output frames.
740
+ * Returns array of frames - may be empty if filter needs more input.
741
+ * On first frame, automatically builds filter graph with frame properties.
742
+ * One input frame can produce zero, one, or multiple output frames depending on filter.
743
+ * Hardware frames context is automatically detected from frame.
389
744
  *
390
- * @param frames - Array of input frames
745
+ * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
391
746
  *
392
- * @returns Array of all output frames (empty if filter closed)
747
+ * @param frame - Input frame to process
393
748
  *
394
- * @throws {Error} If filter not ready
749
+ * @returns Array of filtered frames (empty if buffered or filter closed)
750
+ *
751
+ * @throws {Error} If filter could not be initialized
395
752
  *
396
753
  * @throws {FFmpegError} If processing fails
397
754
  *
398
755
  * @example
399
756
  * ```typescript
400
- * const outputs = filter.processMultipleSync([frame1, frame2, frame3]);
757
+ * const outputs = filter.processAllSync(inputFrame);
401
758
  * for (const output of outputs) {
402
- * console.log(`Output frame: pts=${output.pts}`);
759
+ * console.log(`Got filtered frame: pts=${output.pts}`);
403
760
  * output.free();
404
761
  * }
405
762
  * ```
406
763
  *
407
- * @see {@link processMultiple} For async version
764
+ * @example
765
+ * ```typescript
766
+ * // Process frame - may return multiple frames (e.g. fps filter)
767
+ * const outputs = filter.processAllSync(frame);
768
+ * for (const output of outputs) {
769
+ * yield output;
770
+ * }
771
+ * ```
772
+ *
773
+ * @see {@link processSync} For single frame processing
774
+ * @see {@link framesSync} For processing frame streams
775
+ * @see {@link flushSync} For end-of-stream handling
776
+ * @see {@link process} For async version
408
777
  */
409
- processMultipleSync(frames) {
410
- const outputFrames = [];
411
- for (const frame of frames) {
412
- const output = this.processSync(frame);
413
- if (output) {
414
- outputFrames.push(output);
415
- }
416
- // Drain any additional frames
417
- while (!this.isClosed) {
418
- const additional = this.receiveSync();
419
- if (!additional)
420
- break;
421
- outputFrames.push(additional);
422
- }
778
+ processAllSync(frame) {
779
+ if (frame) {
780
+ this.processSync(frame);
423
781
  }
424
- return outputFrames;
782
+ else {
783
+ this.flushSync();
784
+ }
785
+ // Receive all available frames
786
+ const frames = [];
787
+ while (true) {
788
+ const outputFrame = this.receiveSync();
789
+ if (!outputFrame)
790
+ break; // Stop on EAGAIN or EOF
791
+ frames.push(outputFrame); // Only push actual frames
792
+ }
793
+ return frames;
425
794
  }
426
795
  /**
427
796
  * Process frame stream through filter.
428
797
  *
429
798
  * High-level async generator for filtering frame streams.
430
- * Automatically handles buffering and flushing.
431
- * Frees input frames after processing.
799
+ * Filter is only flushed when EOF (null) signal is explicitly received.
800
+ * Primary interface for stream-based filtering.
801
+ *
802
+ * **EOF Handling:**
803
+ * - Send null to flush filter and get remaining buffered frames
804
+ * - Generator yields null after flushing when null is received
805
+ * - No automatic flushing - filter stays open until EOF or close()
432
806
  *
433
- * @param frames - Async generator of input frames
807
+ * @param frames - Async iterable of frames, single frame, or null to flush
434
808
  *
435
- * @yields {Frame} Filtered frames
809
+ * @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
436
810
  *
437
811
  * @throws {Error} If filter not ready
438
812
  *
@@ -440,8 +814,12 @@ export class FilterAPI {
440
814
  *
441
815
  * @example
442
816
  * ```typescript
443
- * // Filter decoded frames
817
+ * // Stream of frames with automatic EOF propagation
444
818
  * for await (const frame of filter.frames(decoder.frames(packets))) {
819
+ * if (frame === null) {
820
+ * console.log('Filter flushed');
821
+ * break;
822
+ * }
445
823
  * await encoder.encode(frame);
446
824
  * frame.free();
447
825
  * }
@@ -449,64 +827,91 @@ export class FilterAPI {
449
827
  *
450
828
  * @example
451
829
  * ```typescript
452
- * // Chain filters
453
- * const filter1 = FilterAPI.create('scale=640:480', {
454
- * timeBase: video.timeBase
455
- * });
456
- * const filter2 = FilterAPI.create('rotate=PI/4', {
457
- * timeBase: video.timeBase
458
- * });
830
+ * // Single frame - no automatic flush
831
+ * for await (const frame of filter.frames(singleFrame)) {
832
+ * await encoder.encode(frame);
833
+ * frame.free();
834
+ * }
835
+ * // Filter remains open, buffered frames not flushed
836
+ * ```
459
837
  *
460
- * for await (const frame of filter2.frames(filter1.frames(input))) {
461
- * // Process filtered frames
838
+ * @example
839
+ * ```typescript
840
+ * // Explicit flush with EOF
841
+ * for await (const frame of filter.frames(null)) {
842
+ * if (frame === null) {
843
+ * console.log('All buffered frames flushed');
844
+ * break;
845
+ * }
846
+ * console.log('Buffered frame:', frame.pts);
462
847
  * frame.free();
463
848
  * }
464
849
  * ```
465
850
  *
466
851
  * @see {@link process} For single frame processing
467
- * @see {@link flush} For end-of-stream handling
852
+ * @see {@link Decoder.frames} For frames source
853
+ * @see {@link framesSync} For sync version
468
854
  */
469
855
  async *frames(frames) {
470
- for await (const frame of frames) {
856
+ const self = this;
857
+ const processFrame = async function* (frame) {
858
+ await self.process(frame);
859
+ while (true) {
860
+ const filtered = await self.receive();
861
+ if (!filtered)
862
+ break;
863
+ yield filtered;
864
+ }
865
+ }.bind(this);
866
+ const finalize = async function* () {
867
+ for await (const remaining of self.flushFrames()) {
868
+ yield remaining;
869
+ }
870
+ yield null;
871
+ }.bind(this);
872
+ if (frames === null) {
873
+ yield* finalize();
874
+ return;
875
+ }
876
+ if (frames instanceof Frame) {
877
+ yield* processFrame(frames);
878
+ return;
879
+ }
880
+ for await (const frame_1 of frames) {
881
+ const env_1 = { stack: [], error: void 0, hasError: false };
471
882
  try {
472
- // Process input frame
473
- const output = await this.process(frame);
474
- if (output) {
475
- yield output;
476
- }
477
- // Drain any buffered frames
478
- while (!this.isClosed) {
479
- const buffered = await this.receive();
480
- if (!buffered)
481
- break;
482
- yield buffered;
883
+ const frame = __addDisposableResource(env_1, frame_1, false);
884
+ if (frame === null) {
885
+ yield* finalize();
886
+ return;
483
887
  }
888
+ yield* processFrame(frame);
889
+ }
890
+ catch (e_1) {
891
+ env_1.error = e_1;
892
+ env_1.hasError = true;
484
893
  }
485
894
  finally {
486
- // Free the input frame after processing
487
- frame.free();
895
+ __disposeResources(env_1);
488
896
  }
489
897
  }
490
- // Flush and get remaining frames
491
- await this.flush();
492
- while (!this.isClosed) {
493
- const remaining = await this.receive();
494
- if (!remaining)
495
- break;
496
- yield remaining;
497
- }
498
898
  }
499
899
  /**
500
900
  * Process frame stream through filter synchronously.
501
901
  * Synchronous version of frames.
502
902
  *
503
903
  * High-level sync generator for filtering frame streams.
504
- * Automatically handles buffering and flushing.
505
- * Frees input frames after processing.
904
+ * Filter is only flushed when EOF (null) signal is explicitly received.
905
+ * Primary interface for stream-based filtering.
906
+ *
907
+ * **EOF Handling:**
908
+ * - Send null to flush filter and get remaining buffered frames
909
+ * - Generator yields null after flushing when null is received
910
+ * - No automatic flushing - filter stays open until EOF or close()
506
911
  *
507
- * @param frames - Generator of input frames
912
+ * @param frames - Iterable of frames, single frame, or null to flush
508
913
  *
509
- * @yields {Frame} Filtered frames
914
+ * @yields {Frame | null} Filtered frames, followed by null when explicitly flushed
510
915
  *
511
916
  * @throws {Error} If filter not ready
512
917
  *
@@ -514,8 +919,12 @@ export class FilterAPI {
514
919
  *
515
920
  * @example
516
921
  * ```typescript
517
- * // Filter decoded frames
922
+ * // Stream of frames with automatic EOF propagation
518
923
  * for (const frame of filter.framesSync(decoder.framesSync(packets))) {
924
+ * if (frame === null) {
925
+ * console.log('Filter flushed');
926
+ * break;
927
+ * }
519
928
  * encoder.encodeSync(frame);
520
929
  * frame.free();
521
930
  * }
@@ -523,51 +932,83 @@ export class FilterAPI {
523
932
  *
524
933
  * @example
525
934
  * ```typescript
526
- * // Chain filters
527
- * const filter1 = FilterAPI.create('scale=640:480', {
528
- * timeBase: video.timeBase
529
- * });
530
- * const filter2 = FilterAPI.create('rotate=PI/4', {
531
- * timeBase: video.timeBase
532
- * });
935
+ * // Single frame - no automatic flush
936
+ * for (const frame of filter.framesSync(singleFrame)) {
937
+ * encoder.encodeSync(frame);
938
+ * frame.free();
939
+ * }
940
+ * // Filter remains open, buffered frames not flushed
941
+ * ```
533
942
  *
534
- * for (const frame of filter2.framesSync(filter1.framesSync(input))) {
535
- * // Process filtered frames
943
+ * @example
944
+ * ```typescript
945
+ * // Explicit flush with EOF
946
+ * for (const frame of filter.framesSync(null)) {
947
+ * if (frame === null) {
948
+ * console.log('All buffered frames flushed');
949
+ * break;
950
+ * }
951
+ * console.log('Buffered frame:', frame.pts);
536
952
  * frame.free();
537
953
  * }
538
954
  * ```
539
955
  *
956
+ * @see {@link processSync} For single frame processing
957
+ * @see {@link Decoder.framesSync} For frames source
540
958
  * @see {@link frames} For async version
541
959
  */
542
960
  *framesSync(frames) {
543
- for (const frame of frames) {
961
+ const self = this;
962
+ // Helper: Process frame and yield all available filtered frames (filters out EAGAIN nulls)
963
+ const processFrame = function* (frame) {
964
+ self.processSync(frame);
965
+ // Receive ALL filtered frames (filter out null/EAGAIN)
966
+ while (true) {
967
+ const filtered = self.receiveSync();
968
+ if (!filtered)
969
+ break; // EAGAIN or EOF - no more frames available
970
+ yield filtered; // Only yield actual frames, not null
971
+ }
972
+ }.bind(this);
973
+ // Helper: Flush filter and signal EOF
974
+ const finalize = function* () {
975
+ for (const remaining of self.flushFramesSync()) {
976
+ yield remaining; // Only yield actual frames
977
+ }
978
+ yield null; // Signal end-of-stream
979
+ }.bind(this);
980
+ // Case 1: EOF input -> flush only
981
+ if (frames === null) {
982
+ yield* finalize();
983
+ return;
984
+ }
985
+ // Case 2: Single frame
986
+ if (frames instanceof Frame) {
987
+ yield* processFrame(frames);
988
+ // No automatic flush - only flush on explicit EOF
989
+ return;
990
+ }
991
+ // Case 3: Iterable of frames
992
+ for (const frame_2 of frames) {
993
+ const env_2 = { stack: [], error: void 0, hasError: false };
544
994
  try {
545
- // Process input frame
546
- const output = this.processSync(frame);
547
- if (output) {
548
- yield output;
549
- }
550
- // Drain any buffered frames
551
- while (!this.isClosed) {
552
- const buffered = this.receiveSync();
553
- if (!buffered)
554
- break;
555
- yield buffered;
995
+ const frame = __addDisposableResource(env_2, frame_2, false);
996
+ // Check for EOF signal from upstream
997
+ if (frame === null) {
998
+ yield* finalize();
999
+ return;
556
1000
  }
1001
+ yield* processFrame(frame);
1002
+ }
1003
+ catch (e_2) {
1004
+ env_2.error = e_2;
1005
+ env_2.hasError = true;
557
1006
  }
558
1007
  finally {
559
- // Free the input frame after processing
560
- frame.free();
1008
+ __disposeResources(env_2);
561
1009
  }
562
1010
  }
563
- // Flush and get remaining frames
564
- this.flushSync();
565
- while (!this.isClosed) {
566
- const remaining = this.receiveSync();
567
- if (!remaining)
568
- break;
569
- yield remaining;
570
- }
1011
+ // No automatic flush - only flush on explicit EOF
571
1012
  }
572
1013
  /**
573
1014
  * Flush filter and signal end-of-stream.
@@ -591,14 +1032,15 @@ export class FilterAPI {
591
1032
  * ```
592
1033
  *
593
1034
  * @see {@link flushFrames} For async iteration
594
- * @see {@link frames} For complete pipeline
1035
+ * @see {@link receive} For getting flushed frames
1036
+ * @see {@link flushSync} For synchronous version
595
1037
  */
596
1038
  async flush() {
597
1039
  if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
598
1040
  return;
599
1041
  }
600
1042
  // Send flush frame (null)
601
- const ret = await this.buffersrcCtx.buffersrcAddFrame(null);
1043
+ const ret = await this.buffersrcCtx.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
602
1044
  if (ret < 0 && ret !== AVERROR_EOF) {
603
1045
  FFmpegError.throwIfError(ret, 'Failed to flush filter');
604
1046
  }
@@ -625,6 +1067,8 @@ export class FilterAPI {
625
1067
  * }
626
1068
  * ```
627
1069
  *
1070
+ * @see {@link flushFramesSync} For sync iteration
1071
+ * @see {@link receiveSync} For getting flushed frames
628
1072
  * @see {@link flush} For async version
629
1073
  */
630
1074
  flushSync() {
@@ -632,7 +1076,7 @@ export class FilterAPI {
632
1076
  return;
633
1077
  }
634
1078
  // Send flush frame (null)
635
- const ret = this.buffersrcCtx.buffersrcAddFrameSync(null);
1079
+ const ret = this.buffersrcCtx.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
636
1080
  if (ret < 0 && ret !== AVERROR_EOF) {
637
1081
  FFmpegError.throwIfError(ret, 'Failed to flush filter');
638
1082
  }
@@ -656,16 +1100,19 @@ export class FilterAPI {
656
1100
  * }
657
1101
  * ```
658
1102
  *
1103
+ * @see {@link process} For frame processing
659
1104
  * @see {@link flush} For manual flush
660
- * @see {@link frames} For complete pipeline
1105
+ * @see {@link flushFramesSync} For sync version
661
1106
  */
662
1107
  async *flushFrames() {
663
1108
  // Send flush signal
664
1109
  await this.flush();
665
- // Yield all remaining frames
666
- let frame;
667
- while ((frame = await this.receive()) !== null) {
668
- yield frame;
1110
+ // Yield all remaining frames (filter out null/EAGAIN and EOF)
1111
+ while (true) {
1112
+ const frame = await this.receive();
1113
+ if (!frame)
1114
+ break; // Stop on EAGAIN or EOF
1115
+ yield frame; // Only yield actual frames
669
1116
  }
670
1117
  }
671
1118
  /**
@@ -688,54 +1135,95 @@ export class FilterAPI {
688
1135
  * }
689
1136
  * ```
690
1137
  *
1138
+ * @see {@link processSync} For frame processing
1139
+ * @see {@link flushSync} For manual flush
691
1140
  * @see {@link flushFrames} For async version
692
1141
  */
693
1142
  *flushFramesSync() {
694
1143
  // Send flush signal
695
1144
  this.flushSync();
696
- // Yield all remaining frames
697
- let frame;
698
- while ((frame = this.receiveSync()) !== null) {
699
- yield frame;
1145
+ // Yield all remaining frames (filter out null/EAGAIN and EOF)
1146
+ while (true) {
1147
+ const frame = this.receiveSync();
1148
+ if (!frame)
1149
+ break; // Stop on EAGAIN or EOF
1150
+ yield frame; // Only yield actual frames
700
1151
  }
701
1152
  }
702
1153
  /**
703
1154
  * Receive buffered frame from filter.
704
1155
  *
705
1156
  * Drains frames buffered by the filter.
706
- * Call repeatedly until null to get all buffered frames.
707
- * Returns null if filter is closed, not initialized, or no frames available.
1157
+ * Call repeatedly until null or EOF to get all buffered frames.
1158
+ * Implements FFmpeg's send/receive pattern.
1159
+ *
1160
+ * **Return Values:**
1161
+ * - `Frame` - Successfully received frame (AVERROR >= 0)
1162
+ * - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
1163
+ * - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
708
1164
  *
709
1165
  * Direct mapping to av_buffersink_get_frame().
710
1166
  *
711
- * @returns Buffered frame or null if none available
1167
+ * @returns Buffered frame, null if need more data, or undefined if stream ended
712
1168
  *
713
1169
  * @throws {FFmpegError} If receiving fails
714
1170
  *
715
1171
  * @example
716
1172
  * ```typescript
717
- * let frame;
718
- * while ((frame = await filter.receive()) !== null) {
1173
+ * // Process all buffered frames
1174
+ * while (true) {
1175
+ * const frame = await filter.receive();
1176
+ * if (!frame) break; // Stop on EAGAIN or EOF
719
1177
  * console.log(`Received frame: pts=${frame.pts}`);
720
1178
  * frame.free();
721
1179
  * }
722
1180
  * ```
1181
+ *
1182
+ * @example
1183
+ * ```typescript
1184
+ * // Handle each return value explicitly
1185
+ * const frame = await filter.receive();
1186
+ * if (frame === EOF) {
1187
+ * console.log('Filter stream ended');
1188
+ * } else if (frame === null) {
1189
+ * console.log('Need more input data');
1190
+ * } else {
1191
+ * console.log(`Got frame: pts=${frame.pts}`);
1192
+ * frame.free();
1193
+ * }
1194
+ * ```
1195
+ *
1196
+ * @see {@link process} For frame processing
1197
+ * @see {@link flush} For flushing filter
1198
+ * @see {@link receiveSync} For synchronous version
1199
+ * @see {@link EOF} For end-of-stream signal
723
1200
  */
724
1201
  async receive() {
725
- if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
1202
+ if (this.isClosed) {
1203
+ return EOF;
1204
+ }
1205
+ if (!this.initialized || !this.buffersinkCtx) {
726
1206
  return null;
727
1207
  }
728
- const frame = new Frame();
729
- frame.alloc();
730
- const ret = await this.buffersinkCtx.buffersinkGetFrame(frame);
1208
+ // Reuse frame - but alloc() instead of unref() for buffersink
1209
+ // buffersink needs a fresh allocated frame, not an unreferenced one
1210
+ this.frame.alloc();
1211
+ const ret = await this.buffersinkCtx.buffersinkGetFrame(this.frame);
731
1212
  if (ret >= 0) {
732
- return frame;
1213
+ // Post-process output frame (set timeBase from buffersink, calculate duration)
1214
+ this.postProcessOutputFrame(this.frame);
1215
+ // Clone for user (keeps internal frame for reuse)
1216
+ return this.frame.clone();
1217
+ }
1218
+ else if (ret === AVERROR_EAGAIN) {
1219
+ // Need more data
1220
+ return null;
1221
+ }
1222
+ else if (ret === AVERROR_EOF) {
1223
+ // End of stream
1224
+ return EOF;
733
1225
  }
734
1226
  else {
735
- frame.free();
736
- if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
737
- return null;
738
- }
739
1227
  FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
740
1228
  return null;
741
1229
  }
@@ -745,41 +1233,74 @@ export class FilterAPI {
745
1233
  * Synchronous version of receive.
746
1234
  *
747
1235
  * Drains frames buffered by the filter.
748
- * Call repeatedly until null to get all buffered frames.
749
- * Returns null if filter is closed, not initialized, or no frames available.
1236
+ * Call repeatedly until null or EOF to get all buffered frames.
1237
+ * Implements FFmpeg's send/receive pattern.
1238
+ *
1239
+ * **Return Values:**
1240
+ * - `Frame` - Successfully received frame (AVERROR >= 0)
1241
+ * - `null` - Need more input data (AVERROR_EAGAIN), or filter not initialized
1242
+ * - `undefined` - End of stream reached (AVERROR_EOF), or filter is closed
750
1243
  *
751
1244
  * Direct mapping to av_buffersink_get_frame().
752
1245
  *
753
- * @returns Buffered frame or null if none available
1246
+ * @returns Buffered frame, null if need more data, or undefined if stream ended
754
1247
  *
755
1248
  * @throws {FFmpegError} If receiving fails
756
1249
  *
757
1250
  * @example
758
1251
  * ```typescript
759
- * let frame;
760
- * while ((frame = filter.receiveSync()) !== null) {
1252
+ * // Process all buffered frames
1253
+ * while (true) {
1254
+ * const frame = filter.receiveSync();
1255
+ * if (!frame) break; // Stop on EAGAIN or EOF
761
1256
  * console.log(`Received frame: pts=${frame.pts}`);
762
1257
  * frame.free();
763
1258
  * }
764
1259
  * ```
765
1260
  *
1261
+ * @example
1262
+ * ```typescript
1263
+ * // Handle each return value explicitly
1264
+ * const frame = filter.receiveSync();
1265
+ * if (frame === EOF) {
1266
+ * console.log('Filter stream ended');
1267
+ * } else if (frame === null) {
1268
+ * console.log('Need more input data');
1269
+ * } else {
1270
+ * console.log(`Got frame: pts=${frame.pts}`);
1271
+ * frame.free();
1272
+ * }
1273
+ * ```
1274
+ *
1275
+ * @see {@link processSync} For frame processing
1276
+ * @see {@link flushSync} For flushing filter
766
1277
  * @see {@link receive} For async version
1278
+ * @see {@link EOF} For end-of-stream signal
767
1279
  */
768
1280
  receiveSync() {
769
- if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
1281
+ if (this.isClosed) {
1282
+ return EOF;
1283
+ }
1284
+ if (!this.initialized || !this.buffersinkCtx) {
770
1285
  return null;
771
1286
  }
772
- const frame = new Frame();
773
- frame.alloc();
774
- const ret = this.buffersinkCtx.buffersinkGetFrameSync(frame);
1287
+ // Reuse frame - but alloc() instead of unref() for buffersink
1288
+ // buffersink needs a fresh allocated frame, not an unreferenced one
1289
+ this.frame.alloc();
1290
+ const ret = this.buffersinkCtx.buffersinkGetFrameSync(this.frame);
775
1291
  if (ret >= 0) {
776
- return frame;
1292
+ // Post-process output frame (set timeBase from buffersink, calculate duration)
1293
+ this.postProcessOutputFrame(this.frame);
1294
+ // Clone for user (keeps internal frame for reuse)
1295
+ return this.frame.clone();
1296
+ }
1297
+ else if (ret === AVERROR_EAGAIN) {
1298
+ return null; // Need more data
1299
+ }
1300
+ else if (ret === AVERROR_EOF) {
1301
+ return EOF; // End of stream
777
1302
  }
778
1303
  else {
779
- frame.free();
780
- if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
781
- return null;
782
- }
783
1304
  FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
784
1305
  return null;
785
1306
  }
@@ -869,6 +1390,24 @@ export class FilterAPI {
869
1390
  const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
870
1391
  FFmpegError.throwIfError(ret, 'Failed to queue filter command');
871
1392
  }
1393
+ pipeTo(target) {
1394
+ const t = target;
1395
+ // Store reference to next component for flush propagation
1396
+ this.nextComponent = t;
1397
+ // Start worker if not already running
1398
+ this.workerPromise ??= this.runWorker();
1399
+ // Start pipe task: filter.outputQueue -> target.inputQueue (via target.send)
1400
+ this.pipeToPromise = (async () => {
1401
+ while (true) {
1402
+ const frame = await this.receiveFrame();
1403
+ if (!frame)
1404
+ break;
1405
+ await t.sendToQueue(frame);
1406
+ }
1407
+ })();
1408
+ // Return scheduler for chaining (target is now the last component)
1409
+ return new Scheduler(this, t);
1410
+ }
872
1411
  /**
873
1412
  * Free filter resources.
874
1413
  *
@@ -887,10 +1426,123 @@ export class FilterAPI {
887
1426
  return;
888
1427
  }
889
1428
  this.isClosed = true;
1429
+ // Close queues
1430
+ this.inputQueue.close();
1431
+ this.outputQueue.close();
1432
+ this.frame.free();
890
1433
  this.graph.free();
891
1434
  this.buffersrcCtx = null;
892
1435
  this.buffersinkCtx = null;
893
1436
  this.initialized = false;
1437
+ this.initializePromise = null;
1438
+ }
1439
+ /**
1440
+ * Worker loop for push-based processing.
1441
+ *
1442
+ * @internal
1443
+ */
1444
+ async runWorker() {
1445
+ try {
1446
+ // Outer loop - receive frames
1447
+ while (!this.inputQueue.isClosed) {
1448
+ const env_3 = { stack: [], error: void 0, hasError: false };
1449
+ try {
1450
+ const frame = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
1451
+ if (!frame)
1452
+ break;
1453
+ await this.process(frame);
1454
+ // Receive all available frames
1455
+ while (!this.outputQueue.isClosed) {
1456
+ const buffered = await this.receive();
1457
+ if (!buffered)
1458
+ break; // Stop on EAGAIN or EOF
1459
+ await this.outputQueue.send(buffered); // Only send actual frames
1460
+ }
1461
+ }
1462
+ catch (e_3) {
1463
+ env_3.error = e_3;
1464
+ env_3.hasError = true;
1465
+ }
1466
+ finally {
1467
+ __disposeResources(env_3);
1468
+ }
1469
+ }
1470
+ // Flush filter at end
1471
+ await this.flush();
1472
+ while (!this.outputQueue.isClosed) {
1473
+ const frame = await this.receive();
1474
+ if (!frame)
1475
+ break; // Stop on EAGAIN or EOF
1476
+ await this.outputQueue.send(frame); // Only send actual frames
1477
+ }
1478
+ }
1479
+ catch {
1480
+ // Ignore error ?
1481
+ }
1482
+ finally {
1483
+ // Close output queue when done
1484
+ this.outputQueue?.close();
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Send frame to input queue or flush the pipeline.
1489
+ *
1490
+ * When frame is provided, queues it for filtering.
1491
+ * When null is provided, triggers flush sequence:
1492
+ * - Closes input queue
1493
+ * - Waits for worker completion
1494
+ * - Flushes filter and sends remaining frames to output queue
1495
+ * - Closes output queue
1496
+ * - Waits for pipeTo task completion
1497
+ * - Propagates flush to next component (if any)
1498
+ *
1499
+ * Used by scheduler system for pipeline control.
1500
+ *
1501
+ * @param frame - Frame to send, or null to flush
1502
+ *
1503
+ * @internal
1504
+ */
1505
+ async sendToQueue(frame) {
1506
+ if (frame) {
1507
+ await this.inputQueue.send(frame);
1508
+ }
1509
+ else {
1510
+ // Close input queue to signal end of stream to worker
1511
+ this.inputQueue.close();
1512
+ // Wait for worker to finish processing all frames (if exists)
1513
+ if (this.workerPromise) {
1514
+ await this.workerPromise;
1515
+ }
1516
+ // Flush filter at end (like FFmpeg does)
1517
+ await this.flush();
1518
+ // Send all flushed frames to output queue
1519
+ while (true) {
1520
+ const frame = await this.receive();
1521
+ if (!frame)
1522
+ break; // Stop on EAGAIN or EOF
1523
+ await this.outputQueue.send(frame); // Only send actual frames
1524
+ }
1525
+ // Close output queue to signal end of stream to pipeTo() task
1526
+ this.outputQueue.close();
1527
+ // Wait for pipeTo() task to finish processing all frames (if exists)
1528
+ if (this.pipeToPromise) {
1529
+ await this.pipeToPromise;
1530
+ }
1531
+ // Then propagate flush to next component
1532
+ if (this.nextComponent) {
1533
+ await this.nextComponent.sendToQueue(null);
1534
+ }
1535
+ }
1536
+ }
1537
+ /**
1538
+ * Receive frame from output queue.
1539
+ *
1540
+ * @returns Frame from output queue or null if closed
1541
+ *
1542
+ * @internal
1543
+ */
1544
+ async receiveFrame() {
1545
+ return await this.outputQueue.receive();
894
1546
  }
895
1547
  /**
896
1548
  * Initialize filter graph from first frame.
@@ -908,6 +1560,23 @@ export class FilterAPI {
908
1560
  * @internal
909
1561
  */
910
1562
  async initialize(frame) {
1563
+ // Calculate timeBase from first frame
1564
+ this.calculatedTimeBase = this.calculateTimeBase(frame);
1565
+ // Track initial frame properties for change detection
1566
+ this.lastFrameProps = {
1567
+ format: frame.format,
1568
+ width: frame.width,
1569
+ height: frame.height,
1570
+ sampleRate: frame.sampleRate,
1571
+ channels: frame.channelLayout?.nbChannels ?? 0,
1572
+ };
1573
+ // Set graph options before parsing
1574
+ if (this.options.scaleSwsOpts) {
1575
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1576
+ }
1577
+ if (this.options.audioResampleOpts) {
1578
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1579
+ }
911
1580
  // Create buffer source and sink
912
1581
  this.createBufferSource(frame);
913
1582
  this.createBufferSink(frame);
@@ -937,6 +1606,23 @@ export class FilterAPI {
937
1606
  * @see {@link initialize} For async version
938
1607
  */
939
1608
  initializeSync(frame) {
1609
+ // Calculate timeBase from first frame
1610
+ this.calculatedTimeBase = this.calculateTimeBase(frame);
1611
+ // Track initial frame properties for change detection
1612
+ this.lastFrameProps = {
1613
+ format: frame.format,
1614
+ width: frame.width,
1615
+ height: frame.height,
1616
+ sampleRate: frame.sampleRate,
1617
+ channels: frame.channelLayout?.nbChannels ?? 0,
1618
+ };
1619
+ // Set graph options before parsing
1620
+ if (this.options.scaleSwsOpts) {
1621
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1622
+ }
1623
+ if (this.options.audioResampleOpts) {
1624
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1625
+ }
940
1626
  // Create buffer source and sink
941
1627
  this.createBufferSource(frame);
942
1628
  this.createBufferSink(frame);
@@ -947,6 +1633,125 @@ export class FilterAPI {
947
1633
  FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
948
1634
  this.initialized = true;
949
1635
  }
1636
+ /**
1637
+ * Check if frame properties changed and handle according to dropOnChange/allowReinit options.
1638
+ *
1639
+ * Implements FFmpeg's IFILTER_FLAG_DROPCHANGED and IFILTER_FLAG_REINIT logic
1640
+ *
1641
+ * @param frame - Frame to check
1642
+ *
1643
+ * @returns true if frame should be processed, false if frame should be dropped
1644
+ *
1645
+ * @throws {Error} If format changed and allowReinit is false
1646
+ *
1647
+ * @internal
1648
+ */
1649
+ checkFramePropertiesChanged(frame) {
1650
+ if (!this.lastFrameProps) {
1651
+ return true; // No previous frame, allow
1652
+ }
1653
+ // Check for property changes
1654
+ const changed = frame.format !== this.lastFrameProps.format ||
1655
+ frame.width !== this.lastFrameProps.width ||
1656
+ frame.height !== this.lastFrameProps.height ||
1657
+ frame.sampleRate !== this.lastFrameProps.sampleRate ||
1658
+ (frame.channelLayout?.nbChannels ?? 0) !== this.lastFrameProps.channels;
1659
+ if (!changed) {
1660
+ return true; // No changes, process frame
1661
+ }
1662
+ // Properties changed - check dropOnChange flag
1663
+ if (this.options.dropOnChange) {
1664
+ return false; // Drop frame
1665
+ }
1666
+ // Check allowReinit flag
1667
+ // Default is true (allow reinit), only block if explicitly set to false
1668
+ const allowReinit = this.options.allowReinit !== false;
1669
+ if (!allowReinit && this.initialized) {
1670
+ throw new Error('Frame properties changed but allowReinit is false. ' +
1671
+ `Format: ${this.lastFrameProps.format}->${frame.format}, ` +
1672
+ `Size: ${this.lastFrameProps.width}x${this.lastFrameProps.height}->${frame.width}x${frame.height}`);
1673
+ }
1674
+ // Close current graph and reinitialize
1675
+ this.graph.free();
1676
+ // Create new graph
1677
+ this.graph = new FilterGraph();
1678
+ this.graph.alloc();
1679
+ // Configure threading
1680
+ if (this.options.threads !== undefined) {
1681
+ this.graph.nbThreads = this.options.threads;
1682
+ }
1683
+ // Configure scaler options
1684
+ if (this.options.scaleSwsOpts) {
1685
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1686
+ }
1687
+ this.buffersrcCtx = null;
1688
+ this.buffersinkCtx = null;
1689
+ this.initialized = false;
1690
+ this.initializePromise = null;
1691
+ this.calculatedTimeBase = null;
1692
+ return true; // Will be reinitialized on next process
1693
+ }
1694
+ /**
1695
+ * Calculate timeBase from frame based on media type and CFR option.
1696
+ *
1697
+ * Implements FFmpeg's ifilter_parameters_from_frame logic:
1698
+ * - Audio: Always { 1, sample_rate }
1699
+ * - Video CFR: 1/framerate (inverse of framerate)
1700
+ * - Video VFR: Use frame.timeBase
1701
+ *
1702
+ * @param frame - Input frame
1703
+ *
1704
+ * @returns Calculated timeBase
1705
+ *
1706
+ * @internal
1707
+ */
1708
+ calculateTimeBase(frame) {
1709
+ if (frame.isAudio()) {
1710
+ // Audio: Always { 1, sample_rate }
1711
+ return { num: 1, den: frame.sampleRate };
1712
+ }
1713
+ else {
1714
+ // Video: Check CFR flag
1715
+ if (this.options.cfr) {
1716
+ // CFR mode: timeBase = 1/framerate = inverse(framerate)
1717
+ // Note: framerate is guaranteed to be set (validated in create())
1718
+ return avInvQ(this.options.framerate);
1719
+ }
1720
+ else {
1721
+ // VFR mode: Use frame.timeBase
1722
+ return frame.timeBase;
1723
+ }
1724
+ }
1725
+ }
1726
+ /**
1727
+ * Post-process output frame from buffersink.
1728
+ *
1729
+ * Applies FFmpeg's fg_output_step() behavior:
1730
+ * 1. Sets frame.timeBase from buffersink (filters can change timeBase, e.g., aresample)
1731
+ * 2. Calculates video frame duration from frame rate if not set
1732
+ *
1733
+ * This must be called AFTER buffersinkGetFrame() for every output frame.
1734
+ *
1735
+ * @param frame - Output frame from buffersink
1736
+ *
1737
+ * @throws {Error} If buffersink context not available
1738
+ *
1739
+ * @internal
1740
+ */
1741
+ postProcessOutputFrame(frame) {
1742
+ if (!this.buffersinkCtx) {
1743
+ throw new Error('Buffersink context not available');
1744
+ }
1745
+ // Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
1746
+ // Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
1747
+ frame.timeBase = this.buffersinkCtx.buffersinkGetTimeBase();
1748
+ if (frame.isVideo() && !frame.duration) {
1749
+ const frameRate = this.buffersinkCtx.buffersinkGetFrameRate();
1750
+ if (frameRate.num > 0 && frameRate.den > 0) {
1751
+ frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
1752
+ }
1753
+ }
1754
+ }
950
1755
  /**
951
1756
  * Create buffer source with frame parameters.
952
1757
  *
@@ -967,6 +1772,10 @@ export class FilterAPI {
967
1772
  if (!bufferFilter) {
968
1773
  throw new Error(`${filterName} filter not found`);
969
1774
  }
1775
+ // Ensure timeBase was calculated
1776
+ if (!this.calculatedTimeBase) {
1777
+ throw new Error('TimeBase not calculated - this should not happen');
1778
+ }
970
1779
  // For audio, create with args. For video, use allocFilter + buffersrcParametersSet
971
1780
  if (frame.isVideo()) {
972
1781
  // Allocate filter without args
@@ -978,8 +1787,8 @@ export class FilterAPI {
978
1787
  width: frame.width,
979
1788
  height: frame.height,
980
1789
  format: frame.format,
981
- timeBase: this.options.timeBase,
982
- frameRate: this.options.frameRate ?? frame.timeBase,
1790
+ timeBase: this.calculatedTimeBase,
1791
+ frameRate: this.options.framerate,
983
1792
  sampleAspectRatio: frame.sampleAspectRatio,
984
1793
  colorRange: frame.colorRange,
985
1794
  colorSpace: frame.colorSpace,
@@ -995,7 +1804,7 @@ export class FilterAPI {
995
1804
  const formatName = avGetSampleFmtName(frame.format);
996
1805
  const channelLayout = frame.channelLayout.mask === 0n ? 'stereo' : frame.channelLayout.mask.toString();
997
1806
  // eslint-disable-next-line @stylistic/max-len
998
- const args = `time_base=${this.options.timeBase.num}/${this.options.timeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
1807
+ const args = `time_base=${this.calculatedTimeBase.num}/${this.calculatedTimeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
999
1808
  this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
1000
1809
  if (!this.buffersrcCtx) {
1001
1810
  throw new Error('Failed to create audio buffer source');
@@ -1061,8 +1870,8 @@ export class FilterAPI {
1061
1870
  if (filters) {
1062
1871
  for (const filterCtx of filters) {
1063
1872
  const filter = filterCtx.filter;
1064
- if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
1065
- filterCtx.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? this.options.hardware?.deviceContext ?? null;
1873
+ if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
1874
+ filterCtx.hwDeviceCtx = this.options.hardware?.deviceContext ?? frame.hwFramesCtx?.deviceRef ?? null;
1066
1875
  // Set extra_hw_frames if specified
1067
1876
  if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
1068
1877
  filterCtx.extraHWFrames = this.options.extraHWFrames;