node-av 3.1.2 → 4.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 (157) hide show
  1. package/README.md +65 -52
  2. package/binding.gyp +4 -0
  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 +319 -78
  7. package/dist/api/bitstream-filter.js +680 -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 +279 -17
  17. package/dist/api/decoder.js +998 -209
  18. package/dist/api/decoder.js.map +1 -1
  19. package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
  20. package/dist/api/demuxer.js +1968 -0
  21. package/dist/api/demuxer.js.map +1 -0
  22. package/dist/api/encoder.d.ts +308 -50
  23. package/dist/api/encoder.js +1133 -111
  24. package/dist/api/encoder.js.map +1 -1
  25. package/dist/api/filter-presets.d.ts +12 -5
  26. package/dist/api/filter-presets.js +21 -7
  27. package/dist/api/filter-presets.js.map +1 -1
  28. package/dist/api/filter.d.ts +406 -40
  29. package/dist/api/filter.js +966 -139
  30. package/dist/api/filter.js.map +1 -1
  31. package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
  32. package/dist/api/fmp4-stream.js +539 -0
  33. package/dist/api/fmp4-stream.js.map +1 -0
  34. package/dist/api/hardware.d.ts +58 -6
  35. package/dist/api/hardware.js +127 -11
  36. package/dist/api/hardware.js.map +1 -1
  37. package/dist/api/index.d.ts +6 -4
  38. package/dist/api/index.js +14 -8
  39. package/dist/api/index.js.map +1 -1
  40. package/dist/api/io-stream.d.ts +3 -3
  41. package/dist/api/io-stream.js +5 -4
  42. package/dist/api/io-stream.js.map +1 -1
  43. package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
  44. package/dist/api/muxer.js +1934 -0
  45. package/dist/api/muxer.js.map +1 -0
  46. package/dist/api/pipeline.d.ts +77 -29
  47. package/dist/api/pipeline.js +435 -425
  48. package/dist/api/pipeline.js.map +1 -1
  49. package/dist/api/rtp-stream.d.ts +312 -0
  50. package/dist/api/rtp-stream.js +630 -0
  51. package/dist/api/rtp-stream.js.map +1 -0
  52. package/dist/api/types.d.ts +476 -55
  53. package/dist/api/utilities/async-queue.d.ts +91 -0
  54. package/dist/api/utilities/async-queue.js +162 -0
  55. package/dist/api/utilities/async-queue.js.map +1 -0
  56. package/dist/api/utilities/audio-sample.d.ts +1 -1
  57. package/dist/api/utilities/image.d.ts +1 -1
  58. package/dist/api/utilities/index.d.ts +2 -0
  59. package/dist/api/utilities/index.js +4 -0
  60. package/dist/api/utilities/index.js.map +1 -1
  61. package/dist/api/utilities/media-type.d.ts +1 -1
  62. package/dist/api/utilities/pixel-format.d.ts +1 -1
  63. package/dist/api/utilities/sample-format.d.ts +1 -1
  64. package/dist/api/utilities/scheduler.d.ts +169 -0
  65. package/dist/api/utilities/scheduler.js +136 -0
  66. package/dist/api/utilities/scheduler.js.map +1 -0
  67. package/dist/api/utilities/streaming.d.ts +74 -15
  68. package/dist/api/utilities/streaming.js +170 -12
  69. package/dist/api/utilities/streaming.js.map +1 -1
  70. package/dist/api/utilities/timestamp.d.ts +1 -1
  71. package/dist/api/webrtc-stream.d.ts +288 -0
  72. package/dist/api/webrtc-stream.js +440 -0
  73. package/dist/api/webrtc-stream.js.map +1 -0
  74. package/dist/constants/constants.d.ts +51 -1
  75. package/dist/constants/constants.js +47 -1
  76. package/dist/constants/constants.js.map +1 -1
  77. package/dist/constants/encoders.d.ts +2 -1
  78. package/dist/constants/encoders.js +4 -3
  79. package/dist/constants/encoders.js.map +1 -1
  80. package/dist/constants/hardware.d.ts +26 -0
  81. package/dist/constants/hardware.js +27 -0
  82. package/dist/constants/hardware.js.map +1 -0
  83. package/dist/constants/index.d.ts +1 -0
  84. package/dist/constants/index.js +1 -0
  85. package/dist/constants/index.js.map +1 -1
  86. package/dist/lib/binding.d.ts +19 -8
  87. package/dist/lib/binding.js.map +1 -1
  88. package/dist/lib/codec-context.d.ts +87 -0
  89. package/dist/lib/codec-context.js +125 -4
  90. package/dist/lib/codec-context.js.map +1 -1
  91. package/dist/lib/codec-parameters.d.ts +183 -1
  92. package/dist/lib/codec-parameters.js +209 -0
  93. package/dist/lib/codec-parameters.js.map +1 -1
  94. package/dist/lib/codec-parser.d.ts +23 -0
  95. package/dist/lib/codec-parser.js +25 -0
  96. package/dist/lib/codec-parser.js.map +1 -1
  97. package/dist/lib/codec.d.ts +26 -4
  98. package/dist/lib/codec.js +35 -0
  99. package/dist/lib/codec.js.map +1 -1
  100. package/dist/lib/dictionary.js +1 -0
  101. package/dist/lib/dictionary.js.map +1 -1
  102. package/dist/lib/error.js +1 -1
  103. package/dist/lib/error.js.map +1 -1
  104. package/dist/lib/filter-context.d.ts +52 -11
  105. package/dist/lib/filter-context.js +56 -12
  106. package/dist/lib/filter-context.js.map +1 -1
  107. package/dist/lib/filter-graph.d.ts +9 -0
  108. package/dist/lib/filter-graph.js +13 -0
  109. package/dist/lib/filter-graph.js.map +1 -1
  110. package/dist/lib/filter.d.ts +21 -0
  111. package/dist/lib/filter.js +28 -0
  112. package/dist/lib/filter.js.map +1 -1
  113. package/dist/lib/format-context.d.ts +48 -14
  114. package/dist/lib/format-context.js +76 -7
  115. package/dist/lib/format-context.js.map +1 -1
  116. package/dist/lib/frame.d.ts +168 -0
  117. package/dist/lib/frame.js +212 -0
  118. package/dist/lib/frame.js.map +1 -1
  119. package/dist/lib/hardware-device-context.d.ts +3 -2
  120. package/dist/lib/hardware-device-context.js.map +1 -1
  121. package/dist/lib/index.d.ts +1 -0
  122. package/dist/lib/index.js +2 -0
  123. package/dist/lib/index.js.map +1 -1
  124. package/dist/lib/input-format.d.ts +21 -0
  125. package/dist/lib/input-format.js +42 -2
  126. package/dist/lib/input-format.js.map +1 -1
  127. package/dist/lib/native-types.d.ts +48 -26
  128. package/dist/lib/option.d.ts +25 -13
  129. package/dist/lib/option.js +28 -0
  130. package/dist/lib/option.js.map +1 -1
  131. package/dist/lib/output-format.d.ts +22 -1
  132. package/dist/lib/output-format.js +28 -0
  133. package/dist/lib/output-format.js.map +1 -1
  134. package/dist/lib/packet.d.ts +35 -0
  135. package/dist/lib/packet.js +52 -2
  136. package/dist/lib/packet.js.map +1 -1
  137. package/dist/lib/stream.d.ts +126 -0
  138. package/dist/lib/stream.js +188 -5
  139. package/dist/lib/stream.js.map +1 -1
  140. package/dist/lib/sync-queue.d.ts +179 -0
  141. package/dist/lib/sync-queue.js +197 -0
  142. package/dist/lib/sync-queue.js.map +1 -0
  143. package/dist/lib/types.d.ts +27 -1
  144. package/dist/lib/utilities.d.ts +281 -53
  145. package/dist/lib/utilities.js +298 -55
  146. package/dist/lib/utilities.js.map +1 -1
  147. package/install/check.js +18 -7
  148. package/package.json +20 -19
  149. package/dist/api/fmp4.js +0 -710
  150. package/dist/api/fmp4.js.map +0 -1
  151. package/dist/api/media-input.js +0 -1075
  152. package/dist/api/media-input.js.map +0 -1
  153. package/dist/api/media-output.js +0 -1040
  154. package/dist/api/media-output.js.map +0 -1
  155. package/dist/api/webrtc.d.ts +0 -664
  156. package/dist/api/webrtc.js +0 -1132
  157. 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 } 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,58 @@ 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
