node-av 1.2.0 → 2.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 (99) hide show
  1. package/README.md +37 -59
  2. package/dist/api/bitstream-filter.d.ts +5 -2
  3. package/dist/api/bitstream-filter.js +7 -4
  4. package/dist/api/bitstream-filter.js.map +1 -1
  5. package/dist/api/decoder.d.ts +135 -119
  6. package/dist/api/decoder.js +195 -202
  7. package/dist/api/decoder.js.map +1 -1
  8. package/dist/api/encoder.d.ts +141 -78
  9. package/dist/api/encoder.js +241 -193
  10. package/dist/api/encoder.js.map +1 -1
  11. package/dist/api/filter-presets.d.ts +699 -573
  12. package/dist/api/filter-presets.js +1157 -840
  13. package/dist/api/filter-presets.js.map +1 -1
  14. package/dist/api/filter.d.ts +180 -157
  15. package/dist/api/filter.js +314 -366
  16. package/dist/api/filter.js.map +1 -1
  17. package/dist/api/hardware.d.ts +28 -29
  18. package/dist/api/hardware.js +80 -74
  19. package/dist/api/hardware.js.map +1 -1
  20. package/dist/api/index.d.ts +1 -1
  21. package/dist/api/index.js +1 -1
  22. package/dist/api/index.js.map +1 -1
  23. package/dist/api/io-stream.d.ts +6 -0
  24. package/dist/api/io-stream.js +6 -0
  25. package/dist/api/io-stream.js.map +1 -1
  26. package/dist/api/media-input.d.ts +2 -1
  27. package/dist/api/media-input.js +3 -8
  28. package/dist/api/media-input.js.map +1 -1
  29. package/dist/api/media-output.d.ts +37 -126
  30. package/dist/api/media-output.js +138 -206
  31. package/dist/api/media-output.js.map +1 -1
  32. package/dist/api/pipeline.d.ts +193 -0
  33. package/dist/api/pipeline.js +36 -42
  34. package/dist/api/pipeline.js.map +1 -1
  35. package/dist/api/types.d.ts +22 -57
  36. package/dist/api/utilities/audio-sample.d.ts +0 -8
  37. package/dist/api/utilities/audio-sample.js +0 -8
  38. package/dist/api/utilities/audio-sample.js.map +1 -1
  39. package/dist/api/utilities/channel-layout.d.ts +0 -8
  40. package/dist/api/utilities/channel-layout.js +0 -8
  41. package/dist/api/utilities/channel-layout.js.map +1 -1
  42. package/dist/api/utilities/image.d.ts +0 -8
  43. package/dist/api/utilities/image.js +0 -8
  44. package/dist/api/utilities/image.js.map +1 -1
  45. package/dist/api/utilities/index.d.ts +3 -3
  46. package/dist/api/utilities/index.js +3 -3
  47. package/dist/api/utilities/index.js.map +1 -1
  48. package/dist/api/utilities/media-type.d.ts +1 -9
  49. package/dist/api/utilities/media-type.js +1 -9
  50. package/dist/api/utilities/media-type.js.map +1 -1
  51. package/dist/api/utilities/pixel-format.d.ts +1 -9
  52. package/dist/api/utilities/pixel-format.js +1 -9
  53. package/dist/api/utilities/pixel-format.js.map +1 -1
  54. package/dist/api/utilities/sample-format.d.ts +1 -9
  55. package/dist/api/utilities/sample-format.js +1 -9
  56. package/dist/api/utilities/sample-format.js.map +1 -1
  57. package/dist/api/utilities/streaming.d.ts +0 -8
  58. package/dist/api/utilities/streaming.js +0 -8
  59. package/dist/api/utilities/streaming.js.map +1 -1
  60. package/dist/api/utilities/timestamp.d.ts +0 -8
  61. package/dist/api/utilities/timestamp.js +0 -8
  62. package/dist/api/utilities/timestamp.js.map +1 -1
  63. package/dist/api/utils.js +2 -0
  64. package/dist/api/utils.js.map +1 -1
  65. package/dist/constants/constants.d.ts +1 -1
  66. package/dist/constants/constants.js +2 -0
  67. package/dist/constants/constants.js.map +1 -1
  68. package/dist/lib/binding.d.ts +1 -0
  69. package/dist/lib/binding.js +2 -0
  70. package/dist/lib/binding.js.map +1 -1
  71. package/dist/lib/codec.d.ts +4 -4
  72. package/dist/lib/codec.js +4 -4
  73. package/dist/lib/dictionary.d.ts +2 -2
  74. package/dist/lib/dictionary.js +2 -2
  75. package/dist/lib/dictionary.js.map +1 -1
  76. package/dist/lib/error.d.ts +1 -1
  77. package/dist/lib/error.js +1 -1
  78. package/dist/lib/filter-context.d.ts +19 -2
  79. package/dist/lib/filter-context.js +15 -0
  80. package/dist/lib/filter-context.js.map +1 -1
  81. package/dist/lib/format-context.d.ts +18 -18
  82. package/dist/lib/format-context.js +20 -20
  83. package/dist/lib/format-context.js.map +1 -1
  84. package/dist/lib/frame.d.ts +43 -1
  85. package/dist/lib/frame.js +53 -0
  86. package/dist/lib/frame.js.map +1 -1
  87. package/dist/lib/index.d.ts +1 -1
  88. package/dist/lib/index.js +1 -1
  89. package/dist/lib/index.js.map +1 -1
  90. package/dist/lib/native-types.d.ts +1 -0
  91. package/dist/lib/option.d.ts +176 -0
  92. package/dist/lib/option.js +176 -0
  93. package/dist/lib/option.js.map +1 -1
  94. package/dist/lib/utilities.d.ts +64 -1
  95. package/dist/lib/utilities.js +65 -0
  96. package/dist/lib/utilities.js.map +1 -1
  97. package/install/ffmpeg.js +0 -11
  98. package/package.json +16 -18
  99. package/release_notes.md +0 -48
