node-av 1.3.0 → 2.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 (127) hide show
  1. package/README.md +47 -40
  2. package/binding.gyp +12 -0
  3. package/dist/api/bitstream-filter.d.ts +134 -2
  4. package/dist/api/bitstream-filter.js +200 -2
  5. package/dist/api/bitstream-filter.js.map +1 -1
  6. package/dist/api/decoder.d.ts +261 -105
  7. package/dist/api/decoder.js +384 -171
  8. package/dist/api/decoder.js.map +1 -1
  9. package/dist/api/encoder.d.ts +338 -74
  10. package/dist/api/encoder.js +546 -188
  11. package/dist/api/encoder.js.map +1 -1
  12. package/dist/api/filter-presets.d.ts +479 -1513
  13. package/dist/api/filter-presets.js +1044 -2005
  14. package/dist/api/filter-presets.js.map +1 -1
  15. package/dist/api/filter.d.ts +370 -150
  16. package/dist/api/filter.js +647 -364
  17. package/dist/api/filter.js.map +1 -1
  18. package/dist/api/hardware.d.ts +25 -31
  19. package/dist/api/hardware.js +36 -70
  20. package/dist/api/hardware.js.map +1 -1
  21. package/dist/api/index.d.ts +1 -1
  22. package/dist/api/index.js +1 -1
  23. package/dist/api/index.js.map +1 -1
  24. package/dist/api/io-stream.d.ts +6 -0
  25. package/dist/api/io-stream.js +2 -0
  26. package/dist/api/io-stream.js.map +1 -1
  27. package/dist/api/media-input.d.ts +208 -2
  28. package/dist/api/media-input.js +356 -8
  29. package/dist/api/media-input.js.map +1 -1
  30. package/dist/api/media-output.d.ts +142 -104
  31. package/dist/api/media-output.js +446 -179
  32. package/dist/api/media-output.js.map +1 -1
  33. package/dist/api/pipeline.d.ts +82 -17
  34. package/dist/api/pipeline.js +80 -42
  35. package/dist/api/pipeline.js.map +1 -1
  36. package/dist/api/types.d.ts +24 -57
  37. package/dist/api/utils.js +2 -0
  38. package/dist/api/utils.js.map +1 -1
  39. package/dist/lib/audio-fifo.d.ts +103 -0
  40. package/dist/lib/audio-fifo.js +109 -0
  41. package/dist/lib/audio-fifo.js.map +1 -1
  42. package/dist/lib/binding.d.ts +1 -0
  43. package/dist/lib/binding.js.map +1 -1
  44. package/dist/lib/bitstream-filter-context.d.ts +79 -0
  45. package/dist/lib/bitstream-filter-context.js +83 -0
  46. package/dist/lib/bitstream-filter-context.js.map +1 -1
  47. package/dist/lib/bitstream-filter.d.ts +2 -0
  48. package/dist/lib/bitstream-filter.js +2 -0
  49. package/dist/lib/bitstream-filter.js.map +1 -1
  50. package/dist/lib/codec-context.d.ts +168 -0
  51. package/dist/lib/codec-context.js +178 -0
  52. package/dist/lib/codec-context.js.map +1 -1
  53. package/dist/lib/codec-parameters.d.ts +3 -0
  54. package/dist/lib/codec-parameters.js +3 -0
  55. package/dist/lib/codec-parameters.js.map +1 -1
  56. package/dist/lib/codec-parser.d.ts +6 -0
  57. package/dist/lib/codec-parser.js +6 -0
  58. package/dist/lib/codec-parser.js.map +1 -1
  59. package/dist/lib/codec.d.ts +12 -0
  60. package/dist/lib/codec.js +12 -0
  61. package/dist/lib/codec.js.map +1 -1
  62. package/dist/lib/dictionary.d.ts +18 -2
  63. package/dist/lib/dictionary.js +18 -2
  64. package/dist/lib/dictionary.js.map +1 -1
  65. package/dist/lib/error.d.ts +8 -0
  66. package/dist/lib/error.js +9 -0
  67. package/dist/lib/error.js.map +1 -1
  68. package/dist/lib/filter-context.d.ts +119 -2
  69. package/dist/lib/filter-context.js +119 -0
  70. package/dist/lib/filter-context.js.map +1 -1
  71. package/dist/lib/filter-graph.d.ts +80 -0
  72. package/dist/lib/filter-graph.js +84 -0
  73. package/dist/lib/filter-graph.js.map +1 -1
  74. package/dist/lib/filter-inout.d.ts +1 -0
  75. package/dist/lib/filter-inout.js +1 -0
  76. package/dist/lib/filter-inout.js.map +1 -1
  77. package/dist/lib/filter.d.ts +2 -0
  78. package/dist/lib/filter.js +2 -0
  79. package/dist/lib/filter.js.map +1 -1
  80. package/dist/lib/format-context.d.ts +356 -20
  81. package/dist/lib/format-context.js +375 -23
  82. package/dist/lib/format-context.js.map +1 -1
  83. package/dist/lib/frame.d.ts +84 -1
  84. package/dist/lib/frame.js +96 -0
  85. package/dist/lib/frame.js.map +1 -1
  86. package/dist/lib/hardware-device-context.d.ts +8 -0
  87. package/dist/lib/hardware-device-context.js +8 -0
  88. package/dist/lib/hardware-device-context.js.map +1 -1
  89. package/dist/lib/hardware-frames-context.d.ts +55 -0
  90. package/dist/lib/hardware-frames-context.js +57 -0
  91. package/dist/lib/hardware-frames-context.js.map +1 -1
  92. package/dist/lib/input-format.d.ts +43 -3
  93. package/dist/lib/input-format.js +48 -0
  94. package/dist/lib/input-format.js.map +1 -1
  95. package/dist/lib/io-context.d.ts +212 -0
  96. package/dist/lib/io-context.js +228 -0
  97. package/dist/lib/io-context.js.map +1 -1
  98. package/dist/lib/log.d.ts +2 -0
  99. package/dist/lib/log.js +2 -0
  100. package/dist/lib/log.js.map +1 -1
  101. package/dist/lib/native-types.d.ts +39 -1
  102. package/dist/lib/option.d.ts +90 -0
  103. package/dist/lib/option.js +97 -0
  104. package/dist/lib/option.js.map +1 -1
  105. package/dist/lib/output-format.d.ts +4 -0
  106. package/dist/lib/output-format.js +4 -0
  107. package/dist/lib/output-format.js.map +1 -1
  108. package/dist/lib/packet.d.ts +7 -0
  109. package/dist/lib/packet.js +7 -0
  110. package/dist/lib/packet.js.map +1 -1
  111. package/dist/lib/rational.d.ts +1 -0
  112. package/dist/lib/rational.js +1 -0
  113. package/dist/lib/rational.js.map +1 -1
  114. package/dist/lib/software-resample-context.d.ts +64 -0
  115. package/dist/lib/software-resample-context.js +66 -0
  116. package/dist/lib/software-resample-context.js.map +1 -1
  117. package/dist/lib/software-scale-context.d.ts +98 -0
  118. package/dist/lib/software-scale-context.js +102 -0
  119. package/dist/lib/software-scale-context.js.map +1 -1
  120. package/dist/lib/stream.d.ts +1 -0
  121. package/dist/lib/stream.js +1 -0
  122. package/dist/lib/stream.js.map +1 -1
  123. package/dist/lib/utilities.d.ts +60 -0
  124. package/dist/lib/utilities.js +60 -0
  125. package/dist/lib/utilities.js.map +1 -1
  126. package/package.json +18 -18
  127. package/release_notes.md +0 -29