141
  * Creates and allocates filter graph immediately.
67
142
  * Filter configuration is completed on first frame with frame properties.
143
+ * TimeBase is automatically calculated from first frame based on CFR option.
68
144
  * Hardware frames context is automatically detected from input frames.
69
145
  *
70
146
  * Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
71
147
  *
72
148
  * @param description - Filter graph description
73
149
  *
74
- * @param options - Filter options including required timeBase
150
+ * @param options - Filter options
75
151
  *
76
152
  * @returns Configured filter instance
77
153
  *
154
+ * @throws {Error} If cfr=true but framerate is not set
155
+ *
78
156
  * @example
79
157
  * ```typescript
80
- * // Simple video filter
81
- * const filter = FilterAPI.create('scale=640:480', {
82
- * timeBase: video.timeBase
83
- * });
158
+ * // Simple video filter (VFR mode, auto timeBase)
159
+ * const filter = FilterAPI.create('scale=640:480');
84
160
  * ```
85
161
  *
86
162
  * @example
87
163
  * ```typescript
88
- * // Complex filter chain
89
- * const filter = FilterAPI.create('crop=640:480:0:0,rotate=PI/4', {
90
- * timeBase: video.timeBase
164
+ * // CFR mode with constant framerate
165
+ * const filter = FilterAPI.create('scale=1920:1080', {
166
+ * cfr: true,
167
+ * framerate: { num: 25, den: 1 }
91
168
  * });
92
169
  * ```
93
170
  *
94
171
  * @example
95
172
  * ```typescript
96
- * // Audio filter
97
- * const filter = FilterAPI.create('volume=0.5,aecho=0.8:0.9:1000:0.3', {
98
- * timeBase: audio.timeBase
173
+ * // Audio filter with resampling
174
+ * const filter = FilterAPI.create('aformat=sample_fmts=s16:sample_rates=44100', {
175
+ * audioResampleOpts: 'async=1'
99
176
  * });
100
177
  * ```
101
178
  *
102
179
  * @see {@link process} For frame processing
103
180
  * @see {@link FilterOptions} For configuration options
104
181
  */
