node-av 1.0.3 → 1.1.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 (107) hide show
  1. package/README.md +5 -3
  2. package/dist/api/bitstream-filter.js +2 -1
  3. package/dist/api/bitstream-filter.js.map +1 -1
  4. package/dist/api/decoder.d.ts +10 -1
  5. package/dist/api/decoder.js +44 -25
  6. package/dist/api/decoder.js.map +1 -1
  7. package/dist/api/encoder.d.ts +19 -7
  8. package/dist/api/encoder.js +94 -130
  9. package/dist/api/encoder.js.map +1 -1
  10. package/dist/api/filter-presets.d.ts +316 -0
  11. package/dist/api/filter-presets.js +823 -0
  12. package/dist/api/filter-presets.js.map +1 -0
  13. package/dist/api/filter.d.ts +133 -173
  14. package/dist/api/filter.js +309 -393
  15. package/dist/api/filter.js.map +1 -1
  16. package/dist/api/hardware.d.ts +33 -73
  17. package/dist/api/hardware.js +86 -134
  18. package/dist/api/hardware.js.map +1 -1
  19. package/dist/api/index.d.ts +2 -1
  20. package/dist/api/index.js +1 -0
  21. package/dist/api/index.js.map +1 -1
  22. package/dist/api/io-stream.js +2 -1
  23. package/dist/api/io-stream.js.map +1 -1
  24. package/dist/api/media-input.d.ts +2 -1
  25. package/dist/api/media-input.js +2 -1
  26. package/dist/api/media-input.js.map +1 -1
  27. package/dist/api/media-output.js +2 -1
  28. package/dist/api/media-output.js.map +1 -1
  29. package/dist/api/types.d.ts +7 -1
  30. package/dist/api/utilities/audio-sample.d.ts +1 -1
  31. package/dist/api/utilities/image.d.ts +1 -1
  32. package/dist/api/utilities/media-type.d.ts +1 -1
  33. package/dist/api/utilities/pixel-format.d.ts +1 -1
  34. package/dist/api/utilities/sample-format.d.ts +1 -1
  35. package/dist/api/utilities/timestamp.d.ts +1 -1
  36. package/dist/{lib → constants}/channel-layouts.d.ts +1 -1
  37. package/dist/constants/channel-layouts.js.map +1 -0
  38. package/dist/{lib → constants}/constants.d.ts +19 -4
  39. package/dist/{lib → constants}/constants.js +15 -1
  40. package/dist/constants/constants.js.map +1 -0
  41. package/dist/constants/decoders.d.ts +609 -0
  42. package/dist/constants/decoders.js +617 -0
  43. package/dist/constants/decoders.js.map +1 -0
  44. package/dist/constants/encoders.d.ts +285 -0
  45. package/dist/constants/encoders.js +298 -0
  46. package/dist/constants/encoders.js.map +1 -0
  47. package/dist/constants/index.d.ts +4 -0
  48. package/dist/constants/index.js +5 -0
  49. package/dist/constants/index.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +2 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/lib/audio-fifo.d.ts +1 -1
  54. package/dist/lib/binding.d.ts +7 -5
  55. package/dist/lib/binding.js.map +1 -1
  56. package/dist/lib/bitstream-filter.d.ts +1 -1
  57. package/dist/lib/codec-context.d.ts +1 -1
  58. package/dist/lib/codec-parameters.d.ts +1 -1
  59. package/dist/lib/codec-parser.d.ts +1 -1
  60. package/dist/lib/codec.d.ts +131 -3
  61. package/dist/lib/codec.js +191 -0
  62. package/dist/lib/codec.js.map +1 -1
  63. package/dist/lib/dictionary.d.ts +1 -1
  64. package/dist/lib/dictionary.js +1 -1
  65. package/dist/lib/dictionary.js.map +1 -1
  66. package/dist/lib/error.d.ts +1 -1
  67. package/dist/lib/error.js.map +1 -1
  68. package/dist/lib/filter-context.d.ts +58 -1
  69. package/dist/lib/filter-context.js +72 -0
  70. package/dist/lib/filter-context.js.map +1 -1
  71. package/dist/lib/filter-graph.d.ts +1 -1
  72. package/dist/lib/filter.js +1 -1
  73. package/dist/lib/filter.js.map +1 -1
  74. package/dist/lib/format-context.d.ts +1 -1
  75. package/dist/lib/format-context.js +1 -1
  76. package/dist/lib/format-context.js.map +1 -1
  77. package/dist/lib/frame.d.ts +1 -1
  78. package/dist/lib/hardware-device-context.d.ts +1 -1
  79. package/dist/lib/hardware-frames-context.d.ts +1 -1
  80. package/dist/lib/index.d.ts +0 -2
  81. package/dist/lib/index.js +0 -3
  82. package/dist/lib/index.js.map +1 -1
  83. package/dist/lib/input-format.d.ts +1 -1
  84. package/dist/lib/io-context.d.ts +1 -1
  85. package/dist/lib/io-context.js +1 -1
  86. package/dist/lib/io-context.js.map +1 -1
  87. package/dist/lib/log.d.ts +1 -1
  88. package/dist/lib/native-types.d.ts +10 -6
  89. package/dist/lib/native-types.js +16 -0
  90. package/dist/lib/native-types.js.map +1 -1
  91. package/dist/lib/option.d.ts +1 -1
  92. package/dist/lib/option.js +1 -1
  93. package/dist/lib/option.js.map +1 -1
  94. package/dist/lib/output-format.d.ts +1 -1
  95. package/dist/lib/packet.d.ts +1 -1
  96. package/dist/lib/software-resample-context.d.ts +1 -1
  97. package/dist/lib/software-scale-context.d.ts +1 -1
  98. package/dist/lib/software-scale-context.js +1 -1
  99. package/dist/lib/software-scale-context.js.map +1 -1
  100. package/dist/lib/stream.d.ts +1 -1
  101. package/dist/lib/types.d.ts +1 -1
  102. package/dist/lib/utilities.d.ts +1 -1
  103. package/package.json +18 -19
  104. package/release_notes.md +59 -13
  105. package/dist/lib/channel-layouts.js.map +0 -1
  106. package/dist/lib/constants.js.map +0 -1
  107. /package/dist/{lib → constants}/channel-layouts.js +0 -0