@@ -1,5 +1,5 @@
1
- import { AVERROR_EAGAIN, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
2
- import { avGetPixFmtName, avGetSampleFmtName, Codec, CodecContext, FFmpegError, Packet, Rational } from '../lib/index.js';
1
+ import { AVERROR_EAGAIN, AVERROR_EOF } from '../constants/constants.js';
2
+ import { Codec, CodecContext, Dictionary, FFmpegError, Packet, Rational } from '../lib/index.js';
3
3
  import { parseBitrate } from './utils.js';
4
4
  /**
5
5
  * High-level encoder for audio and video streams.
@@ -37,17 +37,18 @@ import { parseBitrate } from './utils.js';
37
37
  *
38
38
  * @example
39
39
  * ```typescript
40
- * // Hardware-accelerated encoding
40
+ * // Hardware-accelerated encoding with lazy initialization
41
41
  * import { HardwareContext } from 'node-av/api';
42
- * import { AV_HWDEVICE_TYPE_CUDA } from 'node-av/constants';
42
+ * import { FF_ENCODER_H264_VIDEOTOOLBOX } from 'node-av/constants';
43
43
  *
44
- * const hw = HardwareContext.create(AV_HWDEVICE_TYPE_CUDA);
45
- * const encoder = await Encoder.create('h264_nvenc', streamInfo, {
46
- * hardware: hw,
44
+ * const hw = HardwareContext.auto();
45
+ * const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
46
+ * const encoder = await Encoder.create(encoderCodec, {
47
+ * timeBase: video.timeBase,
47
48
  * bitrate: '10M'
48
49
  * });
49
50
  *
50
- * // Frames with hw_frames_ctx will be encoded on GPU
51
+ * // Hardware context will be detected from first frame's hw_frames_ctx
51
52
  * for await (const packet of encoder.packets(frames)) {
52
53
  * await output.writePacket(packet);
53
54
  * packet.free();
@@ -62,18 +63,22 @@ export class Encoder {
62
63
  codecContext;
63
64
  packet;
64
65
  codec;
65
- isOpen = true;
66
- hardware;
66
+ initialized = false;
67
+ isClosed = false;
68
+ opts;
67
69
  /**
68
70
  * @param codecContext - Configured codec context
71
+ *
69
72
  * @param codec - Encoder codec
70
- * @param hardware - Optional hardware context
73
+ *
74
+ * @param opts - Encoder options as Dictionary
75
+ *
71
76
  * @internal
72
77
  */
73
- constructor(codecContext, codec, hardware) {
78
+ constructor(codecContext, codec, opts) {
74
79
  this.codecContext = codecContext;
75
80
  this.codec = codec;
76
- this.hardware = hardware;
81
+ this.opts = opts;
77
82
  this.packet = new Packet();
78
83
  this.packet.alloc();
79
84
  }
@@ -81,25 +86,26 @@ export class Encoder {
81
86
  * Create an encoder with specified codec and options.
82
87
  *
83
88
  * Initializes an encoder with the appropriate codec and configuration.
84
- * Automatically configures parameters based on input stream info.
85
- * Handles hardware acceleration setup if provided.
89
+ * Uses lazy initialization - encoder is opened when first frame is received.
90
+ * Hardware context will be automatically detected from first frame if not provided.
86
91
  *
87
92
  * Direct mapping to avcodec_find_encoder_by_name() or avcodec_find_encoder().
88
93
  *
89
94
  * @param encoderCodec - Codec name, ID, or instance to use for encoding
90
- * @param input - Stream information to configure encoder
91
- * @param options - Encoder configuration options
95
+ *
96
+ * @param options - Encoder configuration options including required timeBase
97
+ *
92
98
  * @returns Configured encoder instance
93
99
  *
94
- * @throws {Error} If encoder not found or unsupported format
100
+ * @throws {Error} If encoder not found or timeBase not provided
95
101
  *
96
- * @throws {FFmpegError} If codec initialization fails
102
+ * @throws {FFmpegError} If codec allocation fails
97
103
  *
98
104
  * @example
99
105
  * ```typescript
100
106
  * // From decoder stream info
101
- * const streamInfo = decoder.getOutputStreamInfo();
102
- * const encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo, {
107
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
108
+ * timeBase: video.timeBase,
103
109
  * bitrate: '5M',
104
110
  * gopSize: 60,
105
111
  * options: {
@@ -113,12 +119,7 @@ export class Encoder {
113
119
  * ```typescript
114
120
  * // With custom stream info
115
121
  * const encoder = await Encoder.create(FF_ENCODER_AAC, {
116
- * type: 'audio',
117
- * sampleRate: 48000,
118
- * sampleFormat: AV_SAMPLE_FMT_FLTP,
119
- * channelLayout: AV_CH_LAYOUT_STEREO,
120
- * timeBase: { num: 1, den: 48000 }
121
- * }, {
122
+ * timeBase: audio.timeBase,
122
123
  * bitrate: '192k'
123
124
  * });
124
125
  * ```
@@ -127,16 +128,16 @@ export class Encoder {
127
128
  * ```typescript
128
129
  * // Hardware encoder
129
130
  * const hw = HardwareContext.auto();
130
- * const encoder = await Encoder.create('hevc_videotoolbox', streamInfo, {
131
- * hardware: hw,
131
+ * const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
132
+ * const encoder = await Encoder.create(encoderCodec, {
133
+ * timeBase: video.timeBase,
132
134
  * bitrate: '8M'
133
135
  * });
134
136
  * ```
135
137
  *
136
- * @see {@link Decoder.getOutputStreamInfo} For stream info source
137
138
  * @see {@link EncoderOptions} For configuration options
138
139
  */
139
- static async create(encoderCodec, input, options = {}) {
140
+ static async create(encoderCodec, options) {
140
141
  let codec = null;
141
142
  let codecName = '';
142
143
  if (encoderCodec instanceof Codec) {
@@ -157,52 +158,6 @@ export class Encoder {
157
158
  // Allocate codec context
158
159
  const codecContext = new CodecContext();
159
160
  codecContext.allocContext3(codec);
160
- // It's StreamInfo - apply manually
161
- if (input.type === 'video' && codec.type === AVMEDIA_TYPE_VIDEO) {
162
- const videoInfo = input;
163
- const codecPixelformats = codec.pixelFormats;
164
- if (codecPixelformats && !codecPixelformats.includes(videoInfo.pixelFormat)) {
165
- codecContext.freeContext();
166
- const pixelFormatName = avGetPixFmtName(videoInfo.pixelFormat) ?? 'unknown';
167
- const codecPixFmtNames = codecPixelformats.map(avGetPixFmtName).filter(Boolean).join(', ');
168
- throw new Error(`Unsupported pixel format for '${codecName}' encoder: ${pixelFormatName}! Supported formats: ${codecPixFmtNames}`);
169
- }
170
- codecContext.width = videoInfo.width;
171
- codecContext.height = videoInfo.height;
172
- codecContext.pixelFormat = videoInfo.pixelFormat;
173
- // Set pkt_timebase and timeBase to input timebase
174
- codecContext.pktTimebase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
175
- codecContext.timeBase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
176
- if (videoInfo.frameRate) {
177
- codecContext.framerate = new Rational(videoInfo.frameRate.num, videoInfo.frameRate.den);
178
- }
179
- if (videoInfo.sampleAspectRatio) {
180
- codecContext.sampleAspectRatio = new Rational(videoInfo.sampleAspectRatio.num, videoInfo.sampleAspectRatio.den);
181
- }
182
- }
183
- else if (input.type === 'audio' && codec.type === AVMEDIA_TYPE_AUDIO) {
184
- const audioInfo = input;
185
- const codecSampleFormats = codec.sampleFormats;
186
- if (codecSampleFormats && !codecSampleFormats.includes(audioInfo.sampleFormat)) {
187
- codecContext.freeContext();
188
- const sampleFormatName = avGetSampleFmtName(audioInfo.sampleFormat) ?? 'unknown';
189
- const supportedFormats = codecSampleFormats.map(avGetSampleFmtName).filter(Boolean).join(', ');
190
- throw new Error(`Unsupported sample format for '${codecName}' encoder: ${sampleFormatName}! Supported formats: ${supportedFormats}`);
191
- }
192
- codecContext.sampleRate = audioInfo.sampleRate;
193
- codecContext.sampleFormat = audioInfo.sampleFormat;
194
- codecContext.channelLayout = audioInfo.channelLayout;
195
- // Set both pkt_timebase and timeBase for audio
196
- codecContext.pktTimebase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
197
- codecContext.timeBase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
198
- if (audioInfo.frameSize) {
199
- codecContext.frameSize = audioInfo.frameSize;
200
- }
201
- }
202
- else {
203
- codecContext.freeContext();
204
- throw new Error(`Unsupported codec type for encoder! Input type: ${input.type}, Codec type: ${codec.type}`);
205
- }
206
161
  // Apply encoder-specific options
207
162
  if (options.gopSize !== undefined) {
208
163
  codecContext.gopSize = options.gopSize;
@@ -215,32 +170,28 @@ export class Encoder {
215
170
  const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
216
171
  codecContext.bitRate = bitrate;
217
172
  }
218
- if (options.threads !== undefined) {
219
- codecContext.threadCount = options.threads;
173
+ if (options.minRate !== undefined) {
174
+ const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
175
+ codecContext.rcMinRate = minRate;
220
176
  }
221
- // Override timeBase if explicitly specified in options
222
- if (options.timeBase) {
223
- codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
177
+ if (options.maxRate !== undefined) {
178
+ const maxRate = typeof options.maxRate === 'string' ? parseBitrate(options.maxRate) : BigInt(options.maxRate);
179
+ codecContext.rcMaxRate = maxRate;
224
180
  }
225
- // Apply codec-specific options via AVOptions
226
- if (options.options) {
227
- for (const [key, value] of Object.entries(options.options)) {
228
- codecContext.setOption(key, value.toString());
229
- }
181
+ if (options.bufSize !== undefined) {
182
+ const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
183
+ codecContext.rcBufferSize = Number(bufSize);
230
184
  }
231
- const isHWEncoder = codec.isHardwareAcceleratedEncoder();
232
- if (isHWEncoder && !options.hardware) {
233
- codecContext.freeContext();
234
- throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
185
+ if (options.threads !== undefined) {
186
+ codecContext.threadCount = options.threads;
235
187
  }
236
- // Open codec
237
- const openRet = await codecContext.open2(codec, null);
238
- if (openRet < 0) {
239
- codecContext.freeContext();
240
- FFmpegError.throwIfError(openRet, 'Failed to open encoder');
188
+ codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
189
+ codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
190
+ if (options.frameRate) {
191
+ codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
241
192
  }
242
- const encoder = new Encoder(codecContext, codec, isHWEncoder ? options.hardware : undefined);
243
- return encoder;
193
+ const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
194
+ return new Encoder(codecContext, codec, opts);
244
195
  }
245
196
  /**
246
197
  * Check if encoder is open.
@@ -253,7 +204,25 @@ export class Encoder {
253
204
  * ```
254
205
  */
255
206
  get isEncoderOpen() {
256
- return this.isOpen;
207
+ return !this.isClosed;
208
+ }
209
+ /**
210
+ * Check if encoder has been initialized.
211
+ *
212
+ * Returns true after first frame has been processed and encoder opened.
213
+ * Useful for checking if encoder has received frame properties.
214
+ *
215
+ * @returns true if encoder has been initialized with frame data
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * if (!encoder.isEncoderInitialized) {
220
+ * console.log('Encoder will initialize on first frame');
221
+ * }
222
+ * ```
223
+ */
224
+ get isEncoderInitialized() {
225
+ return this.initialized;
257
226
  }
258
227
  /**
259
228
  * Check if encoder uses hardware acceleration.
@@ -270,18 +239,34 @@ export class Encoder {
270
239
  * @see {@link HardwareContext} For hardware setup
271
240
  */
272
241
  isHardware() {
273
- return !!this.hardware;
242
+ return this.codec.isHardwareAcceleratedEncoder();
243
+ }
244
+ /**
245
+ * Check if encoder is ready for processing.
246
+ *
247
+ * @returns true if initialized and ready
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * if (encoder.isReady()) {
252
+ * const packet = await encoder.encode(frame);
253
+ * }
254
+ * ```
255
+ */
256
+ isReady() {
257
+ return this.initialized && !this.isClosed;
274
258
  }
275
259
  /**
276
260
  * Encode a frame to a packet.
277
261
  *
278
262
  * Sends a frame to the encoder and attempts to receive an encoded packet.
263
+ * On first frame, automatically initializes encoder with frame properties.
279
264
  * Handles internal buffering - may return null if more frames needed.
280
- * Automatically manages encoder state and hardware context binding.
281
265
  *
282
266
  * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
283
267
  *
284
268
  * @param frame - Raw frame to encode (or null to flush)
269
+ *
285
270
  * @returns Encoded packet or null if more data needed
286
271
  *
287
272
  * @throws {Error} If encoder is closed
@@ -315,21 +300,96 @@ export class Encoder {
315
300
  * @see {@link flush} For end-of-stream handling
316
301
  */
317
302
  async encode(frame) {
318
- if (!this.isOpen) {
303
+ if (this.isClosed) {
304
+ if (!frame) {
305
+ return null;
306
+ }
319
307
  throw new Error('Encoder is closed');
320
308
  }
321
- // Late binding of hw_frames_ctx for hardware encoders
322
- // Hardware encoders get hw_frames_ctx from the frames they receive
323
- if (this.hardware && frame?.hwFramesCtx && !this.codecContext.hwFramesCtx) {
324
- // Use the hw_frames_ctx from the frame
325
- this.codecContext.hwFramesCtx = frame.hwFramesCtx;
326
- this.codecContext.pixelFormat = this.hardware.devicePixelFormat;
309
+ // Open encoder if not already done
310
+ if (!this.initialized) {
311
+ if (!frame) {
312
+ return null;
313
+ }
314
+ await this.initialize(frame);
327
315
  }
328
316
  // Send frame to encoder
329
317
  const sendRet = await this.codecContext.sendFrame(frame);
330
318
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
331
319
  // Encoder might be full, try to receive first
332
- const packet = await this.receivePacket();
320
+ const packet = await this.receive();
321
+ if (packet)
322
+ return packet;
323
+ // If still failing, it's an error
324
+ if (sendRet !== AVERROR_EAGAIN) {
325
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
326
+ }
327
+ }
328
+ // Try to receive packet
329
+ return await this.receive();
330
+ }
331
+ /**
332
+ * Encode a frame to a packet synchronously.
333
+ * Synchronous version of encode.
334
+ *
335
+ * Sends a frame to the encoder and attempts to receive an encoded packet.
336
+ * On first frame, automatically initializes encoder with frame properties.
337
+ * Handles internal buffering - may return null if more frames needed.
338
+ *
339
+ * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
340
+ *
341
+ * @param frame - Raw frame to encode (or null to flush)
342
+ *
343
+ * @returns Encoded packet or null if more data needed
344
+ *
345
+ * @throws {Error} If encoder is closed
346
+ *
347
+ * @throws {FFmpegError} If encoding fails
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const packet = encoder.encodeSync(frame);
352
+ * if (packet) {
353
+ * console.log(`Encoded packet with PTS: ${packet.pts}`);
354
+ * output.writePacketSync(packet);
355
+ * packet.free();
356
+ * }
357
+ * ```
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * // Encode loop
362
+ * for (const frame of decoder.framesSync(packets)) {
363
+ * const packet = encoder.encodeSync(frame);
364
+ * if (packet) {
365
+ * output.writePacketSync(packet);
366
+ * packet.free();
367
+ * }
368
+ * frame.free();
369
+ * }
370
+ * ```
371
+ *
372
+ * @see {@link encode} For async version
373
+ */
374
+ encodeSync(frame) {
375
+ if (this.isClosed) {
376
+ if (!frame) {
377
+ return null;
378
+ }
379
+ throw new Error('Encoder is closed');
380
+ }
381
+ // Open encoder if not already done
382
+ if (!this.initialized) {
383
+ if (!frame) {
384
+ return null;
385
+ }
386
+ this.initializeSync(frame);
387
+ }
388
+ // Send frame to encoder
389
+ const sendRet = this.codecContext.sendFrameSync(frame);
390
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
391
+ // Encoder might be full, try to receive first
392
+ const packet = this.receiveSync();
333
393
  if (packet)
334
394
  return packet;
335
395
  // If still failing, it's an error
@@ -338,7 +398,7 @@ export class Encoder {
338
398
  }
339
399
  }
340
400
  // Try to receive packet
341
- return await this.receivePacket();
401
+ return this.receiveSync();
342
402
  }
343
403
  /**
344
404
  * Encode frame stream to packet stream.
@@ -349,7 +409,9 @@ export class Encoder {
349
409
  * Primary interface for stream-based encoding.
350
410
  *
351
411
  * @param frames - Async iterable of frames (freed automatically)
352
- * @yields Encoded packets (caller must free)
412
+ *
413
+ * @yields {Packet} Encoded packets (caller must free)
414
+ *
353
415
  * @throws {Error} If encoder is closed
354
416
  *
355
417
  * @throws {FFmpegError} If encoding fails
@@ -368,11 +430,11 @@ export class Encoder {
368
430
  * // With frame filtering
369
431
  * async function* filteredFrames() {
370
432
  * for await (const frame of decoder.frames(input.packets())) {
371
- * await filter.filterFrame(frame);
372
- * const filtered = await filter.getFrame();
433
+ * const filtered = await filter.process(frame);
373
434
  * if (filtered) {
374
435
  * yield filtered;
375
436
  * }
437
+ * frame.free();
376
438
  * }
377
439
  * }
378
440
  *
@@ -400,9 +462,6 @@ export class Encoder {
400
462
  * @see {@link Decoder.frames} For frame source
401
463
  */
402
464
  async *packets(frames) {
403
- if (!this.isOpen) {
404
- throw new Error('Encoder is closed');
405
- }
406
465
  // Process frames
407
466
  for await (const frame of frames) {
408
467
  try {
@@ -417,29 +476,101 @@ export class Encoder {
417
476
  }
418
477
  }
419
478
  // Flush encoder after all frames
420
- let packet;
421
- while ((packet = await this.flush()) !== null) {
422
- yield packet;
479
+ await this.flush();
480
+ while (true) {
481
+ const remaining = await this.receive();
482
+ if (!remaining)
483
+ break;
484
+ yield remaining;
423
485
  }
424
486
  }
425
487
  /**
426
- * Flush encoder and get buffered packet.
488
+ * Encode frame stream to packet stream synchronously.
489
+ * Synchronous version of packets.
427
490
  *
428
- * Signals end-of-stream and retrieves remaining packets.
429
- * Call repeatedly until null to get all buffered packets.
430
- * Essential for ensuring all frames are encoded.
491
+ * High-level sync generator for complete encoding pipeline.
492
+ * Automatically manages frame memory, encoder state,
493
+ * and flushes buffered packets at end.
494
+ * Primary interface for stream-based encoding.
431
495
  *
432
- * Direct mapping to avcodec_send_frame(NULL).
496
+ * @param frames - Iterable of frames (freed automatically)
433
497
  *
434
- * @returns Buffered packet or null if none remaining
498
+ * @yields {Packet} Encoded packets (caller must free)
435
499
  *
436
500
  * @throws {Error} If encoder is closed
437
501
  *
502
+ * @throws {FFmpegError} If encoding fails
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * // Basic encoding pipeline
507
+ * for (const packet of encoder.packetsSync(decoder.framesSync(packets))) {
508
+ * output.writePacketSync(packet);
509
+ * packet.free(); // Must free output packets
510
+ * }
511
+ * ```
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * // With frame filtering
516
+ * function* filteredFrames() {
517
+ * for (const frame of decoder.framesSync(packets)) {
518
+ * const filtered = filter.processSync(frame);
519
+ * if (filtered) {
520
+ * yield filtered;
521
+ * }
522
+ * frame.free();
523
+ * }
524
+ * }
525
+ *
526
+ * for (const packet of encoder.packetsSync(filteredFrames())) {
527
+ * output.writePacketSync(packet);
528
+ * packet.free();
529
+ * }
530
+ * ```
531
+ *
532
+ * @see {@link packets} For async version
533
+ */
534
+ *packetsSync(frames) {
535
+ // Process frames
536
+ for (const frame of frames) {
537
+ try {
538
+ const packet = this.encodeSync(frame);
539
+ if (packet) {
540
+ yield packet;
541
+ }
542
+ }
543
+ finally {
544
+ // Free the input frame after encoding
545
+ frame.free();
546
+ }
547
+ }
548
+ // Flush encoder after all frames
549
+ this.flushSync();
550
+ while (true) {
551
+ const remaining = this.receiveSync();
552
+ if (!remaining)
553
+ break;
554
+ yield remaining;
555
+ }
556
+ }
557
+ /**
558
+ * Flush encoder and signal end-of-stream.
559
+ *
560
+ * Sends null frame to encoder to signal end-of-stream.
561
+ * Does nothing if encoder was never initialized or is closed.
562
+ * Must call receive() to get remaining buffered packets.
563
+ *
564
+ * Direct mapping to avcodec_send_frame(NULL).
565
+ *
438
566
  * @example
439
567
  * ```typescript
440
- * // Flush remaining packets
568
+ * // Signal end of stream
569
+ * await encoder.flush();
570
+ *
571
+ * // Then get remaining packets
441
572
  * let packet;
442
- * while ((packet = await encoder.flush()) !== null) {
573
+ * while ((packet = await encoder.receive()) !== null) {
443
574
  * console.log('Got buffered packet');
444
575
  * await output.writePacket(packet);
445
576
  * packet.free();
@@ -447,26 +578,66 @@ export class Encoder {
447
578
  * ```
448
579
  *
449
580
  * @see {@link flushPackets} For async iteration
450
- * @see {@link packets} For complete encoding pipeline
581
+ * @see {@link receive} For getting buffered packets
451
582
  */
452
583
  async flush() {
453
- if (!this.isOpen) {
454
- throw new Error('Encoder is closed');
584
+ if (this.isClosed || !this.initialized) {
585
+ return;
586
+ }
587
+ // Send flush frame (null)
588
+ const ret = await this.codecContext.sendFrame(null);
589
+ if (ret < 0 && ret !== AVERROR_EOF) {
590
+ if (ret !== AVERROR_EAGAIN) {
591
+ FFmpegError.throwIfError(ret, 'Failed to flush encoder');
592
+ }
593
+ }
594
+ }
595
+ /**
596
+ * Flush encoder and signal end-of-stream synchronously.
597
+ * Synchronous version of flush.
598
+ *
599
+ * Sends null frame to encoder to signal end-of-stream.
600
+ * Does nothing if encoder was never initialized or is closed.
601
+ * Must call receiveSync() to get remaining buffered packets.
602
+ *
603
+ * Direct mapping to avcodec_send_frame(NULL).
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * // Signal end of stream
608
+ * encoder.flushSync();
609
+ *
610
+ * // Then get remaining packets
611
+ * let packet;
612
+ * while ((packet = encoder.receiveSync()) !== null) {
613
+ * console.log('Got buffered packet');
614
+ * output.writePacketSync(packet);
615
+ * packet.free();
616
+ * }
617
+ * ```
618
+ *
619
+ * @see {@link flush} For async version
620
+ */
621
+ flushSync() {
622
+ if (this.isClosed || !this.initialized) {
623
+ return;
455
624
  }
456
625
  // Send flush frame (null)
457
- await this.codecContext.sendFrame(null);
458
- // Receive packet
459
- return await this.receivePacket();
626
+ const ret = this.codecContext.sendFrameSync(null);
627
+ if (ret < 0 && ret !== AVERROR_EOF) {
628
+ if (ret !== AVERROR_EAGAIN) {
629
+ FFmpegError.throwIfError(ret, 'Failed to flush encoder');
630
+ }
631
+ }
460
632
  }
461
633
  /**
462
634
  * Flush all buffered packets as async generator.
463
635
  *
464
636
  * Convenient async iteration over remaining packets.
465
- * Automatically handles repeated flush calls.
466
- * Useful for end-of-stream processing.
637
+ * Automatically handles flush and repeated receive calls.
638
+ * Returns immediately if encoder was never initialized or is closed.
467
639
  *
468
- * @yields Buffered packets
469
- * @throws {Error} If encoder is closed
640
+ * @yields {Packet} Buffered packets
470
641
  *
471
642
  * @example
472
643
  * ```typescript
@@ -478,96 +649,151 @@ export class Encoder {
478
649
  * }
479
650
  * ```
480
651
  *
481
- * @see {@link flush} For single packet flush
652
+ * @see {@link flush} For signaling end-of-stream
482
653
  * @see {@link packets} For complete pipeline
483
654
  */
484
655
  async *flushPackets() {
485
- if (!this.isOpen) {
486
- throw new Error('Encoder is closed');
487
- }
656
+ // Send flush signal
657
+ await this.flush();
488
658
  let packet;
489
- while ((packet = await this.flush()) !== null) {
659
+ while ((packet = await this.receive()) !== null) {
490
660
  yield packet;
491
661
  }
492
662
  }
493
663
  /**
494
- * Close encoder and free resources.
664
+ * Flush all buffered packets as generator synchronously.
665
+ * Synchronous version of flushPackets.
495
666
  *
496
- * Releases codec context and internal packet buffer.
497
- * Safe to call multiple times.
498
- * Does NOT dispose hardware context - caller is responsible.
499
- * Automatically called by Symbol.dispose.
667
+ * Convenient sync iteration over remaining packets.
668
+ * Automatically handles flush and repeated receive calls.
669
+ * Returns immediately if encoder was never initialized or is closed.
670
+ *
671
+ * @yields {Packet} Buffered packets
500
672
  *
501
673
  * @example
502
674
  * ```typescript
503
- * const encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo);
504
- * try {
505
- * // Use encoder
506
- * } finally {
507
- * encoder.close();
675
+ * // Flush at end of encoding
676
+ * for (const packet of encoder.flushPacketsSync()) {
677
+ * console.log('Processing buffered packet');
678
+ * output.writePacketSync(packet);
679
+ * packet.free();
508
680
  * }
509
681
  * ```
510
682
  *
511
- * @see {@link Symbol.dispose} For automatic cleanup
683
+ * @see {@link flushPackets} For async version
512
684
  */
513
- close() {
514
- if (!this.isOpen)
515
- return;
516
- this.packet.free();
517
- this.codecContext.freeContext();
518
- this.isOpen = false;
685
+ *flushPacketsSync() {
686
+ // Send flush signal
687
+ this.flushSync();
688
+ let packet;
689
+ while ((packet = this.receiveSync()) !== null) {
690
+ yield packet;
691
+ }
519
692
  }
520
693
  /**
521
- * Get encoder codec.
694
+ * Receive packet from encoder.
522
695
  *
523
- * Returns the codec used by this encoder.
524
- * Useful for checking codec capabilities and properties.
696
+ * Gets encoded packets from the codec's internal buffer.
697
+ * Handles packet cloning and error checking.
698
+ * Returns null if encoder is closed, not initialized, or no packets available.
699
+ * Call repeatedly until null to drain all buffered packets.
525
700
  *
526
- * @returns Codec instance
701
+ * Direct mapping to avcodec_receive_packet().
702
+ *
703
+ * @returns Cloned packet or null if no packets available
704
+ *
705
+ * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
527
706
  *
528
707
  * @example
529
708
  * ```typescript
530
- * const codec = encoder.getCodec();
531
- * console.log(`Using codec: ${codec.name}`);
532
- * console.log(`Capabilities: ${codec.capabilities}`);
709
+ * const packet = await encoder.receive();
710
+ * if (packet) {
711
+ * console.log(`Got packet with PTS: ${packet.pts}`);
712
+ * await output.writePacket(packet);
713
+ * packet.free();
714
+ * }
533
715
  * ```
534
716
  *
535
- * @see {@link Codec} For codec properties
536
- */
537
- getCodec() {
538
- return this.codec;
539
- }
540
- /**
541
- * Get underlying codec context.
542
- *
543
- * Returns the internal codec context for advanced operations.
544
- * Returns null if encoder is closed.
545
- *
546
- * @returns Codec context or null
717
+ * @example
718
+ * ```typescript
719
+ * // Drain all buffered packets
720
+ * let packet;
721
+ * while ((packet = await encoder.receive()) !== null) {
722
+ * console.log(`Packet size: ${packet.size}`);
723
+ * await output.writePacket(packet);
724
+ * packet.free();
725
+ * }
726
+ * ```
547
727
  *
548
- * @internal
728
+ * @see {@link encode} For sending frames and receiving packets
729
+ * @see {@link flush} For signaling end-of-stream
549
730
  */
550
- getCodecContext() {
551
- return this.isOpen ? this.codecContext : null;
731
+ async receive() {
732
+ if (this.isClosed || !this.initialized) {
733
+ return null;
734
+ }
735
+ // Clear previous packet data
736
+ this.packet.unref();
737
+ const ret = await this.codecContext.receivePacket(this.packet);
738
+ if (ret === 0) {
739
+ // Got a packet, clone it for the user
740
+ return this.packet.clone();
741
+ }
742
+ else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
743
+ // Need more data or end of stream
744
+ return null;
745
+ }
746
+ else {
747
+ // Error
748
+ FFmpegError.throwIfError(ret, 'Failed to receive packet');
749
+ return null;
750
+ }
552
751
  }
553
752
  /**
554
- * Receive packet from encoder.
753
+ * Receive packet from encoder synchronously.
754
+ * Synchronous version of receive.
555
755
  *
556
- * Internal method to get encoded packets from codec.
756
+ * Gets encoded packets from the codec's internal buffer.
557
757
  * Handles packet cloning and error checking.
758
+ * Returns null if encoder is closed, not initialized, or no packets available.
759
+ * Call repeatedly until null to drain all buffered packets.
558
760
  *
559
761
  * Direct mapping to avcodec_receive_packet().
560
762
  *
561
- * @returns Cloned packet or null
763
+ * @returns Cloned packet or null if no packets available
562
764
  *
563
765
  * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
564
766
  *
565
- * @internal
767
+ * @example
768
+ * ```typescript
769
+ * const packet = encoder.receiveSync();
770
+ * if (packet) {
771
+ * console.log(`Got packet with PTS: ${packet.pts}`);
772
+ * output.writePacketSync(packet);
773
+ * packet.free();
774
+ * }
775
+ * ```
776
+ *
777
+ * @example
778
+ * ```typescript
779
+ * // Drain all buffered packets
780
+ * let packet;
781
+ * while ((packet = encoder.receiveSync()) !== null) {
782
+ * console.log(`Packet size: ${packet.size}`);
783
+ * output.writePacketSync(packet);
784
+ * packet.free();
785
+ * }
786
+ * ```
787
+ *
788
+ * @see {@link receive} For async version
566
789
  */
567
- async receivePacket() {
790
+ receiveSync() {
791
+ if (this.isClosed || !this.initialized) {
792
+ return null;
793
+ }
568
794
  // Clear previous packet data
569
795
  this.packet.unref();
570
- const ret = await this.codecContext.receivePacket(this.packet);
796
+ const ret = this.codecContext.receivePacketSync(this.packet);
571
797
  if (ret === 0) {
572
798
  // Got a packet, clone it for the user
573
799
  return this.packet.clone();
@@ -582,6 +808,138 @@ export class Encoder {
582
808
  return null;
583
809
  }
584
810
  }
811
+ /**
812
+ * Close encoder and free resources.
813
+ *
814
+ * Releases codec context and internal packet buffer.
815
+ * Safe to call multiple times.
816
+ * Automatically called by Symbol.dispose.
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
821
+ * try {
822
+ * // Use encoder
823
+ * } finally {
824
+ * encoder.close();
825
+ * }
826
+ * ```
827
+ *
828
+ * @see {@link Symbol.dispose} For automatic cleanup
829
+ */
830
+ close() {
831
+ if (this.isClosed) {
832
+ return;
833
+ }
834
+ this.isClosed = true;
835
+ this.packet.free();
836
+ this.codecContext.freeContext();
837
+ this.initialized = false;
838
+ }
839
+ /**
840
+ * Initialize encoder from first frame.
841
+ *
842
+ * Sets codec context parameters from frame properties.
843
+ * Configures hardware context if present in frame.
844
+ * Opens encoder with accumulated options.
845
+ *
846
+ * @param frame - First frame to encode
847
+ *
848
+ * @throws {FFmpegError} If encoder open fails
849
+ *
850
+ * @internal
851
+ */
852
+ async initialize(frame) {
853
+ if (frame.isVideo()) {
854
+ this.codecContext.width = frame.width;
855
+ this.codecContext.height = frame.height;
856
+ this.codecContext.pixelFormat = frame.format;
857
+ this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
858
+ }
859
+ else {
860
+ this.codecContext.sampleRate = frame.sampleRate;
861
+ this.codecContext.sampleFormat = frame.format;
862
+ this.codecContext.channelLayout = frame.channelLayout;
863
+ }
864
+ this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
865
+ this.codecContext.hwFramesCtx = frame.hwFramesCtx;
866
+ // Open codec
867
+ const openRet = await this.codecContext.open2(this.codec, this.opts);
868
+ if (openRet < 0) {
869
+ this.codecContext.freeContext();
870
+ FFmpegError.throwIfError(openRet, 'Failed to open encoder');
871
+ }
872
+ this.initialized = true;
873
+ }
874
+ /**
875
+ * Initialize encoder from first frame synchronously.
876
+ * Synchronous version of initialize.
877
+ *
878
+ * Sets codec context parameters from frame properties.
879
+ * Configures hardware context if present in frame.
880
+ * Opens encoder with accumulated options.
881
+ *
882
+ * @param frame - First frame to encode
883
+ *
884
+ * @throws {FFmpegError} If encoder open fails
885
+ *
886
+ * @internal
887
+ *
888
+ * @see {@link initialize} For async version
889
+ */
890
+ initializeSync(frame) {
891
+ if (frame.isVideo()) {
892
+ this.codecContext.width = frame.width;
893
+ this.codecContext.height = frame.height;
894
+ this.codecContext.pixelFormat = frame.format;
895
+ this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
896
+ }
897
+ else {
898
+ this.codecContext.sampleRate = frame.sampleRate;
899
+ this.codecContext.sampleFormat = frame.format;
900
+ this.codecContext.channelLayout = frame.channelLayout;
901
+ }
902
+ this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
903
+ this.codecContext.hwFramesCtx = frame.hwFramesCtx;
904
+ // Open codec
905
+ const openRet = this.codecContext.open2Sync(this.codec, this.opts);
906
+ if (openRet < 0) {
907
+ this.codecContext.freeContext();
908
+ FFmpegError.throwIfError(openRet, 'Failed to open encoder');
909
+ }
910
+ this.initialized = true;
911
+ }
912
+ /**
913
+ * Get encoder codec.
914
+ *
915
+ * Returns the codec used by this encoder.
916
+ * Useful for checking codec capabilities and properties.
917
+ *
918
+ * @returns Codec instance
919
+ *
920
+ * @internal
921
+ *
922
+ * @see {@link Codec} For codec details
923
+ */
924
+ getCodec() {
925
+ return this.codec;
926
+ }
927
+ /**
928
+ * Get underlying codec context.
929
+ *
930
+ * Returns the codec context for advanced operations.
931
+ * Useful for accessing low-level codec properties and settings.
932
+ * Returns null if encoder is closed or not initialized.
933
+ *
934
+ * @returns Codec context or null if closed/not initialized
935
+ *
936
+ * @internal
937
+ *
938
+ * @see {@link CodecContext} For context details
939
+ */
940
+ getCodecContext() {
941
+ return !this.isClosed && this.initialized ? this.codecContext : null;
942
+ }
585
943
  /**
586
944
  * Dispose of encoder.
587
945
  *
@@ -591,7 +949,7 @@ export class Encoder {
591
949
  * @example
592
950
  * ```typescript
593
951
  * {
594
- * using encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo);
952
+ * using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
595
953
  * // Encode frames...
596
954
  * } // Automatically closed
597
955
  * ```