105
- static create(description, options) {
182
+ static create(description, options = {}) {
183
+ // Validate options: CFR requires framerate
184
+ if (options.cfr && !options.framerate) {
185
+ throw new Error('cfr=true requires framerate to be set');
186
+ }
106
187
  // Create graph
107
188
  const graph = new FilterGraph();
108
189
  graph.alloc();
@@ -147,6 +228,261 @@ export class FilterAPI {
147
228
  get isFilterInitialized() {
148
229
  return this.initialized;
149
230
  }
231
+ /**
232
+ * Get buffersink filter context.
233
+ *
234
+ * Provides access to the buffersink filter context for advanced operations.
235
+ * Returns null if filter is not initialized.
236
+ *
237
+ * @returns Buffersink context or null
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * const sink = filter.buffersink;
242
+ * if (sink) {
243
+ * const fr = sink.buffersinkGetFrameRate();
244
+ * console.log(`Output frame rate: ${fr.num}/${fr.den}`);
245
+ * }
246
+ * ```
247
+ */
248
+ get buffersink() {
249
+ return this.buffersinkCtx;
250
+ }
251
+ /**
252
+ * Output frame rate from filter graph.
253
+ *
254
+ * Returns the frame rate determined by the filter graph output.
255
+ * Matches FFmpeg CLI's av_buffersink_get_frame_rate() behavior.
256
+ * Returns null if filter is not initialized or frame rate is not set.
257
+ *
258
+ * Direct mapping to av_buffersink_get_frame_rate().
259
+ *
260
+ * @returns Frame rate or null if not available
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const frameRate = filter.frameRate;
265
+ * if (frameRate) {
266
+ * console.log(`Filter output: ${frameRate.num}/${frameRate.den} fps`);
267
+ * }
268
+ * ```
269
+ *
270
+ * @see {@link timeBase} For output timebase
271
+ */
272
+ get frameRate() {
273
+ if (!this.initialized || !this.buffersinkCtx) {
274
+ return null;
275
+ }
276
+ const fr = this.buffersinkCtx.buffersinkGetFrameRate();
277
+ // Return null if frame rate is not set (0/0 or 0/1)
278
+ if (fr.num <= 0 || fr.den <= 0) {
279
+ return null;
280
+ }
281
+ return fr;
282
+ }
283
+ /**
284
+ * Output time base from filter graph.
285
+ *
286
+ * Returns the time base of the buffersink output.
287
+ * Matches FFmpeg CLI's av_buffersink_get_time_base() behavior.
288
+ *
289
+ * Direct mapping to av_buffersink_get_time_base().
290
+ *
291
+ * @returns Time base or null if not initialized
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const timeBase = filter.timeBase;
296
+ * if (timeBase) {
297
+ * console.log(`Filter timebase: ${timeBase.num}/${timeBase.den}`);
298
+ * }
299
+ * ```
300
+ *
301
+ * @see {@link frameRate} For output frame rate
302
+ */
303
+ get timeBase() {
304
+ if (!this.initialized || !this.buffersinkCtx) {
305
+ return null;
306
+ }
307
+ return this.buffersinkCtx.buffersinkGetTimeBase();
308
+ }
309
+ /**
310
+ * Output format from filter graph.
311
+ *
312
+ * Returns the pixel format (video) or sample format (audio) of the buffersink output.
313
+ * Matches FFmpeg CLI's av_buffersink_get_format() behavior.
314
+ *
315
+ * Direct mapping to av_buffersink_get_format().
316
+ *
317
+ * @returns Pixel format or sample format, or null if not initialized
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * const format = filter.format;
322
+ * if (format !== null) {
323
+ * console.log(`Filter output format: ${format}`);
324
+ * }
325
+ * ```
326
+ */
327
+ get format() {
328
+ if (!this.initialized || !this.buffersinkCtx) {
329
+ return null;
330
+ }
331
+ return this.buffersinkCtx.buffersinkGetFormat();
332
+ }
333
+ /**
334
+ * Output dimensions from filter graph (video only).
335
+ *
336
+ * Returns the width and height of the buffersink output.
337
+ * Matches FFmpeg CLI's av_buffersink_get_w() and av_buffersink_get_h() behavior.
338
+ * Only meaningful for video filters.
339
+ *
340
+ * Direct mapping to av_buffersink_get_w() and av_buffersink_get_h().
341
+ *
342
+ * @returns Dimensions object or null if not initialized
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const dims = filter.dimensions;
347
+ * if (dims) {
348
+ * console.log(`Filter output: ${dims.width}x${dims.height}`);
349
+ * }
350
+ * ```
351
+ */
352
+ get dimensions() {
353
+ if (!this.initialized || !this.buffersinkCtx) {
354
+ return null;
355
+ }
356
+ return {
357
+ width: this.buffersinkCtx.buffersinkGetWidth(),
358
+ height: this.buffersinkCtx.buffersinkGetHeight(),
359
+ };
360
+ }
361
+ /**
362
+ * Output sample rate from filter graph (audio only).
363
+ *
364
+ * Returns the sample rate of the buffersink output.
365
+ * Matches FFmpeg CLI's av_buffersink_get_sample_rate() behavior.
366
+ * Only meaningful for audio filters.
367
+ *
368
+ * Direct mapping to av_buffersink_get_sample_rate().
369
+ *
370
+ * @returns Sample rate or null if not initialized
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * const sampleRate = filter.sampleRate;
375
+ * if (sampleRate) {
376
+ * console.log(`Filter output sample rate: ${sampleRate} Hz`);
377
+ * }
378
+ * ```
379
+ */
380
+ get sampleRate() {
381
+ if (!this.initialized || !this.buffersinkCtx) {
382
+ return null;
383
+ }
384
+ return this.buffersinkCtx.buffersinkGetSampleRate();
385
+ }
386
+ /**
387
+ * Output channel layout from filter graph (audio only).
388
+ *
389
+ * Returns the channel layout of the buffersink output.
390
+ * Matches FFmpeg CLI's av_buffersink_get_ch_layout() behavior.
391
+ * Only meaningful for audio filters.
392
+ *
393
+ * Direct mapping to av_buffersink_get_ch_layout().
394
+ *
395
+ * @returns Channel layout or null if not initialized
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const layout = filter.channelLayout;
400
+ * if (layout) {
401
+ * console.log(`Filter output channels: ${layout.nbChannels}`);
402
+ * }
403
+ * ```
404
+ */
405
+ get channelLayout() {
406
+ if (!this.initialized || !this.buffersinkCtx) {
407
+ return null;
408
+ }
409
+ return this.buffersinkCtx.buffersinkGetChannelLayout();
410
+ }
411
+ /**
412
+ * Output color space from filter graph (video only).
413
+ *
414
+ * Returns the color space of the buffersink output.
415
+ * Matches FFmpeg CLI's av_buffersink_get_colorspace() behavior.
416
+ * Only meaningful for video filters.
417
+ *
418
+ * Direct mapping to av_buffersink_get_colorspace().
419
+ *
420
+ * @returns Color space or null if not initialized
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * const colorSpace = filter.colorSpace;
425
+ * if (colorSpace !== null) {
426
+ * console.log(`Filter output color space: ${colorSpace}`);
427
+ * }
428
+ * ```
429
+ */
430
+ get colorSpace() {
431
+ if (!this.initialized || !this.buffersinkCtx) {
432
+ return null;
433
+ }
434
+ return this.buffersinkCtx.buffersinkGetColorspace();
435
+ }
436
+ /**
437
+ * Output color range from filter graph (video only).
438
+ *
439
+ * Returns the color range of the buffersink output.
440
+ * Matches FFmpeg CLI's av_buffersink_get_color_range() behavior.
441
+ * Only meaningful for video filters.
442
+ *
443
+ * Direct mapping to av_buffersink_get_color_range().
444
+ *
445
+ * @returns Color range or null if not initialized
446
+ *
447
+ * @example
448
+ * ```typescript
449
+ * const colorRange = filter.colorRange;
450
+ * if (colorRange !== null) {
451
+ * console.log(`Filter output color range: ${colorRange}`);
452
+ * }
453
+ * ```
454
+ */
455
+ get colorRange() {
456
+ if (!this.initialized || !this.buffersinkCtx) {
457
+ return null;
458
+ }
459
+ return this.buffersinkCtx.buffersinkGetColorRange();
460
+ }
461
+ /**
462
+ * Output sample aspect ratio from filter graph (video only).
463
+ *
464
+ * Returns the sample aspect ratio of the buffersink output.
465
+ * Matches FFmpeg CLI's av_buffersink_get_sample_aspect_ratio() behavior.
466
+ * Only meaningful for video filters.
467
+ *
468
+ * Direct mapping to av_buffersink_get_sample_aspect_ratio().
469
+ *
470
+ * @returns Sample aspect ratio or null if not initialized
471
+ *
472
+ * @example
473
+ * ```typescript
474
+ * const sar = filter.sampleAspectRatio;
475
+ * if (sar) {
476
+ * console.log(`Filter output SAR: ${sar.num}:${sar.den}`);
477
+ * }
478
+ * ```
479
+ */
480
+ get sampleAspectRatio() {
481
+ if (!this.initialized || !this.buffersinkCtx) {
482
+ return null;
483
+ }
484
+ return this.buffersinkCtx.buffersinkGetSampleAspectRatio();
485
+ }
150
486
  /**
151
487
  * Check if filter is ready for processing.
152
488
  *
@@ -190,6 +526,10 @@ export class FilterAPI {
190
526
  * Hardware frames context is automatically detected from frame.
191
527
  * Returns null if filter is closed and frame is null.
192
528
  *
529
+ * **Note**: This method receives only ONE frame per call.
530
+ * A single input frame can produce multiple output frames (e.g., fps filter, deinterlace).
531
+ * To receive all frames from an input, use {@link processAll} or {@link frames} instead.
532
+ *
193
533
  * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
194
534
  *
195
535
  * @param frame - Input frame to process (or null to flush)
@@ -220,8 +560,10 @@ export class FilterAPI {
220
560
  * // For buffered frames, use the frames() async generator
221
561
  * ```
222
562
  *
563
+ * @see {@link processAll} For processing multiple output frames
223
564
  * @see {@link frames} For processing frame streams
224
565
  * @see {@link flush} For end-of-stream handling
566
+ * @see {@link processSync} For synchronous version
225
567
  */
226
568
  async process(frame) {
227
569
  if (this.isClosed) {
@@ -232,34 +574,40 @@ export class FilterAPI {
232
574
  if (!frame) {
233
575
  return null;
234
576
  }
235
- await this.initialize(frame);
577
+ this.initializePromise ??= this.initialize(frame);
236
578
  }
579
+ await this.initializePromise;
237
580
  if (!this.initialized) {
238
581
  return null;
239
582
  }
240
583
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
241
584
  throw new Error('Could not initialize filter contexts');
242
585
  }
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();
586
+ // Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
587
+ if (frame && !this.checkFramePropertiesChanged(frame)) {
588
+ // Frame dropped due to property change
256
589
  return null;
257
590
  }
258
- else {
259
- outputFrame.free();
260
- FFmpegError.throwIfError(getRet, 'Failed to get frame from filter');
261
- return null;
591
+ // If reinitialized, reinitialize now
592
+ if (!this.initialized && frame) {
593
+ this.initializePromise = this.initialize(frame);
594
+ await this.initializePromise;
595
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
596
+ throw new Error('Could not reinitialize filter contexts');
597
+ }
262
598
  }
599
+ // Rescale timestamps to filter's timeBase
600
+ if (frame && this.calculatedTimeBase) {
601
+ const originalTimeBase = frame.timeBase;
602
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
603
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
604
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
605
+ }
606
+ // Send frame to filter with PUSH flag for immediate processing
607
+ const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
608
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
609
+ // Try to get filtered frame using receive()
610
+ return await this.receive();
263
611
  }
264
612
  /**
265
613
  * Process a frame through the filter synchronously.
@@ -271,6 +619,10 @@ export class FilterAPI {
271
619
  * Hardware frames context is automatically detected from frame.
272
620
  * Returns null if filter is closed and frame is null.
273
621
  *
622
+ * **Note**: This method receives only ONE frame per call.
623
+ * A single input frame can produce multiple output frames (e.g., fps filter, deinterlace).
624
+ * To receive all frames from an input, use {@link processAllSync} or {@link framesSync} instead.
625
+ *
274
626
  * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
275
627
  *
276
628
  * @param frame - Input frame to process (or null to flush)
@@ -301,6 +653,9 @@ export class FilterAPI {
301
653
  * // For buffered frames, use the framesSync() generator
302
654
  * ```
303
655
  *
656
+ * @see {@link processAllSync} For processing multiple output frames
657
+ * @see {@link framesSync} For processing frame streams
658
+ * @see {@link flushSync} For end-of-stream handling
304
659
  * @see {@link process} For async version
305
660
  */
306
661
  processSync(frame) {
@@ -314,114 +669,193 @@ export class FilterAPI {
314
669
  }
315
670
  this.initializeSync(frame);
316
671
  }
672
+ if (!this.initialized) {
673
+ return null;
674
+ }
317
675
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
318
676
  throw new Error('Could not initialize filter contexts');
319
677
  }
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();
678
+ // Check for frame property changes (FFmpeg: dropOnChange/allowReinit logic)
679
+ if (frame && !this.checkFramePropertiesChanged(frame)) {
680
+ // Frame dropped due to property change
333
681
  return null;
334
682
  }
335
- else {
336
- outputFrame.free();
337
- FFmpegError.throwIfError(getRet, 'Failed to get frame from filter');
338
- return null;
683
+ // If reinitialized, reinitialize now
684
+ if (!this.initialized && frame) {
685
+ this.initializeSync(frame);
686
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
687
+ throw new Error('Could not reinitialize filter contexts');
688
+ }
339
689
  }
690
+ // Rescale timestamps to filter's timeBase
691
+ if (frame && this.calculatedTimeBase) {
692
+ const originalTimeBase = frame.timeBase;
693
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
694
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
695
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
696
+ }
697
+ // Send frame to filter with PUSH flag for immediate processing
698
+ const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
699
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
700
+ // Try to get filtered frame using receiveSync()
701
+ return this.receiveSync();
340
702
  }
341
703
  /**
342
- * Process multiple frames at once.
704
+ * Process a frame through the filter.
343
705
  *
344
- * Processes batch of frames and drains all output.
345
- * Useful for filters that buffer multiple frames.
706
+ * Applies filter operations to input frame and receives all available output frames.
707
+ * Returns array of frames - may be empty if filter needs more input.
708
+ * On first frame, automatically builds filter graph with frame properties.
709
+ * One input frame can produce zero, one, or multiple output frames depending on filter.
710
+ * Hardware frames context is automatically detected from frame.
711
+ *
712
+ * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
346
713
  *
347
- * @param frames - Array of input frames
714
+ * @param frame - Input frame to process (or null to flush)
348
715
  *
349
- * @returns Array of all output frames (empty if filter closed)
716
+ * @returns Array of filtered frames (empty if buffered or filter closed)
350
717
  *
351
- * @throws {Error} If filter not ready
718
+ * @throws {Error} If filter could not be initialized
352
719
  *
353
720
  * @throws {FFmpegError} If processing fails
354
721
  *
355
722
  * @example
356
723
  * ```typescript
357
- * const outputs = await filter.processMultiple([frame1, frame2, frame3]);
358
- * for (const output of outputs) {
359
- * console.log(`Output frame: pts=${output.pts}`);
724
+ * const frames = await filter.processAll(inputFrame);
725
+ * for (const output of frames) {
726
+ * console.log(`Got filtered frame: pts=${output.pts}`);
360
727
  * output.free();
361
728
  * }
362
729
  * ```
363
730
  *
731
+ * @example
732
+ * ```typescript
733
+ * // Process frame - may return multiple frames (e.g. fps filter)
734
+ * const frames = await filter.processAll(frame);
735
+ * for (const output of frames) {
736
+ * yield output;
737
+ * }
738
+ * ```
739
+ *
364
740
  * @see {@link process} For single frame processing
741
+ * @see {@link frames} For processing frame streams
742
+ * @see {@link flush} For end-of-stream handling
743
+ * @see {@link processAllSync} For synchronous version
365
744
  */
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);
745
+ async processAll(frame) {
746
+ if (this.isClosed) {
747
+ return [];
748
+ }
749
+ // Open filter if not already done
750
+ if (!this.initialized) {
751
+ if (!frame) {
752
+ return [];
379
753
  }
754
+ this.initializePromise ??= this.initialize(frame);
755
+ }
756
+ await this.initializePromise;
757
+ if (!this.initialized) {
758
+ return [];
759
+ }
760
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
761
+ throw new Error('Could not initialize filter contexts');
762
+ }
763
+ // Rescale timestamps to filter's timeBase
764
+ if (frame && this.calculatedTimeBase) {
765
+ const originalTimeBase = frame.timeBase;
766
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
767
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
768
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
769
+ }
770
+ // Send frame to filter with PUSH flag for immediate processing
771
+ const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
772
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
773
+ // Receive all available frames using receive()
774
+ const frames = [];
775
+ while (true) {
776
+ const outputFrame = await this.receive();
777
+ if (!outputFrame)
778
+ break;
779
+ frames.push(outputFrame);
380
780
  }