@@ -1,4 +1,17 @@
1
- import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, avGetPixFmtName, avGetSampleFmtName, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, FFmpegError, Frame, Filter as LowLevelFilter, FilterGraph as LowLevelFilterGraph, FilterInOut as LowLevelFilterInOut, Stream, } from '../lib/index.js';
1
+ /**
2
+ * Filter - High-level wrapper for media filtering
3
+ *
4
+ * Implements FFmpeg CLI's filter graph behavior with proper hardware context handling.
5
+ * Uses lazy initialization for hardware inputs: graph is built when first frame arrives
6
+ * with hw_frames_ctx. For software inputs, initializes immediately.
7
+ *
8
+ * Handles filter graph creation, frame processing, and format conversion.
9
+ * Supports complex filter chains and hardware-accelerated filters.
10
+ *
11
+ * @module api/filter
12
+ */
13
+ import { AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
14
+ import { AVERROR_EAGAIN, avGetSampleFmtName, avIsHardwarePixelFormat, FFmpegError, Filter, FilterGraph, FilterInOut, Frame } from '../lib/index.js';
2
15
  /**
3
16
  * High-level filter API for media processing.
4
17
  *
@@ -6,11 +19,15 @@ import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, avGetPixFmtName, a
6
19
  * Supports both simple filter chains and complex filter graphs.
7
20
  * Handles automatic format negotiation and buffer management.
8
21
  *
22
+ * The filter graph uses lazy initialization for hardware inputs - it's built when
23
+ * the first frame arrives with hw_frames_ctx. This matches FFmpeg CLI behavior
24
+ * for proper hardware context propagation.
25
+ *
9
26
  * @example
10
27
  * ```typescript
11
- * import { FilterAPI, Frame } from 'node-av/api';
28
+ * import { FilterAPI, Frame } from '@seydx/av/api';
12
29
  *
13
- * // Create a simple video filter from a stream
30
+ * // Simple video filter from a stream
14
31
  * const videoStream = media.video();
15
32
  * const filter = await FilterAPI.create('scale=1280:720,format=yuv420p', videoStream);
16
33
  *
@@ -20,38 +37,56 @@ import { AVERROR_EAGAIN, AVERROR_EOF, AVFILTER_FLAG_HWDEVICE, avGetPixFmtName, a
20
37
  *
21
38
  * @example
22
39
  * ```typescript
23
- * // Create filter with hardware acceleration
40
+ * // Hardware acceleration (decoder -> hw filter -> encoder)
24
41
  * const hw = await HardwareContext.auto();
25
- * const filter = await FilterAPI.create('scale_vt=640:480', videoStream, {
42
+ * const decoder = await Decoder.create(stream, { hardware: hw });
43
+ * const filter = await FilterAPI.create('scale_vt=640:480', decoder.getOutputStreamInfo(), {
26
44
  * hardware: hw
27
45
  * });
28
46
  * ```
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * // Software decode -> hardware encode pipeline with hwupload
51
+ * const decoder = await Decoder.create(stream);
52
+ * const hw = await HardwareContext.auto();
53
+ * const filter = await FilterAPI.create('format=nv12,hwupload', decoder.getOutputStreamInfo(), {
54
+ * hardware: hw // Required for hwupload to create hw_frames_ctx
55
+ * });
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Hardware decode -> software encode pipeline with hwdownload
61
+ * const hw = await HardwareContext.auto();
62
+ * const decoder = await Decoder.create(stream, { hardware: hw });
63
+ * const filter = await FilterAPI.create('hwdownload,format=yuv420p', decoder.getOutputStreamInfo());
64
+ * ```
29
65
  */
30
66
  export class FilterAPI {
31
- graph;
67
+ graph = null;
32
68
  buffersrcCtx = null;
33
69
  buffersinkCtx = null;
34
70
  config;
35
71
  mediaType;
36
72
  initialized = false;
37
- needsHardware = false; // Track if this filter REQUIRES hardware
38
- hardware; // Store reference for hardware context
39
- pendingInit; // For delayed init
73
+ hardware;
74
+ description;
75
+ options;
40
76
  /**
41
77
  * Create a new Filter instance.
42
78
  *
43
- * The filter is uninitialized until setup with a filter description.
44
- * Use the static factory methods for easier creation.
45
- *
46
- * @param config - Filter configuration
47
- * @param hardware - Optional hardware context for late framesContext binding
79
+ * @param config - Stream information from input stream
80
+ * @param description - Filter graph description
81
+ * @param options - Filter options including hardware context
48
82
  * @internal
49
83
  */
50
- constructor(config, hardware) {
84
+ constructor(config, description, options) {
51
85
  this.config = config;
52
- this.hardware = hardware;
86
+ this.description = description;
87
+ this.options = options;
88
+ this.hardware = options.hardware;
53
89
  this.mediaType = config.type === 'video' ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO;
54
- this.graph = new LowLevelFilterGraph();
55
90
  }
56
91
  /**
57
92
  * Create a filter from a filter description string.
@@ -59,9 +94,14 @@ export class FilterAPI {
59
94
  * Accepts either a Stream (from MediaInput/Decoder) or StreamInfo (for raw data).
60
95
  * Automatically sets up buffer source and sink filters.
61
96
  *
62
- * Handles complex filter chains with multiple filters. Automatically detects if ANY
63
- * filter in the chain requires hardware acceleration (e.g., scale_vt in
64
- * "format=nv12,hwupload,scale_vt=640:480").
97
+ * For hardware input formats: Uses lazy initialization, waits for first frame
98
+ * with hw_frames_ctx before configuring the filter graph.
99
+ * For software formats: Initializes immediately.
100
+ *
101
+ * Hardware context handling:
102
+ * - hwupload: Requires hardware context, creates its own hw_frames_ctx
103
+ * - hwdownload: Uses hw_frames_ctx propagated from previous filters
104
+ * - Other HW filters: Use propagated hw_frames_ctx or hwupload's output
65
105
  *
66
106
  * @param description - Filter graph description (e.g., "scale=1280:720" or complex chains)
67
107
  * @param input - Stream or StreamInfo describing the input
@@ -70,6 +110,7 @@ export class FilterAPI {
70
110
  * @returns Promise resolving to configured Filter instance
71
111
  *
72
112
  * @throws {FFmpegError} If filter creation or configuration fails
113
+ * @throws {Error} If hardware filter requires hardware context but none provided
73
114
  *
74
115
  * @example
75
116
  * ```typescript
@@ -78,9 +119,10 @@ export class FilterAPI {
78
119
  *
79
120
  * // Complex filter chain with hardware
80
121
  * const hw = await HardwareContext.auto();
122
+ * const decoder = await Decoder.create(stream, { hardware: hw });
81
123
  * const filter = await FilterAPI.create(
82
- * 'format=nv12,hwupload,scale_vt=640:480,hwdownload,format=yuv420p',
83
- * videoStream,
124
+ * 'scale_vt=640:480,hwdownload,format=yuv420p',
125
+ * decoder.getOutputStreamInfo(),
84
126
  * { hardware: hw }
85
127
  * );
86
128
  *
@@ -96,106 +138,37 @@ export class FilterAPI {
96
138
  */
97
139
  static async create(description, input, options = {}) {
98
140
  let config;
99
- if (input instanceof Stream) {
100
- if (input.codecpar.codecType === AVMEDIA_TYPE_VIDEO) {
101
- config = {
102
- type: 'video',
103
- width: input.codecpar.width,
104
- height: input.codecpar.height,
105
- pixelFormat: input.codecpar.format,
106
- timeBase: input.timeBase,
107
- frameRate: input.rFrameRate,
108
- sampleAspectRatio: input.codecpar.sampleAspectRatio,
109
- };
110
- }
111
- else if (input.codecpar.codecType === AVMEDIA_TYPE_AUDIO) {
112
- config = {
113
- type: 'audio',
114
- sampleRate: input.codecpar.sampleRate,
115
- sampleFormat: input.codecpar.format,
116
- channelLayout: input.codecpar.channelLayout.mask,
117
- timeBase: input.timeBase,
118
- };
119
- }
120
- else {
121
- throw new Error('Unsupported codec type');
122
- }
123
- }
124
- else {
125
- if (input.type === 'video') {
126
- config = {
127
- type: 'video',
128
- width: input.width,
129
- height: input.height,
130
- pixelFormat: input.pixelFormat,
131
- timeBase: input.timeBase,
132
- frameRate: input.frameRate,
133
- sampleAspectRatio: input.sampleAspectRatio,
134
- };
135
- }
136
- else {
137
- config = {
138
- type: 'audio',
139
- sampleRate: input.sampleRate,
140
- sampleFormat: input.sampleFormat,
141
- channelLayout: typeof input.channelLayout === 'bigint' ? input.channelLayout : input.channelLayout.mask || 3n,
142
- timeBase: input.timeBase,
143
- };
144
- }
145
- }
146
- const filter = new FilterAPI(config, options.hardware);
147
- // Parse the entire filter chain to check if ANY filter requires hardware
148
- // Split by comma to get individual filters, handle complex chains like:
149
- // "format=nv12,hwupload,scale_vt=100:100,hwdownload,format=yuv420p"
150
- const filterNames = description
151
- .split(',')
152
- .map((f) => {
153
- // Extract filter name (before = or : or whitespace)
154
- const match = /^([a-zA-Z0-9_]+)/.exec(f.trim());
155
- return match ? match[1] : null;
156
- })
157
- .filter(Boolean);
158
- // Check if chain contains hwupload (which creates hw frames context)
159
- const hasHwDownload = filterNames.some((name) => name === 'hwdownload');
160
- const hasHwUpload = filterNames.some((name) => name === 'hwupload');
161
- // Check each filter in the chain
162
- let needsHardwareFramesContext = false;
163
- let needsHardwareDevice = false;
164
- for (const filterName of filterNames) {
165
- if (!filterName)
166
- continue;
167
- const lowLevelFilter = LowLevelFilter.getByName(filterName);
168
- if (lowLevelFilter) {
169
- // Check if this filter needs hardware
170
- if ((lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
171
- needsHardwareDevice = true;
172
- // Only non-hwupload filters need frames context from decoder
173
- if (filterName !== 'hwupload' && filterName !== 'hwdownload') {
174
- needsHardwareFramesContext = true;
175
- }
176
- }
177
- }
178
- }
179
- // If we have hwupload, we don't need hardware frames context from decoder
180
- filter.needsHardware = hasHwDownload || (needsHardwareFramesContext && !hasHwUpload);
181
- // Validation: Hardware filter MUST have HardwareContext
182
- if (needsHardwareDevice && !options.hardware) {
183
- throw new Error('Hardware filter in chain requires a hardware context. ' + 'Please provide one via options.hardware');
184
- }
185
- // Check if we can initialize immediately
186
- // Initialize if: (1) we don't need hardware, OR (2) we need hardware AND have framesContext
187
- if (!filter.needsHardware || (filter.needsHardware && options.hardware?.framesContext)) {
188
- // Can initialize now
189
- if (options.hardware?.framesContext && config.type === 'video') {
190
- config.hwFramesCtx = options.hardware.framesContext;
191
- }
192
- await filter.initialize(description, options);
193
- filter.initialized = true;
141
+ if (input.type === 'video') {
142
+ config = {
143
+ type: 'video',
144
+ width: input.width,
145
+ height: input.height,
146
+ pixelFormat: input.pixelFormat,
147
+ timeBase: input.timeBase,
148
+ frameRate: input.frameRate,
149
+ sampleAspectRatio: input.sampleAspectRatio,
150
+ };
194
151
  }
195
152
  else {
196
- // Delay initialization until first frame (hardware needed but no framesContext yet)
197
- filter.pendingInit = { description, options };
198
- }
153
+ config = {
154
+ type: 'audio',
155
+ sampleRate: input.sampleRate,
156
+ sampleFormat: input.sampleFormat,
157
+ channelLayout: input.channelLayout,
158
+ timeBase: input.timeBase,
159
+ };
160
+ }
161
+ const filter = new FilterAPI(config, description, options);
162
+ // Check if any filters in the chain require hardware context
163
+ if (config.type === 'video') {
164
+ filter.checkHardwareRequirements(description, options);
165
+ }
166
+ // For video filters, always use lazy initialization to properly detect hardware requirements
167
+ // For audio filters, initialize immediately (no hardware audio processing)
168
+ if (config.type === 'audio') {
169
+ await filter.initialize(null);
170
+ }
171
+ // For video: wait for first frame to detect if hw_frames_ctx is present
199
172
  return filter;
200
173
  }
201
174
  /**
@@ -204,11 +177,16 @@ export class FilterAPI {
204
177
  * Sends a frame through the filter graph and returns the filtered result.
205
178
  * May return null if the filter needs more input frames.
206
179
  *
180
+ * On first frame with hw_frames_ctx, initializes the filter graph (lazy initialization).
181
+ * Subsequent frames are processed normally. FFmpeg automatically propagates
182
+ * hw_frames_ctx through the filter chain.
183
+ *
207
184
  * @param frame - Input frame to filter
208
185
  *
209
- * @returns Promise resolving to filtered frame or null
186
+ * @returns Promise resolving to filtered frame or null if more input needed
210
187
  *
211
188
  * @throws {FFmpegError} If processing fails
189
+ * @throws {Error} If filter not initialized or hardware frame required but not provided
212
190
  *
213
191
  * @example
214
192
  * ```typescript
@@ -219,29 +197,9 @@ export class FilterAPI {
219
197
  * ```
220
198
  */
221
199
  async process(frame) {
222
- // Check for delayed initialization
223
- if (!this.initialized && this.pendingInit) {
224
- // Check if hardware frames context became available
225
- if (this.hardware?.framesContext && this.config.type === 'video') {
226
- this.config.hwFramesCtx = this.hardware.framesContext;
227
- // Update pixel format to match hardware frames if using hardware
228
- if (this.needsHardware) {
229
- this.config.pixelFormat = this.hardware.getHardwarePixelFormat();
230
- }
231
- // Now we can initialize
232
- await this.initialize(this.pendingInit.description, this.pendingInit.options);
233
- this.pendingInit = undefined;
234
- this.initialized = true;
235
- }
236
- else if (this.needsHardware) {
237
- throw new Error('Hardware filter requires frames context which is not yet available');
238
- }
239
- else {
240
- // Software filter or hardware not required, can initialize now
241
- await this.initialize(this.pendingInit.description, this.pendingInit.options);
242
- this.pendingInit = undefined;
243
- this.initialized = true;
244
- }
200
+ // Lazy initialization for video filters (detect hardware from first frame)
201
+ if (!this.initialized && this.config.type === 'video') {
202
+ await this.initialize(frame);
245
203
  }
246
204
  if (!this.initialized || !this.buffersrcCtx || !this.buffersinkCtx) {
247
205
  throw new Error('Filter not initialized');
@@ -419,7 +377,7 @@ export class FilterAPI {
419
377
  * ```typescript
420
378
  * for await (const filtered of filter.frames(decoder.frames())) {
421
379
  * // Process filtered frame
422
- * filtered.free(); // Must free output frame
380
+ * using _ = filtered; // Auto cleanup with using statement
423
381
  * }
424
382
  * ```
425
383
  */
@@ -453,6 +411,71 @@ export class FilterAPI {
453
411
  yield remaining;
454
412
  }
455
413
  }
414
+ /**
415
+ * Send a command to a filter in the graph.
416
+ *
417
+ * Allows runtime modification of filter parameters without recreating the graph.
418
+ * Not all filters support commands - check filter documentation.
419
+ *
420
+ * @param target - Filter name or "all" to send to all filters
421
+ * @param cmd - Command name (e.g., "volume", "hue", "brightness")
422
+ * @param arg - Command argument value
423
+ * @param flags - Optional command flags
424
+ *
425
+ * @returns Command response
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * // Change volume dynamically
430
+ * const response = filter.sendCommand('volume', 'volume', '0.5');
431
+ * if (response) {
432
+ * console.log('Volume changed successfully');
433
+ * }
434
+ * ```
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * // Enable/disable all filters at runtime
439
+ * filter.sendCommand('all', 'enable', 'expr=gte(t,10)');
440
+ * ```
441
+ */
442
+ sendCommand(target, cmd, arg, flags) {
443
+ if (!this.initialized || !this.graph) {
444
+ throw new Error('Filter not initialized');
445
+ }
446
+ const result = this.graph.sendCommand(target, cmd, arg, flags);
447
+ if (typeof result === 'number') {
448
+ FFmpegError.throwIfError(result, 'Failed to send filter command');
449
+ }
450
+ return result.response;
451
+ }
452
+ /**
453
+ * Queue a command to be executed at a specific time.
454
+ *
455
+ * Commands are executed when processing frames with matching timestamps.
456
+ * Useful for scripted filter changes synchronized with media playback.
457
+ *
458
+ * @param target - Filter name or "all" to send to all filters
459
+ * @param cmd - Command name (e.g., "volume", "hue", "brightness")
460
+ * @param arg - Command argument value
461
+ * @param ts - Timestamp when command should execute (in seconds)
462
+ * @param flags - Optional command flags
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * // Schedule volume changes at specific times
467
+ * filter.queueCommand('volume', 'volume', '0.5', 5.0); // At 5 seconds
468
+ * filter.queueCommand('volume', 'volume', '0.8', 10.0); // At 10 seconds
469
+ * filter.queueCommand('volume', 'volume', '0.2', 15.0); // At 15 seconds
470
+ * ```
471
+ */
472
+ queueCommand(target, cmd, arg, ts, flags) {
473
+ if (!this.initialized || !this.graph) {
474
+ throw new Error('Filter not initialized');
475
+ }
476
+ const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
477
+ FFmpegError.throwIfError(ret, 'Failed to queue filter command');
478
+ }
456
479
  /**
457
480
  * Get the filter graph description.
458
481
  *
@@ -468,7 +491,7 @@ export class FilterAPI {
468
491
  * ```
469
492
  */
470
493
  getGraphDescription() {
471
- if (!this.initialized) {
494
+ if (!this.initialized || !this.graph) {
472
495
  return null;
473
496
  }
474
497
  return this.graph.dump();
@@ -489,14 +512,6 @@ export class FilterAPI {
489
512
  getMediaType() {
490
513
  return this.mediaType;
491
514
  }
492
- /**
493
- * Get the filter configuration.
494
- *
495
- * @returns The filter configuration used to create this instance
496
- */
497
- getConfig() {
498
- return this.config;
499
- }
500
515
  /**
501
516
  * Free all filter resources.
502
517
  *
@@ -512,6 +527,7 @@ export class FilterAPI {
512
527
  free() {
513
528
  if (this.graph) {
514
529
  this.graph.free();
530
+ this.graph = null;
515
531
  }
516
532
  this.buffersrcCtx = null;
517
533
  this.buffersinkCtx = null;
@@ -523,34 +539,41 @@ export class FilterAPI {
523
539
  * Sets up buffer source, buffer sink, and parses the filter description.
524
540
  * Configures the graph for processing.
525
541
  *
542
+ * For hardware inputs: Uses hw_frames_ctx from first frame
543
+ * For software inputs: Initializes without hw_frames_ctx
544
+ *
526
545
  * @internal
527
546
  */
528
- async initialize(description, options) {
529
- // Allocate graph
547
+ async initialize(firstFrame) {
548
+ // Create graph
549
+ this.graph = new FilterGraph();
530
550
  this.graph.alloc();
531
551
  // Configure threading
532
- if (options.threads !== undefined) {
533
- this.graph.nbThreads = options.threads;
552
+ if (this.options.threads !== undefined) {
553
+ this.graph.nbThreads = this.options.threads;
534
554
  }
535
555
  // Configure scaler options
536
- if (options.scaleSwsOpts) {
537
- this.graph.scaleSwsOpts = options.scaleSwsOpts;
556
+ if (this.options.scaleSwsOpts) {
557
+ this.graph.scaleSwsOpts = this.options.scaleSwsOpts;
558
+ }
559
+ // Create buffer source with hw_frames_ctx if needed
560
+ if (firstFrame?.hwFramesCtx && this.config.type === 'video') {
561
+ this.createBufferSourceWithHwFrames(firstFrame);
562
+ }
563
+ else {
564
+ this.createBufferSource();
538
565
  }
539
- // Create buffer source
540
- this.createBufferSource();
541
566
  // Create buffer sink
542
567
  this.createBufferSink();
543
568
  // Parse filter description
544
- this.parseFilterDescription(description);
545
- // Set hw_device_ctx on hardware filters if we have hardware context
569
+ this.parseFilterDescription(this.description);
570
+ // Set hw_device_ctx on hardware filters
546
571
  if (this.hardware?.deviceContext) {
547
572
  const filters = this.graph.filters;
548
573
  if (filters) {
549
574
  for (const filterCtx of filters) {
550
- // Check if this filter needs hardware device context
551
575
  const filter = filterCtx.filter;
552
576
  if (filter && (filter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
553
- // Set hardware device context on this filter
554
577
  filterCtx.hwDeviceCtx = this.hardware.deviceContext;
555
578
  }
556
579
  }
@@ -562,65 +585,69 @@ export class FilterAPI {
562
585
  this.initialized = true;
563
586
  }
564
587
  /**
565
- * Create and configure the buffer source filter.
588
+ * Create buffer source with hardware frames context.
589
+ *
590
+ * @internal
591
+ */
592
+ createBufferSourceWithHwFrames(frame) {
593
+ const filterName = 'buffer';
594
+ const bufferFilter = Filter.getByName(filterName);
595
+ if (!bufferFilter) {
596
+ throw new Error(`${filterName} filter not found`);
597
+ }
598
+ // Allocate filter without args
599
+ this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
600
+ if (!this.buffersrcCtx) {
601
+ throw new Error('Failed to allocate buffer source');
602
+ }
603
+ // Set parameters including hw_frames_ctx
604
+ const cfg = this.config;
605
+ const ret = this.buffersrcCtx.buffersrcParametersSet({
606
+ width: cfg.width,
607
+ height: cfg.height,
608
+ format: cfg.pixelFormat,
609
+ timeBase: cfg.timeBase,
610
+ frameRate: cfg.frameRate,
611
+ sampleAspectRatio: cfg.sampleAspectRatio,
612
+ hwFramesCtx: frame.hwFramesCtx ?? undefined,
613
+ });
614
+ FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters');
615
+ // Initialize filter
616
+ const initRet = this.buffersrcCtx.init(null);
617
+ FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
618
+ }
619
+ /**
620
+ * Create and configure the buffer source filter without hw_frames_ctx.
566
621
  *
567
622
  * @internal
568
623
  */
569
624
  createBufferSource() {
570
625
  const filterName = this.config.type === 'video' ? 'buffer' : 'abuffer';
571
- const bufferFilter = LowLevelFilter.getByName(filterName);
626
+ const bufferFilter = Filter.getByName(filterName);
572
627
  if (!bufferFilter) {
573
628
  throw new Error(`${filterName} filter not found`);
574
629
  }
575
- // Check if we have hardware frames context for video
576
- const hasHwFrames = this.config.type === 'video' && this.config.hwFramesCtx;
577
- if (hasHwFrames) {
578
- // For hardware frames, allocate filter without initialization
579
- this.buffersrcCtx = this.graph.allocFilter(bufferFilter, 'in');
580
- if (!this.buffersrcCtx) {
581
- throw new Error('Failed to allocate buffer source');
630
+ // Build args string
631
+ let args;
632
+ if (this.config.type === 'video') {
633
+ const cfg = this.config;
634
+ args = `video_size=${cfg.width}x${cfg.height}:pix_fmt=${cfg.pixelFormat}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
635
+ if (cfg.frameRate) {
636
+ args += `:frame_rate=${cfg.frameRate.num}/${cfg.frameRate.den}`;
637
+ }
638
+ if (cfg.sampleAspectRatio) {
639
+ args += `:pixel_aspect=${cfg.sampleAspectRatio.num}/${cfg.sampleAspectRatio.den}`;
582
640
  }
583
- // Set parameters including hardware frames context (BEFORE init)
584
- const videoConfig = this.config;
585
- const ret = this.buffersrcCtx.buffersrcParametersSet({
586
- width: videoConfig.width,
587
- height: videoConfig.height,
588
- format: videoConfig.pixelFormat,
589
- timeBase: videoConfig.timeBase,
590
- frameRate: videoConfig.frameRate,
591
- sampleAspectRatio: videoConfig.sampleAspectRatio,
592
- hwFramesCtx: videoConfig.hwFramesCtx,
593
- });
594
- FFmpegError.throwIfError(ret, 'Failed to set buffer source parameters with hardware frames context');
595
- // Initialize filter AFTER setting parameters
596
- const initRet = this.buffersrcCtx.init(null);
597
- FFmpegError.throwIfError(initRet, 'Failed to initialize buffer source');
598
641
  }
599
642
  else {
600
- // Build initialization string based on media type
601
- let args;
602
- if (this.config.type === 'video') {
603
- const cfg = this.config;
604
- args = `video_size=${cfg.width}x${cfg.height}:pix_fmt=${cfg.pixelFormat}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
605
- if (cfg.frameRate) {
606
- args += `:frame_rate=${cfg.frameRate.num}/${cfg.frameRate.den}`;
607
- }
608
- if (cfg.sampleAspectRatio) {
609
- args += `:pixel_aspect=${cfg.sampleAspectRatio.num}/${cfg.sampleAspectRatio.den}`;
610
- }
611
- }
612
- else {
613
- const cfg = this.config;
614
- // Use sample format name from utilities
615
- const sampleFmtName = avGetSampleFmtName(cfg.sampleFormat);
616
- // Handle invalid channel layout (0) by using stereo as default
617
- const channelLayout = cfg.channelLayout === 0n ? 'stereo' : cfg.channelLayout.toString();
618
- args = `sample_rate=${cfg.sampleRate}:sample_fmt=${sampleFmtName}:channel_layout=${channelLayout}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
619
- }
620
- this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
621
- if (!this.buffersrcCtx) {
622
- throw new Error('Failed to create buffer source');
623
- }
643
+ const cfg = this.config;
644
+ const sampleFmtName = avGetSampleFmtName(cfg.sampleFormat);
645
+ const channelLayout = cfg.channelLayout.mask === 0n ? 'stereo' : cfg.channelLayout.mask.toString();
646
+ args = `sample_rate=${cfg.sampleRate}:sample_fmt=${sampleFmtName}:channel_layout=${channelLayout}:time_base=${cfg.timeBase.num}/${cfg.timeBase.den}`;
647
+ }
648
+ this.buffersrcCtx = this.graph.createFilter(bufferFilter, 'in', args);
649
+ if (!this.buffersrcCtx) {
650
+ throw new Error('Failed to create buffer source');
624
651
  }
625
652
  }
626
653
  /**
@@ -629,12 +656,14 @@ export class FilterAPI {
629
656
  * @internal
630
657
  */
631
658
  createBufferSink() {
659
+ if (!this.graph) {
660
+ throw new Error('Filter graph not initialized');
661
+ }
632
662
  const filterName = this.config.type === 'video' ? 'buffersink' : 'abuffersink';
633
- const sinkFilter = LowLevelFilter.getByName(filterName);
663
+ const sinkFilter = Filter.getByName(filterName);
634
664
  if (!sinkFilter) {
635
665
  throw new Error(`${filterName} filter not found`);
636
666
  }
637
- // Create sink filter - no automatic format conversion
638
667
  this.buffersinkCtx = this.graph.createFilter(sinkFilter, 'out', null);
639
668
  if (!this.buffersinkCtx) {
640
669
  throw new Error('Failed to create buffer sink');
@@ -646,6 +675,9 @@ export class FilterAPI {
646
675
  * @internal
647
676
  */
648
677
  parseFilterDescription(description) {
678
+ if (!this.graph) {
679
+ throw new Error('Filter graph not initialized');
680
+ }
649
681
  if (!this.buffersrcCtx || !this.buffersinkCtx) {
650
682
  throw new Error('Buffer filters not initialized');
651
683
  }
@@ -657,12 +689,12 @@ export class FilterAPI {
657
689
  return;
658
690
  }
659
691
  // Set up inputs and outputs for parsing
660
- const outputs = new LowLevelFilterInOut();
692
+ const outputs = new FilterInOut();
661
693
  outputs.alloc();
662
694
  outputs.name = 'in';
663
695
  outputs.filterCtx = this.buffersrcCtx;
664
696
  outputs.padIdx = 0;
665
- const inputs = new LowLevelFilterInOut();
697
+ const inputs = new FilterInOut();
666
698
  inputs.alloc();
667
699
  inputs.name = 'out';
668
700
  inputs.filterCtx = this.buffersinkCtx;
@@ -670,80 +702,74 @@ export class FilterAPI {
670
702
  // Parse the filter graph
671
703
  const ret = this.graph.parsePtr(description, inputs, outputs);
672
704
  FFmpegError.throwIfError(ret, 'Failed to parse filter description');
673
- // Clean up FilterInOut structures
705
+ // Clean up
674
706
  inputs.free();
675
707
  outputs.free();
676
708
  }
677
709
  /**
678
- * Send a command to a filter in the graph.
710
+ * Check if hardware context is required for the filter chain.
679
711
  *
680
- * Allows runtime modification of filter parameters without recreating the graph.
681
- * Not all filters support commands - check filter documentation.
712
+ * Validates that hardware context is provided when needed:
713
+ * - hwupload: Always requires hardware context
714
+ * - Hardware filters (AVFILTER_FLAG_HWDEVICE): Recommend hardware context
715
+ * - hwdownload: Warns if input is not hardware format
682
716
  *
683
- * @param target - Filter name or "all" to send to all filters
684
- * @param cmd - Command name (e.g., "volume", "hue", "brightness")
685
- * @param arg - Command argument value
686
- * @param flags - Optional command flags
687
- *
688
- * @returns Command response
689
- *
690
- * @example
691
- * ```typescript
692
- * // Change volume dynamically
693
- * const response = filter.sendCommand('volume', 'volume', '0.5');
694
- * if (response) {
695
- * console.log('Volume changed successfully');
696
- * }
697
- * ```
698
- *
699
- * @example
700
- * ```typescript
701
- * // Enable/disable all filters at runtime
702
- * filter.sendCommand('all', 'enable', 'expr=gte(t,10)');
703
- * ```
717
+ * @internal
704
718
  */
705
- sendCommand(target, cmd, arg, flags) {
706
- if (!this.initialized) {
707
- throw new Error('Filter not initialized');
708
- }
709
- const result = this.graph.sendCommand(target, cmd, arg, flags);
710
- if (typeof result === 'number') {
711
- FFmpegError.throwIfError(result, 'Failed to send filter command');
719
+ checkHardwareRequirements(description, options) {
720
+ if (this.config.type !== 'video') {
721
+ return;
712
722
  }
713
- return result.response;
714
- }
715
- /**
716
- * Queue a command to be executed at a specific time.
717
- *
718
- * Commands are executed when processing frames with matching timestamps.
719
- * Useful for scripted filter changes synchronized with media playback.
720
- *
721
- * @param target - Filter name or "all" to send to all filters
722
- * @param cmd - Command name (e.g., "volume", "hue", "brightness")
723
- * @param arg - Command argument value
724
- * @param ts - Timestamp when command should execute (in seconds)
725
- * @param flags - Optional command flags
726
- *
727
- * @example
728
- * ```typescript
729
- * // Schedule volume changes at specific times
730
- * filter.queueCommand('volume', 'volume', '0.5', 5.0); // At 5 seconds
731
- * filter.queueCommand('volume', 'volume', '0.8', 10.0); // At 10 seconds
732
- * filter.queueCommand('volume', 'volume', '0.2', 15.0); // At 15 seconds
733
- * ```
734
- *
735
- * @example
736
- * ```typescript
737
- * // Fade effect at specific timestamp
738
- * filter.queueCommand('fade', 'alpha', '0.5', 30.0);
739
- * ```
740
- */
741
- queueCommand(target, cmd, arg, ts, flags) {
742
- if (!this.initialized) {
743
- throw new Error('Filter not initialized');
723
+ // Parse filter names from description
724
+ const filterNames = description
725
+ .split(',')
726
+ .map((f) => {
727
+ // Extract filter name (before = or : or whitespace)
728
+ const match = /^([a-zA-Z0-9_]+)/.exec(f.trim());
729
+ return match ? match[1] : null;
730
+ })
731
+ .filter(Boolean);
732
+ for (const filterName of filterNames) {
733
+ const lowLevelFilter = Filter.getByName(filterName);
734
+ if (!lowLevelFilter) {
735
+ // Filter will be validated later during graph parsing
736
+ continue;
737
+ }
738
+ if (!options.hardware) {
739
+ if (filterName === 'hwupload' || filterName === 'hwupload_cuda' || (lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
740
+ throw new Error(`Filter '${filterName}' requires a hardware context`);
741
+ }
742
+ else if (filterName === 'hwdownload' && !avIsHardwarePixelFormat(this.config.pixelFormat)) {
743
+ throw new Error(`Pixel Format '${this.config.pixelFormat}' is not hardware compatible`);
744
+ }
745
+ }
746
+ // // Check if this is hwupload - always needs hardware context
747
+ // if (filterName === 'hwupload' || filterName === 'hwupload_cuda') {
748
+ // if (!options.hardware) {
749
+ // throw new Error(`Filter '${filterName}' requires a hardware context`);
750
+ // }
751
+ // } else if (filterName === 'hwdownload') {
752
+ // // Check if this is hwdownload - warn if input is not hardware format
753
+ // if (this.config.type === 'video' && !avIsHardwarePixelFormat(this.config.pixelFormat)) {
754
+ // // prettier-ignore
755
+ // console.warn(
756
+ // `Warning: 'hwdownload' filter used with software input format (${this.config.pixelFormat}). ` +
757
+ // 'This will likely fail at runtime. hwdownload expects hardware frames as input. ' +
758
+ // 'Consider removing hwdownload from your filter chain or ensuring hardware input.',
759
+ // );
760
+ // }
761
+ // } else if ((lowLevelFilter.flags & AVFILTER_FLAG_HWDEVICE) !== 0) {
762
+ // // Check if this is a hardware filter
763
+ // if (!options.hardware) {
764
+ // // prettier-ignore
765
+ // console.warn(
766
+ // `Warning: Hardware filter '${filterName}' used without hardware context. ` +
767
+ // "This may work if hw_frames_ctx is propagated from input, but it's recommended " +
768
+ // 'to pass { hardware: HardwareContext } in filter options.',
769
+ // );
770
+ // }
771
+ // }
744
772
  }
745
- const ret = this.graph.queueCommand(target, cmd, arg, ts, flags);
746
- FFmpegError.throwIfError(ret, 'Failed to queue filter command');
747
773
  }
748
774
  /**
749
775
  * Dispose of the filter.
@@ -763,114 +789,4 @@ export class FilterAPI {
763
789
  this.free();
764
790
  }
765
791
  }
766
- /**
767
- * Common filter presets for convenience.
768
- *
769
- * Provides pre-defined filter strings for common operations.
770
- * Can be used with Filter.create() for quick setup.
771
- *
772
- * @example
773
- * ```typescript
774
- * const filter = await Filter.create(
775
- * FilterPresets.scale(1280, 720),
776
- * config
777
- * );
778
- * ```
779
- */
780
- export class FilterPresets {
781
- /**
782
- * Scale video to specified dimensions.
783
- */
784
- static scale(width, height, flags) {
785
- const base = `scale=${width}:${height}`;
786
- return flags ? `${base}:flags=${flags}` : base;
787
- }
788
- /**
789
- * Crop video to specified dimensions.
790
- */
791
- static crop(width, height, x = 0, y = 0) {
792
- return `crop=${width}:${height}:${x}:${y}`;
793
- }
794
- /**
795
- * Change frame rate.
796
- */
797
- static fps(fps) {
798
- return `fps=${fps}`;
799
- }
800
- /**
801
- * Convert pixel format.
802
- * Can accept either format name string or AVPixelFormat enum.
803
- */
804
- static format(pixelFormat) {
805
- const formatName = typeof pixelFormat === 'string' ? pixelFormat : (avGetPixFmtName(pixelFormat) ?? 'yuv420p');
806
- return `format=${formatName}`;
807
- }
808
- /**
809
- * Rotate video by angle.
810
- */
811
- static rotate(angle) {
812
- return `rotate=${angle}*PI/180`;
813
- }
814
- /**
815
- * Flip video horizontally.
816
- */
817
- static hflip() {
818
- return 'hflip';
819
- }
820
- /**
821
- * Flip video vertically.
822
- */
823
- static vflip() {
824
- return 'vflip';
825
- }
826
- /**
827
- * Apply fade effect.
828
- */
829
- static fade(type, start, duration) {
830
- return `fade=t=${type}:st=${start}:d=${duration}`;
831
- }
832
- /**
833
- * Overlay one video on another.
834
- */
835
- static overlay(x = 0, y = 0) {
836
- return `overlay=${x}:${y}`;
837
- }
838
- /**
839
- * Adjust audio volume.
840
- */
841
- static volume(factor) {
842
- return `volume=${factor}`;
843
- }
844
- /**
845
- * Convert audio sample format.
846
- * Can accept either format name string or AVSampleFormat enum.
847
- */
848
- static aformat(sampleFormat, sampleRate, channelLayout) {
849
- const formatName = typeof sampleFormat === 'string' ? sampleFormat : (avGetSampleFmtName(sampleFormat) ?? 's16');
850
- let filter = `aformat=sample_fmts=${formatName}`;
851
- if (sampleRate)
852
- filter += `:sample_rates=${sampleRate}`;
853
- if (channelLayout)
854
- filter += `:channel_layouts=${channelLayout}`;
855
- return filter;
856
- }
857
- /**
858
- * Change audio tempo without changing pitch.
859
- */
860
- static atempo(factor) {
861
- return `atempo=${factor}`;
862
- }
863
- /**
864
- * Apply audio fade.
865
- */
866
- static afade(type, start, duration) {
867
- return `afade=t=${type}:st=${start}:d=${duration}`;
868
- }
869
- /**
870
- * Mix multiple audio streams.
871
- */
872
- static amix(inputs = 2, duration = 'longest') {
873
- return `amix=inputs=${inputs}:duration=${duration}`;
874
- }
875
- }
876
792
  //# sourceMappingURL=filter.js.map