@@ -1,5 +1,6 @@
1
- import { AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
2
- import { AVERROR_EAGAIN, avGetSampleFmtName, avIsHardwarePixelFormat, FFmpegError, Filter, FilterGraph, FilterInOut, Frame } from '../lib/index.js';
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';
3
4
  /**
4
5
  * High-level filter API for audio and video processing.
5
6
  *
@@ -12,10 +13,12 @@ import { AVERROR_EAGAIN, avGetSampleFmtName, avIsHardwarePixelFormat, FFmpegErro
12
13
  * ```typescript
13
14
  * import { FilterAPI } from 'node-av/api';
14
15
  *
15
- * // Create video filter
16
- * const filter = await FilterAPI.create('scale=1280:720', videoInfo);
16
+ * // Create video filter - initializes on first frame
17
+ * const filter = await FilterAPI.create('scale=1280:720', {
18
+ * timeBase: video.timeBase,
19
+ * });
17
20
  *
18
- * // Process frame
21
+ * // Process frame - first frame configures filter graph
19
22
  * const output = await filter.process(inputFrame);
20
23
  * if (output) {
21
24
  * console.log(`Filtered frame: ${output.width}x${output.height}`);
@@ -25,134 +28,174 @@ import { AVERROR_EAGAIN, avGetSampleFmtName, avIsHardwarePixelFormat, FFmpegErro
25
28
  *
26
29
  * @example
27
30
  * ```typescript
28
- * // Hardware-accelerated filtering
29
- * const hw = HardwareContext.auto();
30
- * const filter = await FilterAPI.create(
31
- * 'hwupload,scale_cuda=1920:1080,hwdownload',
32
- * videoInfo,
33
- * { hardware: hw }
34
- * );
31
+ * // Hardware-accelerated filtering - hw context detected from frame
32
+ * const filter = await FilterAPI.create('hwupload,scale_cuda=1920:1080,hwdownload', {
33
+ * timeBase: video.timeBase,
34
+ * });
35
+ * // Hardware frames context will be automatically detected from first frame
35
36
  * ```
36
37
  *
37
38
  * @see {@link FilterGraph} For low-level filter graph API
38
- * @see {@link HardwareContext} For hardware acceleration
39
39
  * @see {@link Frame} For frame operations
40
40
  */
41
41
  export class FilterAPI {
42
- graph = null;
42
+ graph;
43
+ description;
44
+ options;
43
45
  buffersrcCtx = null;
44
46
  buffersinkCtx = null;
45
- config;
46
- mediaType;
47
47
  initialized = false;
48
- hardware;
49
- description;
50
- options;
48
+ isClosed = false;
51
49
  /**
52
- * @param config - Stream configuration
50
+ * @param graph - Filter graph instance
53
51
  * @param description - Filter description string
54
52
  * @param options - Filter options
55
53
  * @internal
56
54
  */
57
- constructor(config, description, options) {
58
- this.config = config;
55
+ constructor(graph, description, options) {
56
+ this.graph = graph;
59
57
  this.description = description;
60
58
  this.options = options;
61
- this.hardware = options.hardware;
62
- this.mediaType = config.type === 'video' ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO;
63
59
  }
64
60
  /**
65
61
  * Create a filter with specified description and configuration.
66
62
  *
67
- * Constructs filter graph from description string.
68
- * Configures input/output buffers and threading.
69
- * For video filters, uses lazy initialization to detect hardware frames.
63
+ * Creates and allocates filter graph immediately.
64
+ * Filter configuration is completed on first frame with frame properties.
65
+ * Hardware frames context is automatically detected from input frames.
70
66
  *
71
67
  * Direct mapping to avfilter_graph_parse_ptr() and avfilter_graph_config().
72
68
  *
73
69
  * @param description - Filter graph description
74
- * @param input - Input stream configuration
75
- * @param options - Filter options
70
+ * @param options - Filter options including required timeBase
76
71
  * @returns Configured filter instance
77
72
  *
78
73
  * @throws {Error} If filter creation or configuration fails
74
+ *
79
75
  * @throws {FFmpegError} If graph parsing or config fails
80
76
  *
81
77
  * @example
82
78
  * ```typescript
83
79
  * // Simple video filter
84
- * const filter = await FilterAPI.create('scale=640:480', videoInfo);
80
+ * const filter = await FilterAPI.create('scale=640:480', {
81
+ * timeBase: video.timeBase
82
+ * });
85
83
  * ```
86
84
  *
87
85
  * @example
88
86
  * ```typescript
89
87
  * // Complex filter chain
90
- * const filter = await FilterAPI.create(
91
- * 'crop=640:480:0:0,rotate=PI/4',
92
- * videoInfo
93
- * );
88
+ * const filter = await FilterAPI.create('crop=640:480:0:0,rotate=PI/4', {
89
+ * timeBase: video.timeBase
90
+ * });
94
91
  * ```
95
92
  *
96
93
  * @example
97
94
  * ```typescript
98
95
  * // Audio filter
99
- * const filter = await FilterAPI.create(
100
- * 'volume=0.5,aecho=0.8:0.9:1000:0.3',
101
- * audioInfo
102
- * );
96
+ * const filter = await FilterAPI.create('volume=0.5,aecho=0.8:0.9:1000:0.3', {
97
+ * timeBase: audio.timeBase
98
+ * });
103
99
  * ```
104
100
  *
105
101
  * @see {@link process} For frame processing
106
102
  * @see {@link FilterOptions} For configuration options
107
103
  */
108
- static async create(description, input, options = {}) {
109
- let config;
110
- if (input.type === 'video') {
111
- config = {
112
- type: 'video',
113
- width: input.width,
114
- height: input.height,
115
- pixelFormat: input.pixelFormat,
116
- timeBase: input.timeBase,
117
- frameRate: input.frameRate,
118
- sampleAspectRatio: input.sampleAspectRatio,
119
- };
120
- }
121
- else {
122
- config = {
123
- type: 'audio',
124
- sampleRate: input.sampleRate,
125
- sampleFormat: input.sampleFormat,
126
- channelLayout: input.channelLayout,
127
- timeBase: input.timeBase,
128
- };
129
- }
130
- const filter = new FilterAPI(config, description, options);
131
- // Check if any filters in the chain require hardware context
132
- if (config.type === 'video') {
133
- filter.checkHardwareRequirements(description, options);
104
+ static async create(description, options) {
105
+ // Create graph
106
+ const graph = new FilterGraph();
107
+ graph.alloc();
108
+ // Configure threading
109
+ if (options.threads !== undefined) {
110
+ graph.nbThreads = options.threads;
134
111
  }
135
- // For video filters, always use lazy initialization to properly detect hardware requirements
136
- // For audio filters, initialize immediately (no hardware audio processing)
137
- if (config.type === 'audio') {
138
- await filter.initialize(null);
112
+ // Configure scaler options
113
+ if (options.scaleSwsOpts) {
114
+ graph.scaleSwsOpts = options.scaleSwsOpts;
139
115
  }
140
- // For video: wait for first frame to detect if hw_frames_ctx is present
141
- return filter;
116
+ return new FilterAPI(graph, description, options);
117
+ }
118
+ /**
119
+ * Check if filter is open.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * if (filter.isFilterOpen) {
124
+ * const output = await filter.process(frame);
125
+ * }
126
+ * ```
127
+ */
128
+ get isFilterOpen() {
129
+ return !this.isClosed;
130
+ }
131
+ /**
132
+ * Check if filter has been initialized.
133
+ *
134
+ * Returns true after first frame has been processed and filter graph configured.
135
+ * Useful for checking if filter has received frame properties.
136
+ *
137
+ * @returns true if filter graph has been built from first frame
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * if (!filter.isFilterInitialized) {
142
+ * console.log('Filter will initialize on first frame');
143
+ * }
144
+ * ```
145
+ */
146
+ get isFilterInitialized() {
147
+ return this.initialized;
148
+ }
149
+ /**
150
+ * Check if filter is ready for processing.
151
+ *
152
+ * @returns true if initialized and ready
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * if (filter.isReady()) {
157
+ * const output = await filter.process(frame);
158
+ * }
159
+ * ```
160
+ */
161
+ isReady() {
162
+ return this.initialized && this.buffersrcCtx !== null && this.buffersinkCtx !== null && !this.isClosed;
163
+ }
164
+ /**
165
+ * Get filter graph description.
166
+ *
167
+ * Returns human-readable graph structure.
168
+ * Useful for debugging filter chains.
169
+ *
170
+ * Direct mapping to avfilter_graph_dump().
171
+ *
172
+ * @returns Graph description or null if closed
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * const description = filter.getGraphDescription();
177
+ * console.log('Filter graph:', description);
178
+ * ```
179
+ */
180
+ getGraphDescription() {
181
+ return !this.isClosed && this.initialized ? this.graph.dump() : null;
142
182
  }
143
183
  /**
144
184
  * Process a frame through the filter.
145
185
  *
146
186
  * Applies filter operations to input frame.
187
+ * On first frame, automatically builds filter graph with frame properties.
147
188
  * May buffer frames internally before producing output.
148
- * For video, performs lazy initialization on first frame.
189
+ * Hardware frames context is automatically detected from frame.
190
+ * Returns null if filter is closed and frame is null.
149
191
  *
150
192
  * Direct mapping to av_buffersrc_add_frame() and av_buffersink_get_frame().
151
193
  *
152
- * @param frame - Input frame to process
194
+ * @param frame - Input frame to process (or null to flush)
153
195
  * @returns Filtered frame or null if buffered
154
196
  *
155
- * @throws {Error} If filter not ready
197
+ * @throws {Error} If filter is closed with non-null frame
198
+ *
156
199
  * @throws {FFmpegError} If processing fails
157
200
  *
158
201
  * @example
@@ -166,26 +209,33 @@ export class FilterAPI {
166
209
  *
167
210
  * @example
168
211
  * ```typescript
169
- * // Process and drain
212
+ * // Process frame - may buffer internally
170
213
  * const output = await filter.process(frame);
171
- * if (output) yield output;
172
- *
173
- * // Drain buffered frames
174
- * let buffered;
175
- * while ((buffered = await filter.receive()) !== null) {
176
- * yield buffered;
214
+ * if (output) {
215
+ * // Got output immediately
216
+ * yield output;
177
217
  * }
218
+ * // For buffered frames, use the frames() async generator
178
219
  * ```
179
220
  *
180
- * @see {@link receive} For draining buffered frames
181
- * @see {@link frames} For stream processing
221
+ * @see {@link frames} For processing frame streams
222
+ * @see {@link flush} For end-of-stream handling
182
223
  */
183
224
  async process(frame) {
184
- // Lazy initialization for video filters (detect hardware from first frame)
185
- if (!this.initialized && this.config.type === 'video') {
225
+ if (this.isClosed) {
226
+ if (!frame) {
227
+ return null;
228
+ }
229
+ throw new Error('Filter is closed');
230
+ }
231
+ // Open filter if not already done
232
+ if (!this.initialized) {
233
+ if (!frame) {
234
+ return null;
235
+ }
186
236
  await this.initialize(frame);
187
237
  }
188
- if (!this.initialized || !this.buffersrcCtx || !this.buffersinkCtx) {
238
+ if (!this.buffersrcCtx || !this.buffersinkCtx) {
189
239
  throw new Error('Filter not initialized');
190
240
  }
191
241
  // Send frame to filter
@@ -219,6 +269,7 @@ export class FilterAPI {
219
269
  * @returns Array of all output frames
220
270
  *
221
271
  * @throws {Error} If filter not ready
272
+ *
222
273
  * @throws {FFmpegError} If processing fails
223
274
  *
224
275
  * @example
@@ -250,48 +301,74 @@ export class FilterAPI {
250
301
  return outputFrames;
251
302
  }
252
303
  /**
253
- * Receive buffered frame from filter.
304
+ * Process frame stream through filter.
254
305
  *
255
- * Drains frames buffered by the filter.
256
- * Call repeatedly until null to get all buffered frames.
306
+ * High-level async generator for filtering frame streams.
307
+ * Automatically handles buffering and flushing.
308
+ * Frees input frames after processing.
257
309
  *
258
- * Direct mapping to av_buffersink_get_frame().
310
+ * @param frames - Async generator of input frames
311
+ * @yields {Frame} Filtered frames
312
+ * @throws {Error} If filter not ready
259
313
  *
260
- * @returns Buffered frame or null if none available
314
+ * @throws {FFmpegError} If processing fails
261
315
  *
262
- * @throws {Error} If filter not ready
263
- * @throws {FFmpegError} If receive fails
316
+ * @example
317
+ * ```typescript
318
+ * // Filter decoded frames
319
+ * for await (const frame of filter.frames(decoder.frames(packets))) {
320
+ * await encoder.encode(frame);
321
+ * frame.free();
322
+ * }
323
+ * ```
264
324
  *
265
325
  * @example
266
326
  * ```typescript
267
- * // Drain buffered frames
268
- * let frame;
269
- * while ((frame = await filter.receive()) !== null) {
270
- * console.log(`Buffered frame: pts=${frame.pts}`);
327
+ * // Chain filters
328
+ * const filter1 = await FilterAPI.create('scale=640:480', {
329
+ * timeBase: video.timeBase
330
+ * });
331
+ * const filter2 = await FilterAPI.create('rotate=PI/4', {
332
+ * timeBase: video.timeBase
333
+ * });
334
+ *
335
+ * for await (const frame of filter2.frames(filter1.frames(input))) {
336
+ * // Process filtered frames
271
337
  * frame.free();
272
338
  * }
273
339
  * ```
274
340
  *
275
- * @see {@link process} For input processing
276
- * @see {@link flush} For end-of-stream
341
+ * @see {@link process} For single frame processing
342
+ * @see {@link flush} For end-of-stream handling
277
343
  */
278
- async receive() {
279
- if (!this.initialized || !this.buffersinkCtx) {
280
- throw new Error('Filter not initialized');
281
- }
282
- const frame = new Frame();
283
- frame.alloc();
284
- const ret = await this.buffersinkCtx.buffersinkGetFrame(frame);
285
- if (ret >= 0) {
286
- return frame;
287
- }
288
- else {
289
- frame.free();
290
- if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
291
- return null;
344
+ async *frames(frames) {
345
+ for await (const frame of frames) {
346
+ try {
347
+ // Process input frame
348
+ const output = await this.process(frame);
349
+ if (output) {
350
+ yield output;
351
+ }
352
+ // Drain any buffered frames
353
+ while (true) {
354
+ const buffered = await this.receive();
355
+ if (!buffered)
356
+ break;
357
+ yield buffered;
358
+ }
292
359
  }
293
- FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
294
- return null;
360
+ finally {
361
+ // Free the input frame after processing
362
+ frame.free();
363
+ }
364
+ }
365
+ // Flush and get remaining frames
366
+ await this.flush();
367
+ while (true) {
368
+ const remaining = await this.receive();
369
+ if (!remaining)
370
+ break;
371
+ yield remaining;
295
372
  }
296
373
  }
297
374
  /**
@@ -299,10 +376,10 @@ export class FilterAPI {
299
376
  *
300
377
  * Sends null frame to flush buffered data.
301
378
  * Must call receive() to get flushed frames.
379
+ * Does nothing if filter is closed or was never initialized.
302
380
  *
303
381
  * Direct mapping to av_buffersrc_add_frame(NULL).
304
382
  *
305
- * @throws {Error} If filter not ready
306
383
  * @throws {FFmpegError} If flush fails
307
384
  *
308
385
  * @example
@@ -316,12 +393,13 @@ export class FilterAPI {
316
393
  * ```
317
394
  *
318
395
  * @see {@link flushFrames} For async iteration
319
- * @see {@link receive} For draining frames
396
+ * @see {@link frames} For complete pipeline
320
397
  */
321
398
  async flush() {
322
- if (!this.initialized || !this.buffersrcCtx) {
323
- throw new Error('Filter not initialized');
399
+ if (this.isClosed || !this.initialized || !this.buffersrcCtx) {
400
+ return;
324
401
  }
402
+ // Send flush frame (null)
325
403
  const ret = await this.buffersrcCtx.buffersrcAddFrame(null);
326
404
  if (ret < 0 && ret !== AVERROR_EOF) {
327
405
  FFmpegError.throwIfError(ret, 'Failed to flush filter');
@@ -332,9 +410,10 @@ export class FilterAPI {
332
410
  *
333
411
  * Convenient async generator for flushing.
334
412
  * Combines flush and receive operations.
413
+ * Returns immediately if filter is closed or was never initialized.
414
+ *
415
+ * @yields {Frame} Remaining frames from filter
335
416
  *
336
- * @yields Remaining frames from filter
337
- * @throws {Error} If filter not ready
338
417
  * @throws {FFmpegError} If flush fails
339
418
  *
340
419
  * @example
@@ -349,9 +428,6 @@ export class FilterAPI {
349
428
  * @see {@link frames} For complete pipeline
350
429
  */
351
430
  async *flushFrames() {
352
- if (!this.initialized || !this.buffersrcCtx) {
353
- throw new Error('Filter not initialized');
354
- }
355
431
  // Send flush signal
356
432
  await this.flush();
357
433
  // Yield all remaining frames
@@ -361,69 +437,44 @@ export class FilterAPI {
361
437
  }
362
438
  }
363
439
  /**
364
- * Process frame stream through filter.
440
+ * Receive buffered frame from filter.
365
441
  *
366
- * High-level async generator for filtering frame streams.
367
- * Automatically handles buffering and flushing.
368
- * Frees input frames after processing.
442
+ * Drains frames buffered by the filter.
443
+ * Call repeatedly until null to get all buffered frames.
444
+ * Returns null if filter is closed, not initialized, or no frames available.
369
445
  *
370
- * @param frames - Async generator of input frames
371
- * @yields Filtered frames
372
- * @throws {Error} If filter not ready
373
- * @throws {FFmpegError} If processing fails
446
+ * Direct mapping to av_buffersink_get_frame().
374
447
  *
375
- * @example
376
- * ```typescript
377
- * // Filter decoded frames
378
- * for await (const frame of filter.frames(decoder.frames(packets))) {
379
- * await encoder.encode(frame);
380
- * frame.free();
381
- * }
382
- * ```
448
+ * @returns Buffered frame or null if none available
449
+ *
450
+ * @throws {FFmpegError} If receiving fails
383
451
  *
384
452
  * @example
385
453
  * ```typescript
386
- * // Chain filters
387
- * const filter1 = await FilterAPI.create('scale=640:480', info);
388
- * const filter2 = await FilterAPI.create('rotate=PI/4', info);
389
- *
390
- * for await (const frame of filter2.frames(filter1.frames(input))) {
391
- * // Process filtered frames
454
+ * let frame;
455
+ * while ((frame = await filter.receive()) !== null) {
456
+ * console.log(`Received frame: pts=${frame.pts}`);
392
457
  * frame.free();
393
458
  * }
394
459
  * ```
395
- *
396
- * @see {@link process} For single frame processing
397
- * @see {@link flush} For end-of-stream handling
398
460
  */
399
- async *frames(frames) {
400
- for await (const frame of frames) {
401
- try {
402
- // Process input frame
403
- const output = await this.process(frame);
404
- if (output) {
405
- yield output;
406
- }
407
- // Drain any buffered frames
408
- while (true) {
409
- const buffered = await this.receive();
410
- if (!buffered)
411
- break;
412
- yield buffered;
413
- }
414
- }
415
- finally {
416
- // Free the input frame after processing
417
- frame.free();
418
- }
461
+ async receive() {
462
+ if (this.isClosed || !this.initialized || !this.buffersinkCtx) {
463
+ return null;
419
464
  }
420
- // Flush and get remaining frames
421
- await this.flush();
422
- while (true) {
423
- const remaining = await this.receive();
424
- if (!remaining)
425
- break;
426
- yield remaining;
465
+ const frame = new Frame();
466
+ frame.alloc();
467
+ const ret = await this.buffersinkCtx.buffersinkGetFrame(frame);
468
+ if (ret >= 0) {
469
+ return frame;
470
+ }
471
+ else {
472
+ frame.free();
473
+ if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
474
+ return null;
475
+ }
476
+ FFmpegError.throwIfError(ret, 'Failed to receive frame from filter');
477
+ return null;
427
478
  }
428
479
  }
429
480
  /**
@@ -441,6 +492,7 @@ export class FilterAPI {
441
492
  * @returns Response string from filter
442
493
  *
443
494
  * @throws {Error} If filter not ready
495
+ *
444
496
  * @throws {FFmpegError} If command fails
445
497
  *
446
498
  * @example
@@ -453,7 +505,10 @@ export class FilterAPI {
453
505
  * @see {@link queueCommand} For delayed commands
454
506
  */
455
507
  sendCommand(target, cmd, arg, flags) {
456
- if (!this.initialized || !this.graph) {
508
+ if (this.isClosed) {
509
+ throw new Error('Filter is closed');
510
+ }
511
+ if (!this.initialized) {
457
512
  throw new Error('Filter not initialized');
458
513
  }
459
514
  const result = this.graph.sendCommand(target, cmd, arg, flags);
@@ -476,6 +531,7 @@ export class FilterAPI {
476
531
  * @param ts - Timestamp for execution
477
532
  * @param flags - Command flags
478
533
  * @throws {Error} If filter not ready
534
+ *
479
535
  * @throws {FFmpegError} If queue fails
480
536
  *
481
537
  * @example
@@ -487,64 +543,15 @@ export class FilterAPI {
487
543
  * @see {@link sendCommand} For immediate commands
488
544
  */
489
545
  queueCommand(target, cmd, arg, ts, flags) {
490
- if (!this.initialized || !this.graph) {
546
+ if (this.isClosed) {
547
+ throw new Error('Filter is closed');
548
+ }
549
+ if (!this.initialized) {
491
550
  throw new Error('Filter not initialized');
492
551
  }
493
552
  const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
494
553
  FFmpegError.throwIfError(ret, 'Failed to queue filter command');
495
554
  }
496
- /**
497
- * Get filter graph description.
498
- *
499
- * Returns human-readable graph structure.
500
- * Useful for debugging filter chains.
501
- *
502
- * Direct mapping to avfilter_graph_dump().
503
- *
504
- * @returns Graph description or null if not initialized
505
- *
506
- * @example
507
- * ```typescript
508
- * const description = filter.getGraphDescription();
509
- * console.log('Filter graph:', description);
510
- * ```
511
- */
512
- getGraphDescription() {
513
- if (!this.initialized || !this.graph) {
514
- return null;
515
- }
516
- return this.graph.dump();
517
- }
518
- /**
519
- * Check if filter is ready for processing.
520
- *
521
- * @returns true if initialized and ready
522
- *
523
- * @example
524
- * ```typescript
525
- * if (filter.isReady()) {
526
- * const output = await filter.process(frame);
527
- * }
528
- * ```
529
- */
530
- isReady() {
531
- return this.initialized && this.buffersrcCtx !== null && this.buffersinkCtx !== null;
532
- }
533
- /**
534
- * Get media type of filter.
535
- *
536
- * @returns AVMEDIA_TYPE_VIDEO or AVMEDIA_TYPE_AUDIO
537
- *
538
- * @example
539
- * ```typescript
540
- * if (filter.getMediaType() === AVMEDIA_TYPE_VIDEO) {
541
- * console.log('Video filter');
542
- * }
543
- * ```
544
- */
545
- getMediaType() {
546
- return this.mediaType;
547
- }
548
555
  /**
549
556
  * Free filter resources.
550
557
  *
@@ -553,62 +560,50 @@ export class FilterAPI {
553
560
  *
554
561
  * @example
555
562
  * ```typescript
556
- * filter.free();
563
+ * filter.close();
557
564
  * ```
558
565
  *
559
566
  * @see {@link Symbol.dispose} For automatic cleanup
560
567
  */
561
- free() {
562
- if (this.graph) {
563
- this.graph.free();
564
- this.graph = null;
568
+ close() {
569
+ if (this.isClosed) {
570
+ return;
565
571
  }
572
+ this.isClosed = true;
573
+ this.graph.free();
566
574
  this.buffersrcCtx = null;
567
575
  this.buffersinkCtx = null;
568
576
  this.initialized = false;
569
577
  }
570
578
  /**
571
- * Initialize filter graph.
579
+ * Initialize filter graph from first frame.
572
580
  *
573
581
  * Creates and configures filter graph components.
574
- * For video, may use hardware frames context from first frame.
582
+ * Sets buffer source parameters from frame properties.
583
+ * Automatically configures hardware frames context if present.
584
+ *
585
+ * @param frame - First frame to process, provides format and hw context
575
586
  *
576
- * @param firstFrame - First frame for hardware detection (video only)
577
587
  * @throws {Error} If initialization fails
588
+ *
578
589
  * @throws {FFmpegError} If configuration fails
590
+ *
591
+ * @internal
579
592
  */
580
- async initialize(firstFrame) {
581
- // Create graph
582
- this.graph = new FilterGraph();
583
- this.graph.alloc();
584
- // Configure threading
585
- if (this.options.threads !== undefined) {
586
- this.graph.nbThreads = this.options.threads;
587
- }
588
- // Configure scaler options
589
- if (this.options.scaleSwsOpts) {
590
- this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
591
- }
592
- // Create buffer source with hw_frames_ctx if needed
593
- if (firstFrame?.hwFramesCtx && this.config.type === 'video') {
594
- this.createBufferSourceWithHwFrames(firstFrame);
595
- }
596
- else {
597
- this.createBufferSource();
598
- }
593
+ async initialize(frame) {
594
+ // Create buffer source
595
+ this.createBufferSource(frame);
599
596
  // Create buffer sink
600
- this.createBufferSink();
597
+ this.createBufferSink(frame);
601
598
  // Parse filter description
602
599
  this.parseFilterDescription(this.description);
603
600
  // Set hw_device_ctx on hardware filters
604
- if (this.hardware?.deviceContext) {
605
- const filters = this.graph.filters;
606
- if (filters) {
607
- for (const filterCtx of filters) {
608
- const filter = filterCtx.filter;
609
- if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
610
- filterCtx.hwDeviceCtx = this.hardware.deviceContext;
611
- }
601
+ const filters = this.graph.filters;
602
+ if (filters) {
603
+ for (const filterCtx of filters) {
604
+ const filter = filterCtx.filter;
605
+ if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
606
+ filterCtx.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? this.options.hardware?.deviceContext ?? null;
612
607
  }
613
608
  }
614
609
  }
@@ -618,83 +613,71 @@ export class FilterAPI {
618
613
  this.initialized = true;
619
614
  }
620
615
  /**
621
- * Create buffer source with hardware frames context.
616
+ * Create buffer source with frame parameters.
617
+ *
618
+ * Configures buffer source with frame properties including hardware context.
619
+ * Automatically detects video/audio and sets appropriate parameters.
620
+ *
621
+ * @param frame - Frame providing format, dimensions, and hw_frames_ctx
622
622
  *
623
- * @param frame - Frame with hw_frames_ctx
624
623
  * @throws {Error} If creation fails
624
+ *
625
625
  * @throws {FFmpegError} If configuration fails
626
- */
627
- createBufferSourceWithHwFrames(frame) {
628
- const filterName = 'buffer';
629
- const bufferFilter = Filter.getByName(filterName);
630
- if (!bufferFilter) {
631
- throw new Error(`${filterName} filter not found`);
632
- }
633
- // Allocate filter without args
634
- this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
635
- if (!this.buffersrcCtx) {
636
- throw new Error('Failed to allocate buffer source');
637
- }
638
- // Set parameters including hw_frames_ctx
639
- const cfg = this.config;
640
- const ret = this.buffersrcCtx.buffersrcParametersSet({
641
- width: cfg.width,
642
- height: cfg.height,
643
- format: cfg.pixelFormat,
644
- timeBase: cfg.timeBase,
645
- frameRate: cfg.frameRate,
646
- sampleAspectRatio: cfg.sampleAspectRatio,
647
- hwFramesCtx: frame.hwFramesCtx ?? undefined,
648
- });
649
- FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters');
650
- // Initialize filter
651
- const initRet = this.buffersrcCtx.init(null);
652
- FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
653
- }
654
- /**
655
- * Create standard buffer source.
656
626
  *
657
- * @throws {Error} If creation fails
627
+ * @internal
658
628
  */
659
- createBufferSource() {
660
- const filterName = this.config.type === 'video' ? 'buffer' : 'abuffer';
629
+ createBufferSource(frame) {
630
+ const filterName = frame.isVideo() ? 'buffer' : 'abuffer';
661
631
  const bufferFilter = Filter.getByName(filterName);
662
632
  if (!bufferFilter) {
663
633
  throw new Error(`${filterName} filter not found`);
664
634
  }
665
- // Build args string
666
- let args;
667
- if (this.config.type === 'video') {
668
- const cfg = this.config;
669
- args = `video_size=${cfg.width}x${cfg.height}:pix_fmt=${cfg.pixelFormat}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
670
- if (cfg.frameRate) {
671
- args += `:frame_rate=${cfg.frameRate.num}/${cfg.frameRate.den}`;
672
- }
673
- if (cfg.sampleAspectRatio) {
674
- args += `:pixel_aspect=${cfg.sampleAspectRatio.num}/${cfg.sampleAspectRatio.den}`;
635
+ // For audio, create with args. For video, use allocFilter + buffersrcParametersSet
636
+ if (frame.isVideo()) {
637
+ // Allocate filter without args
638
+ this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
639
+ if (!this.buffersrcCtx) {
640
+ throw new Error('Failed to allocate buffer source');
675
641
  }
642
+ const ret = this.buffersrcCtx.buffersrcParametersSet({
643
+ width: frame.width,
644
+ height: frame.height,
645
+ format: frame.format,
646
+ timeBase: this.options.timeBase,
647
+ frameRate: this.options.frameRate ?? frame.timeBase,
648
+ sampleAspectRatio: frame.sampleAspectRatio,
649
+ colorRange: frame.colorRange,
650
+ colorSpace: frame.colorSpace,
651
+ hwFramesCtx: frame.hwFramesCtx,
652
+ });
653
+ FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters');
654
+ // Initialize filter
655
+ const initRet = this.buffersrcCtx.init(null);
656
+ FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
676
657
  }
677
658
  else {
678
- const cfg = this.config;
679
- const sampleFmtName = avGetSampleFmtName(cfg.sampleFormat);
680
- const channelLayout = cfg.channelLayout.mask === 0n ? 'stereo' : cfg.channelLayout.mask.toString();
681
- args = `sample_rate=${cfg.sampleRate}:sample_fmt=${sampleFmtName}:channel_layout=${channelLayout}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
682
- }
683
- this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
684
- if (!this.buffersrcCtx) {
685
- throw new Error('Failed to create buffer source');
659
+ // For audio, create with args string
660
+ const formatName = avGetSampleFmtName(frame.format);
661
+ const channelLayout = frame.channelLayout.mask === 0n ? 'stereo' : frame.channelLayout.mask.toString();
662
+ // eslint-disable-next-line @stylistic/max-len
663
+ const args = `time_base=${this.options.timeBase.num}/${this.options.timeBase.den}:sample_rate=${frame.sampleRate}:sample_fmt=${formatName}:channel_layout=${channelLayout}`;
664
+ this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
665
+ if (!this.buffersrcCtx) {
666
+ throw new Error('Failed to create audio buffer source');
667
+ }
686
668
  }
687
669
  }
688
670
  /**
689
671
  * Create buffer sink.
690
672
  *
673
+ * @param frame - Frame
674
+ *
691
675
  * @throws {Error} If creation fails
676
+ *
677
+ * @internal
692
678
  */
693
- createBufferSink() {
694
- if (!this.graph) {
695
- throw new Error('Filter graph not initialized');
696
- }
697
- const filterName = this.config.type === 'video' ? 'buffersink' : 'abuffersink';
679
+ createBufferSink(frame) {
680
+ const filterName = frame.isVideo() ? 'buffersink' : 'abuffersink';
698
681
  const sinkFilter = Filter.getByName(filterName);
699
682
  if (!sinkFilter) {
700
683
  throw new Error(`${filterName} filter not found`);
@@ -708,13 +691,14 @@ export class FilterAPI {
708
691
  * Parse filter description and build graph.
709
692
  *
710
693
  * @param description - Filter description string
694
+ *
711
695
  * @throws {Error} If parsing fails
696
+ *
712
697
  * @throws {FFmpegError} If graph construction fails
698
+ *
699
+ * @internal
713
700
  */
714
701
  parseFilterDescription(description) {
715
- if (!this.graph) {
716
- throw new Error('Filter graph not initialized');
717
- }
718
702
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
719
703
  throw new Error('Buffer filters not initialized');
720
704
  }
@@ -743,60 +727,24 @@ export class FilterAPI {
743
727
  inputs.free();
744
728
  outputs.free();
745
729
  }
746
- /**
747
- * Check hardware requirements for filters.
748
- *
749
- * @param description - Filter description
750
- * @param options - Filter options
751
- * @throws {Error} If hardware requirements not met
752
- */
753
- checkHardwareRequirements(description, options) {
754
- if (this.config.type !== 'video') {
755
- return;
756
- }
757
- // Parse filter names from description
758
- const filterNames = description
759
- .split(',')
760
- .map((f) => {
761
- // Extract filter name (before = or : or whitespace)
762
- const match = /^([a-zA-Z0-9_]+)/.exec(f.trim());
763
- return match ? match[1] : null;
764
- })
765
- .filter(Boolean);
766
- for (const filterName of filterNames) {
767
- const lowLevelFilter = Filter.getByName(filterName);
768
- if (!lowLevelFilter) {
769
- // Filter will be validated later during graph parsing
770
- continue;
771
- }
772
- if (!options.hardware) {
773
- if (filterName === 'hwupload' || filterName === 'hwupload_cuda' || (lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
774
- throw new Error(`Filter '${filterName}' requires a hardware context`);
775
- }
776
- else if (filterName === 'hwdownload' && !avIsHardwarePixelFormat(this.config.pixelFormat)) {
777
- throw new Error(`Pixel Format '${this.config.pixelFormat}' is not hardware compatible`);
778
- }
779
- }
780
- }
781
- }
782
730
  /**
783
731
  * Dispose of filter.
784
732
  *
785
733
  * Implements Disposable interface for automatic cleanup.
786
- * Equivalent to calling free().
734
+ * Equivalent to calling close().
787
735
  *
788
736
  * @example
789
737
  * ```typescript
790
738
  * {
791
- * using filter = await FilterAPI.create('scale=640:480', info);
739
+ * using filter = await FilterAPI.create('scale=640:480', { ... });
792
740
  * // Use filter...
793
741
  * } // Automatically freed
794
742
  * ```
795
743
  *
796
- * @see {@link free} For manual cleanup
744
+ * @see {@link close} For manual cleanup
797
745
  */
798
746
  [Symbol.dispose]() {
799
- this.free();
747
+ this.close();
800
748
  }
801
749
  }
802
750
  //# sourceMappingURL=filter.js.map