381
- return outputFrames;
781
+ return frames;
382
782
  }
383
783
  /**
384
- * Process multiple frames at once synchronously.
385
- * Synchronous version of processMultiple.
784
+ * Process a frame through the filter synchronously.
785
+ * Synchronous version of processAll.
386
786
  *
387
- * Processes batch of frames and drains all output.
388
- * Useful for filters that buffer multiple frames.
787
+ * Applies filter operations to input frame and receives all available output frames.
788
+ * Returns array of frames - may be empty if filter needs more input.
789
+ * On first frame, automatically builds filter graph with frame properties.
790
+ * One input frame can produce zero, one, or multiple output frames depending on filter.
791
+ * Hardware frames context is automatically detected from frame.
389
792
  *
390
- * @param frames - Array of input frames
793
+ * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
391
794
  *
392
- * @returns Array of all output frames (empty if filter closed)
795
+ * @param frame - Input frame to process (or null to flush)
393
796
  *
394
- * @throws {Error} If filter not ready
797
+ * @returns Array of filtered frames (empty if buffered or filter closed)
798
+ *
799
+ * @throws {Error} If filter could not be initialized
395
800
  *
396
801
  * @throws {FFmpegError} If processing fails
397
802
  *
398
803
  * @example
399
804
  * ```typescript
400
- * const outputs = filter.processMultipleSync([frame1, frame2, frame3]);
805
+ * const outputs = filter.processAllSync(inputFrame);
401
806
  * for (const output of outputs) {
402
- * console.log(`Output frame: pts=${output.pts}`);
807
+ * console.log(`Got filtered frame: pts=${output.pts}`);
403
808
  * output.free();
404
809
  * }
405
810
  * ```
406
811
  *
407
- * @see {@link processMultiple} For async version
812
+ * @example
813
+ * ```typescript
814
+ * // Process frame - may return multiple frames (e.g. fps filter)
815
+ * const outputs = filter.processAllSync(frame);
816
+ * for (const output of outputs) {
817
+ * yield output;
818
+ * }
819
+ * ```
820
+ *
821
+ * @see {@link processSync} For single frame processing
822
+ * @see {@link framesSync} For processing frame streams
823
+ * @see {@link flushSync} For end-of-stream handling
824
+ * @see {@link process} For async version
408
825
  */
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);
826
+ processAllSync(frame) {
827
+ if (this.isClosed) {
828
+ return [];
829
+ }
830
+ // Open filter if not already done
831
+ if (!this.initialized) {
832
+ if (!frame) {
833
+ return [];
422
834
  }
835
+ this.initializeSync(frame);
836
+ }
837
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
838
+ throw new Error('Could not initialize filter contexts');
839
+ }
840
+ // Rescale timestamps to filter's timeBase
841
+ if (frame && this.calculatedTimeBase) {
842
+ const originalTimeBase = frame.timeBase;
843
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
844
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
845
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
846
+ }
847
+ // Send frame to filter with PUSH flag for immediate processing
848
+ const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
849
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
850
+ // Receive all available frames using receiveSync()
851
+ const frames = [];
852
+ while (true) {
853
+ const outputFrame = this.receiveSync();
854
+ if (!outputFrame)
855
+ break;
856
+ frames.push(outputFrame);
423
857
  }
424
- return outputFrames;
858
+ return frames;
425
859
  }
426
860
  /**
427
861
  * Process frame stream through filter.
@@ -464,37 +898,78 @@ export class FilterAPI {
464
898
  * ```
465
899
  *
466
900
  * @see {@link process} For single frame processing
467
- * @see {@link flush} For end-of-stream handling
901
+ * @see {@link Decoder.frames} For frames source
902
+ * @see {@link framesSync} For sync version
468
903
  */
469
904
  async *frames(frames) {
470
- for await (const frame of frames) {
905
+ for await (const frame_1 of frames) {
906
+ const env_1 = { stack: [], error: void 0, hasError: false };
471
907
  try {
472
- // Process input frame
473
- const output = await this.process(frame);
474
- if (output) {
475
- yield output;
908
+ const frame = __addDisposableResource(env_1, frame_1, false);
909
+ // Handle EOF signal
910
+ if (frame === null) {
911
+ // Flush filter
912
+ await this.flush();
913
+ while (true) {
914
+ const remaining = await this.receive();
915
+ if (!remaining)
916
+ break;
917
+ yield remaining;
918
+ }
919
+ // Signal EOF and stop processing
920
+ yield null;
921
+ return;
922
+ }
923
+ if (this.isClosed) {
924
+ break;
925
+ }
926
+ // Open filter if not already done
927
+ if (!this.initialized) {
928
+ this.initializePromise ??= this.initialize(frame);
476
929
  }
477
- // Drain any buffered frames
478
- while (!this.isClosed) {
930
+ await this.initializePromise;
931
+ if (!this.initialized) {
932
+ continue;
933
+ }
934
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
935
+ throw new Error('Could not initialize filter contexts');
936
+ }
937
+ // Rescale timestamps to filter's timeBase
938
+ if (this.calculatedTimeBase) {
939
+ const originalTimeBase = frame.timeBase;
940
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
941
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
942
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
943
+ }
944
+ // Send frame to filter
945
+ const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
946
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
947
+ // Receive all available frames
948
+ while (true) {
479
949
  const buffered = await this.receive();
480
950
  if (!buffered)
481
951
  break;
482
952
  yield buffered;
483
953
  }
484
954
  }
955
+ catch (e_1) {
956
+ env_1.error = e_1;
957
+ env_1.hasError = true;
958
+ }
485
959
  finally {
486
- // Free the input frame after processing
487
- frame.free();
960
+ __disposeResources(env_1);
488
961
  }
489
962
  }
490
- // Flush and get remaining frames
963
+ // Flush and get remaining frames (fallback if no null was sent)
491
964
  await this.flush();
492
- while (!this.isClosed) {
965
+ while (true) {
493
966
  const remaining = await this.receive();
494
967
  if (!remaining)
495
968
  break;
496
969
  yield remaining;
497
970
  }
971
+ // Signal EOF
972
+ yield null;
498
973
  }
499
974
  /**
500
975
  * Process frame stream through filter synchronously.
@@ -537,37 +1012,78 @@ export class FilterAPI {
537
1012
  * }
538
1013
  * ```
539
1014
  *
1015
+ * @see {@link processSync} For single frame processing
1016
+ * @see {@link Decoder.framesSync} For frames source
540
1017
  * @see {@link frames} For async version
541
1018
  */
542
1019
  *framesSync(frames) {
543
- for (const frame of frames) {
1020
+ for (const frame_2 of frames) {
1021
+ const env_2 = { stack: [], error: void 0, hasError: false };
544
1022
  try {
545
- // Process input frame
546
- const output = this.processSync(frame);
547
- if (output) {
548
- yield output;
1023
+ const frame = __addDisposableResource(env_2, frame_2, false);
1024
+ // Handle EOF signal
1025
+ if (frame === null) {
1026
+ // Flush filter
1027
+ this.flushSync();
1028
+ while (true) {
1029
+ const remaining = this.receiveSync();
1030
+ if (!remaining)
1031
+ break;
1032
+ yield remaining;
1033
+ }
1034
+ // Signal EOF and stop processing
1035
+ yield null;
1036
+ return;
549
1037
  }
550
- // Drain any buffered frames
551
- while (!this.isClosed) {
1038
+ if (this.isClosed) {
1039
+ break;
1040
+ }
1041
+ // Open filter if not already done
1042
+ if (!this.initialized) {
1043
+ this.initializeSync(frame);
1044
+ }
1045
+ if (!this.initialized) {
1046
+ continue;
1047
+ }
1048
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
1049
+ throw new Error('Could not initialize filter contexts');
1050
+ }
1051
+ // Rescale timestamps to filter's timeBase
1052
+ if (this.calculatedTimeBase) {
1053
+ const originalTimeBase = frame.timeBase;
1054
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
1055
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
1056
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
1057
+ }
1058
+ // Send frame to filter
1059
+ const addRet = this.buffersrcCtx.buffersrcAddFrameSync(frame, AV_BUFFERSRC_FLAG_PUSH);
1060
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
1061
+ // Receive all available frames
1062
+ while (true) {
552
1063
  const buffered = this.receiveSync();
553
1064
  if (!buffered)
554
1065
  break;
555
1066
  yield buffered;
556
1067
  }
557
1068
  }
1069
+ catch (e_2) {
1070
+ env_2.error = e_2;
1071
+ env_2.hasError = true;
1072
+ }
558
1073
  finally {
559
- // Free the input frame after processing
560
- frame.free();
1074
+ __disposeResources(env_2);
561
1075
  }
562
1076
  }
563
- // Flush and get remaining frames
1077
+ // Flush and get remaining frames (fallback if no null was sent)
564
1078
  this.flushSync();
565
- while (!this.isClosed) {
1079
+ while (true) {
566
1080
  const remaining = this.receiveSync();
567
1081
  if (!remaining)
568
1082
  break;
569
1083
  yield remaining;
570
1084
  }
1085
+ // Signal EOF
1086
+ yield null;
571
1087
  }
572
1088
  /**
573
1089
  * Flush filter and signal end-of-stream.
@@ -591,14 +1107,15 @@ export class FilterAPI {
591
1107
  * ```
592
1108
  *
593
1109
  * @see {@link flushFrames} For async iteration
594
- * @see {@link frames} For complete pipeline
1110
+ * @see {@link receive} For getting flushed frames
1111
+ * @see {@link flushSync} For synchronous version
595
1112
  */
596
1113
  async flush() {
597
1114
  if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
598
1115
  return;
599
1116
  }
600
1117
  // Send flush frame (null)
601
- const ret = await this.buffersrcCtx.buffersrcAddFrame(null);
1118
+ const ret = await this.buffersrcCtx.buffersrcAddFrame(null, AV_BUFFERSRC_FLAG_PUSH);
602
1119
  if (ret < 0 && ret !== AVERROR_EOF) {
603
1120
  FFmpegError.throwIfError(ret, 'Failed to flush filter');
604
1121
  }
@@ -625,6 +1142,8 @@ export class FilterAPI {
625
1142
  * }
626
1143
  * ```
627
1144
  *
1145
+ * @see {@link flushFramesSync} For sync iteration
1146
+ * @see {@link receiveSync} For getting flushed frames
628
1147
  * @see {@link flush} For async version
629
1148
  */
630
1149
  flushSync() {
@@ -632,7 +1151,7 @@ export class FilterAPI {
632
1151
  return;
633
1152
  }
634
1153
  // Send flush frame (null)
635
- const ret = this.buffersrcCtx.buffersrcAddFrameSync(null);
1154
+ const ret = this.buffersrcCtx.buffersrcAddFrameSync(null, AV_BUFFERSRC_FLAG_PUSH);
636
1155
  if (ret < 0 && ret !== AVERROR_EOF) {
637
1156
  FFmpegError.throwIfError(ret, 'Failed to flush filter');
638
1157
  }
@@ -656,8 +1175,9 @@ export class FilterAPI {
656
1175
  * }
657
1176
  * ```
658
1177
  *
1178
+ * @see {@link process} For frame processing
659
1179
  * @see {@link flush} For manual flush
660
- * @see {@link frames} For complete pipeline
1180
+ * @see {@link flushFramesSync} For sync version
661
1181
  */
662
1182
  async *flushFrames() {
663
1183
  // Send flush signal
@@ -688,6 +1208,8 @@ export class FilterAPI {
688
1208
  * }
689
1209
  * ```
690
1210
  *
1211
+ * @see {@link processSync} For frame processing
1212
+ * @see {@link flushSync} For manual flush
691
1213
  * @see {@link flushFrames} For async version
692
1214
  */
693
1215
  *flushFramesSync() {
@@ -720,19 +1242,26 @@ export class FilterAPI {
720
1242
  * frame.free();
721
1243
  * }
722
1244
  * ```
1245
+ *
1246
+ * @see {@link process} For frame processing
1247
+ * @see {@link flush} For flushing filter
1248
+ * @see {@link receiveSync} For synchronous version
723
1249
  */
724
1250
  async receive() {
725
1251
  if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
726
1252
  return null;
727
1253
  }
728
- const frame = new Frame();
729
- frame.alloc();
730
- const ret = await this.buffersinkCtx.buffersinkGetFrame(frame);
1254
+ // Reuse frame - but alloc() instead of unref() for buffersink
1255
+ // buffersink needs a fresh allocated frame, not an unreferenced one
1256
+ this.frame.alloc();
1257
+ const ret = await this.buffersinkCtx.buffersinkGetFrame(this.frame);
731
1258
  if (ret >= 0) {
732
- return frame;
1259
+ // Post-process output frame (set timeBase from buffersink, calculate duration)
1260
+ this.postProcessOutputFrame(this.frame);
1261
+ // Clone for user (keeps internal frame for reuse)
1262
+ return this.frame.clone();
733
1263
  }
734
1264
  else {
735
- frame.free();
736
1265
  if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
737
1266
  return null;
738
1267
  }
@@ -763,20 +1292,25 @@ export class FilterAPI {
763
1292
  * }
764
1293
  * ```
765
1294
  *
1295
+ * @see {@link processSync} For frame processing
1296
+ * @see {@link flushSync} For flushing filter
766
1297
  * @see {@link receive} For async version
767
1298
  */
768
1299
  receiveSync() {
769
1300
  if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
770
1301
  return null;
771
1302
  }
772
- const frame = new Frame();
773
- frame.alloc();
774
- const ret = this.buffersinkCtx.buffersinkGetFrameSync(frame);
1303
+ // Reuse frame - but alloc() instead of unref() for buffersink
1304
+ // buffersink needs a fresh allocated frame, not an unreferenced one
1305
+ this.frame.alloc();
1306
+ const ret = this.buffersinkCtx.buffersinkGetFrameSync(this.frame);
775
1307
  if (ret >= 0) {
776
- return frame;
1308
+ // Post-process output frame (set timeBase from buffersink, calculate duration)
1309
+ this.postProcessOutputFrame(this.frame);
1310
+ // Clone for user (keeps internal frame for reuse)
1311
+ return this.frame.clone();
777
1312
  }
778
1313
  else {
779
- frame.free();
780
1314
  if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
781
1315
  return null;
782
1316
  }
@@ -869,6 +1403,24 @@ export class FilterAPI {
869
1403
  const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
870
1404
  FFmpegError.throwIfError(ret, 'Failed to queue filter command');
871
1405
  }
1406
+ pipeTo(target) {
1407
+ const t = target;
1408
+ // Store reference to next component for flush propagation
1409
+ this.nextComponent = t;
1410
+ // Start worker if not already running
1411
+ this.workerPromise ??= this.runWorker();
1412
+ // Start pipe task: filter.outputQueue -> target.inputQueue (via target.send)
1413
+ this.pipeToPromise = (async () => {
1414
+ while (true) {
1415
+ const frame = await this.receiveFrame();
1416
+ if (!frame)
1417
+ break;
1418
+ await t.sendToQueue(frame);
1419
+ }
1420
+ })();
1421
+ // Return scheduler for chaining (target is now the last component)
1422
+ return new Scheduler(this, t);
1423
+ }
872
1424
  /**
873
1425
  * Free filter resources.
874
1426
  *
@@ -887,10 +1439,137 @@ export class FilterAPI {
887
1439
  return;
888
1440
  }
889
1441
  this.isClosed = true;
1442
+ // Close queues
1443
+ this.inputQueue.close();
1444
+ this.outputQueue.close();
1445
+ this.frame.free();
890
1446
  this.graph.free();
891
1447
  this.buffersrcCtx = null;
892
1448
  this.buffersinkCtx = null;
893
1449
  this.initialized = false;
1450
+ this.initializePromise = null;
1451
+ }
1452
+ /**
1453
+ * Worker loop for push-based processing.
1454
+ *
1455
+ * @internal
1456
+ */
1457
+ async runWorker() {
1458
+ try {
1459
+ // Outer loop - receive frames
1460
+ while (!this.inputQueue.isClosed) {
1461
+ const env_3 = { stack: [], error: void 0, hasError: false };
1462
+ try {
1463
+ const frame = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
1464
+ if (!frame)
1465
+ break;
1466
+ // Open filter if not already done
1467
+ if (!this.initialized) {
1468
+ this.initializePromise ??= this.initialize(frame);
1469
+ }
1470
+ await this.initializePromise;
1471
+ if (!this.initialized) {
1472
+ continue;
1473
+ }
1474
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
1475
+ throw new Error('Could not initialize filter contexts');
1476
+ }
1477
+ // Rescale timestamps to filter's timeBase
1478
+ if (this.calculatedTimeBase) {
1479
+ const originalTimeBase = frame.timeBase;
1480
+ frame.pts = avRescaleQ(frame.pts, originalTimeBase, this.calculatedTimeBase);
1481
+ frame.duration = avRescaleQ(frame.duration, originalTimeBase, this.calculatedTimeBase);
1482
+ frame.timeBase = new Rational(this.calculatedTimeBase.num, this.calculatedTimeBase.den);
1483
+ }
1484
+ // Send frame to filter
1485
+ const addRet = await this.buffersrcCtx.buffersrcAddFrame(frame, AV_BUFFERSRC_FLAG_PUSH);
1486
+ FFmpegError.throwIfError(addRet, 'Failed to add frame to filter');
1487
+ // Receive all available frames
1488
+ while (!this.outputQueue.isClosed) {
1489
+ const buffered = await this.receive();
1490
+ if (!buffered)
1491
+ break;
1492
+ await this.outputQueue.send(buffered);
1493
+ }
1494
+ }
1495
+ catch (e_3) {
1496
+ env_3.error = e_3;
1497
+ env_3.hasError = true;
1498
+ }
1499
+ finally {
1500
+ __disposeResources(env_3);
1501
+ }
1502
+ }
1503
+ // Flush filter at end
1504
+ await this.flush();
1505
+ while (!this.outputQueue.isClosed) {
1506
+ const frame = await this.receive();
1507
+ if (!frame)
1508
+ break;
1509
+ await this.outputQueue.send(frame);
1510
+ }
1511
+ }
1512
+ catch {
1513
+ // Ignore error ?
1514
+ }
1515
+ finally {
1516
+ // Close output queue when done
1517
+ this.outputQueue?.close();
1518
+ }
1519
+ }
1520
+ /**
1521
+ * Send frame to input queue.
1522
+ *
1523
+ * @param frame - Frame to send
1524
+ *
1525
+ * @internal
1526
+ */
1527
+ async sendToQueue(frame) {
1528
+ await this.inputQueue.send(frame);
1529
+ }
1530
+ /**
1531
+ * Receive frame from output queue.
1532
+ *
1533
+ * @returns Frame from output queue or null if closed
1534
+ *
1535
+ * @internal
1536
+ */
1537
+ async receiveFrame() {
1538
+ return await this.outputQueue.receive();
1539
+ }
1540
+ /**
1541
+ * Flush the entire filter pipeline.
1542
+ *
1543
+ * Propagates flush through worker, output queue, and next component.
1544
+ *
1545
+ * @internal
1546
+ */
1547
+ async flushPipeline() {
1548
+ // Close input queue to signal end of stream to worker
1549
+ this.inputQueue.close();
1550
+ // Wait for worker to finish processing all frames (if exists)
1551
+ if (this.workerPromise) {
1552
+ await this.workerPromise;
1553
+ }
1554
+ // Flush filter at end (like FFmpeg does)
1555
+ await this.flush();
1556
+ // Send all flushed frames to output queue
1557
+ while (true) {
1558
+ const frame = await this.receive();
1559
+ if (!frame)
1560
+ break;
1561
+ await this.outputQueue.send(frame);
1562
+ }
1563
+ // Close output queue to signal end of stream to pipeTo() task
1564
+ this.outputQueue.close();
1565
+ // Wait for pipeTo() task to finish processing all frames (if exists)
1566
+ if (this.pipeToPromise) {
1567
+ await this.pipeToPromise;
1568
+ }
1569
+ // Then propagate flush to next component
1570
+ if (this.nextComponent) {
1571
+ await this.nextComponent.flushPipeline();
1572
+ }
894
1573
  }
895
1574
  /**
896
1575
  * Initialize filter graph from first frame.
@@ -908,6 +1587,23 @@ export class FilterAPI {
908
1587
  * @internal
909
1588
  */
910
1589
  async initialize(frame) {
1590
+ // Calculate timeBase from first frame
1591
+ this.calculatedTimeBase = this.calculateTimeBase(frame);
1592
+ // Track initial frame properties for change detection
1593
+ this.lastFrameProps = {
1594
+ format: frame.format,
1595
+ width: frame.width,
1596
+ height: frame.height,
1597
+ sampleRate: frame.sampleRate,
1598
+ channels: frame.channelLayout?.nbChannels ?? 0,
1599
+ };
1600
+ // Set graph options before parsing
1601
+ if (this.options.scaleSwsOpts) {
1602
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1603
+ }
1604
+ if (this.options.audioResampleOpts) {
1605
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1606
+ }
911
1607
  // Create buffer source and sink
912
1608
  this.createBufferSource(frame);
913
1609
  this.createBufferSink(frame);
@@ -937,6 +1633,23 @@ export class FilterAPI {
937
1633
  * @see {@link initialize} For async version
938
1634
  */
939
1635
  initializeSync(frame) {
1636
+ // Calculate timeBase from first frame
1637
+ this.calculatedTimeBase = this.calculateTimeBase(frame);
1638
+ // Track initial frame properties for change detection
1639
+ this.lastFrameProps = {
1640
+ format: frame.format,
1641
+ width: frame.width,
1642
+ height: frame.height,
1643
+ sampleRate: frame.sampleRate,
1644
+ channels: frame.channelLayout?.nbChannels ?? 0,
1645
+ };
1646
+ // Set graph options before parsing
1647
+ if (this.options.scaleSwsOpts) {
1648
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
1649
+ }
1650
+ if (this.options.audioResampleOpts) {
1651
+ this.graph.aresampleSwrOpts = this.options.audioResampleOpts;
1652
+ }
940
1653
  // Create buffer source and sink
941
1654
  this.createBufferSource(frame);
942
1655
  this.createBufferSink(frame);
@@ -947,6 +1660,116 @@ export class FilterAPI {
947
1660
  FFmpegError.throwIfError(ret, 'Failed to configure filter graph');
948
1661
  this.initialized = true;
949
1662
  }
1663
+ /**
1664
+ * Check if frame properties changed and handle according to dropOnChange/allowReinit options.
1665
+ *
1666
+ * Implements FFmpeg's IFILTER_FLAG_DROPCHANGED and IFILTER_FLAG_REINIT logic
1667
+ *
1668
+ * @param frame - Frame to check
1669
+ *
1670
+ * @returns true if frame should be processed, false if frame should be dropped
1671
+ *
1672
+ * @throws {Error} If format changed and allowReinit is false
1673
+ *
1674
+ * @internal
1675
+ */
1676
+ checkFramePropertiesChanged(frame) {
1677
+ if (!this.lastFrameProps) {
1678
+ return true; // No previous frame, allow
1679
+ }
1680
+ // Check for property changes
1681
+ const changed = frame.format !== this.lastFrameProps.format ||
1682
+ frame.width !== this.lastFrameProps.width ||
1683
+ frame.height !== this.lastFrameProps.height ||
1684
+ frame.sampleRate !== this.lastFrameProps.sampleRate ||
1685
+ (frame.channelLayout?.nbChannels ?? 0) !== this.lastFrameProps.channels;
1686
+ if (!changed) {
1687
+ return true; // No changes, process frame
1688
+ }
1689
+ // Properties changed - check dropOnChange flag
1690
+ if (this.options.dropOnChange) {
1691
+ return false; // Drop frame
1692
+ }
1693
+ // Check allowReinit flag
1694
+ // Default is true (allow reinit), only block if explicitly set to false
1695
+ const allowReinit = this.options.allowReinit !== false;
1696
+ if (!allowReinit && this.initialized) {
1697
+ throw new Error('Frame properties changed but allowReinit is false. ' +
1698
+ `Format: ${this.lastFrameProps.format}->${frame.format}, ` +
1699
+ `Size: ${this.lastFrameProps.width}x${this.lastFrameProps.height}->${frame.width}x${frame.height}`);
1700
+ }
1701
+ // Reinit is allowed - reinitialize filtergraph
1702
+ // Close current graph and reinitialize
1703
+ this.graph.free();
1704
+ this.graph = new FilterGraph();
1705
+ this.buffersrcCtx = null;
1706
+ this.buffersinkCtx = null;
1707
+ this.initialized = false;
1708
+ this.initializePromise = null;
1709
+ this.calculatedTimeBase = null;
1710
+ return true; // Will be reinitialized on next process
1711
+ }
1712
+ /**
1713
+ * Calculate timeBase from frame based on media type and CFR option.
1714
+ *
1715
+ * Implements FFmpeg's ifilter_parameters_from_frame logic:
1716
+ * - Audio: Always { 1, sample_rate }
1717
+ * - Video CFR: 1/framerate (inverse of framerate)
1718
+ * - Video VFR: Use frame.timeBase
1719
+ *
1720
+ * @param frame - Input frame
1721
+ *
1722
+ * @returns Calculated timeBase
1723
+ *
1724
+ * @internal
1725
+ */
1726
+ calculateTimeBase(frame) {
1727
+ if (frame.isAudio()) {
1728
+ // Audio: Always { 1, sample_rate }
1729
+ return { num: 1, den: frame.sampleRate };
1730
+ }
1731
+ else {
1732
+ // Video: Check CFR flag
1733
+ if (this.options.cfr) {
1734
+ // CFR mode: timeBase = 1/framerate = inverse(framerate)
1735
+ // Note: framerate is guaranteed to be set (validated in create())
1736
+ return avInvQ(this.options.framerate);
1737
+ }
1738
+ else {
1739
+ // VFR mode: Use frame.timeBase
1740
+ return frame.timeBase;
1741
+ }
1742
+ }
1743
+ }
1744
+ /**
1745
+ * Post-process output frame from buffersink.
1746
+ *
1747
+ * Applies FFmpeg's fg_output_step() behavior:
1748
+ * 1. Sets frame.timeBase from buffersink (filters can change timeBase, e.g., aresample)
1749
+ * 2. Calculates video frame duration from frame rate if not set
1750
+ *
1751
+ * This must be called AFTER buffersinkGetFrame() for every output frame.
1752
+ *
1753
+ * @param frame - Output frame from buffersink
1754
+ *
1755
+ * @throws {Error} If buffersink context not available
1756
+ *
1757
+ * @internal
1758
+ */
1759
+ postProcessOutputFrame(frame) {
1760
+ if (!this.buffersinkCtx) {
1761
+ throw new Error('Buffersink context not available');
1762
+ }
1763
+ // Filters can change timeBase (e.g., aresample sets output to {1, out_sample_rate})
1764
+ // Without this, frame has INPUT timeBase instead of filter's OUTPUT timeBase
1765
+ frame.timeBase = this.buffersinkCtx.buffersinkGetTimeBase();
1766
+ if (frame.isVideo() && !frame.duration) {
1767
+ const frameRate = this.buffersinkCtx.buffersinkGetFrameRate();
1768
+ if (frameRate.num > 0 && frameRate.den > 0) {
1769
+ frame.duration = avRescaleQ(1, avInvQ(frameRate), frame.timeBase);
1770
+ }
1771
+ }
1772
+ }
950
1773
  /**
951
1774
  * Create buffer source with frame parameters.
952
1775
  *
@@ -967,6 +1790,10 @@ export class FilterAPI {
967
1790
  if (!bufferFilter) {
968
1791
  throw new Error(`${filterName} filter not found`);
969
1792
  }
1793
+ // Ensure timeBase was calculated
1794
+ if (!this.calculatedTimeBase) {
1795
+ throw new Error('TimeBase not calculated - this should not happen');
1796
+ }
970
1797
  // For audio, create with args. For video, use allocFilter + buffersrcParametersSet
971
1798
  if (frame.isVideo()) {
972
1799
  // Allocate filter without args
@@ -978,8 +1805,8 @@ export class FilterAPI {
978
1805
  width: frame.width,
979
1806
  height: frame.height,
980
1807
  format: frame.format,
981
- timeBase: this.options.timeBase,
982
- frameRate: this.options.frameRate ?? frame.timeBase,
1808
+ timeBase: this.calculatedTimeBase,
1809
+ frameRate: this.options.framerate,
983
1810
  sampleAspectRatio: frame.sampleAspectRatio,
984
1811
  colorRange: frame.colorRange,
985
1812
  colorSpace: frame.colorSpace,
@@ -995,7 +1822,7 @@ export class FilterAPI {
995
1822
  const formatName = avGetSampleFmtName(frame.format);
996
1823
  const channelLayout = frame.channelLayout.mask === 0n ? 'stereo' : frame.channelLayout.mask.toString();
997
1824
  // 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}`;
1825
+ const args = `time_base=${this.calculatedTimeBase.num}/${this.calculatedTimeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
999
1826
  this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
1000
1827
  if (!this.buffersrcCtx) {
1001
1828
  throw new Error('Failed to create audio buffer source');
@@ -1061,8 +1888,8 @@ export class FilterAPI {
1061
1888
  if (filters) {
1062
1889
  for (const filterCtx of filters) {
1063
1890
  const filter = filterCtx.filter;
1064
- if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
1065
- filterCtx.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? this.options.hardware?.deviceContext ?? null;
1891
+ if (filter?.hasFlags(AVFILTER_FLAG_HWDEVICE)) {
1892
+ filterCtx.hwDeviceCtx = this.options.hardware?.deviceContext ?? frame.hwFramesCtx?.deviceRef ?? null;
1066
1893
  // Set extra_hw_frames if specified
1067
1894
  if (this.options.extraHWFrames !== undefined && this.options.extraHWFrames > 0) {
1068
1895
  filterCtx.extraHWFrames = this.options.extraHWFrames;