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,5 @@
1
- import { AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO } from '../constants/constants.js';
2
- import { AVERROR_EAGAIN, 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.
@@ -12,10 +12,10 @@ import { parseBitrate } from './utils.js';
12
12
  * @example
13
13
  * ```typescript
14
14
  * import { Encoder } from 'node-av/api';
15
- * import { AV_CODEC_ID_H264 } from 'node-av/constants';
15
+ * import { AV_CODEC_ID_H264, FF_ENCODER_LIBX264 } from 'node-av/constants';
16
16
  *
17
17
  * // Create H.264 encoder
18
- * const encoder = await Encoder.create('libx264', {
18
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
19
19
  * type: 'video',
20
20
  * width: 1920,
21
21
  * height: 1080,
@@ -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,19 @@ 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
69
71
  * @param codec - Encoder codec
70
- * @param hardware - Optional hardware context
72
+ * @param opts - Encoder options as Dictionary
71
73
  * @internal
72
74
  */
73
- constructor(codecContext, codec, hardware) {
75
+ constructor(codecContext, codec, opts) {
74
76
  this.codecContext = codecContext;
75
77
  this.codec = codec;
76
- this.hardware = hardware;
78
+ this.opts = opts;
77
79
  this.packet = new Packet();
78
80
  this.packet.alloc();
79
81
  }
@@ -81,24 +83,24 @@ export class Encoder {
81
83
  * Create an encoder with specified codec and options.
82
84
  *
83
85
  * 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.
86
+ * Uses lazy initialization - encoder is opened when first frame is received.
87
+ * Hardware context will be automatically detected from first frame if not provided.
86
88
  *
87
89
  * Direct mapping to avcodec_find_encoder_by_name() or avcodec_find_encoder().
88
90
  *
89
91
  * @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
92
+ * @param options - Encoder configuration options including required timeBase
92
93
  * @returns Configured encoder instance
93
94
  *
94
- * @throws {Error} If encoder not found or unsupported format
95
- * @throws {FFmpegError} If codec initialization fails
95
+ * @throws {Error} If encoder not found or timeBase not provided
96
+ *
97
+ * @throws {FFmpegError} If codec allocation fails
96
98
  *
97
99
  * @example
98
100
  * ```typescript
99
101
  * // From decoder stream info
100
- * const streamInfo = decoder.getOutputStreamInfo();
101
- * const encoder = await Encoder.create('libx264', streamInfo, {
102
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
103
+ * timeBase: video.timeBase,
102
104
  * bitrate: '5M',
103
105
  * gopSize: 60,
104
106
  * options: {
@@ -111,13 +113,8 @@ export class Encoder {
111
113
  * @example
112
114
  * ```typescript
113
115
  * // With custom stream info
114
- * const encoder = await Encoder.create('aac', {
115
- * type: 'audio',
116
- * sampleRate: 48000,
117
- * sampleFormat: AV_SAMPLE_FMT_FLTP,
118
- * channelLayout: AV_CH_LAYOUT_STEREO,
119
- * timeBase: { num: 1, den: 48000 }
120
- * }, {
116
+ * const encoder = await Encoder.create(FF_ENCODER_AAC, {
117
+ * timeBase: audio.timeBase,
121
118
  * bitrate: '192k'
122
119
  * });
123
120
  * ```
@@ -126,16 +123,16 @@ export class Encoder {
126
123
  * ```typescript
127
124
  * // Hardware encoder
128
125
  * const hw = HardwareContext.auto();
129
- * const encoder = await Encoder.create('hevc_videotoolbox', streamInfo, {
130
- * hardware: hw,
126
+ * const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
127
+ * const encoder = await Encoder.create(encoderCodec, {
128
+ * timeBase: video.timeBase,
131
129
  * bitrate: '8M'
132
130
  * });
133
131
  * ```
134
132
  *
135
- * @see {@link Decoder.getOutputStreamInfo} For stream info source
136
133
  * @see {@link EncoderOptions} For configuration options
137
134
  */
138
- static async create(encoderCodec, input, options = {}) {
135
+ static async create(encoderCodec, options) {
139
136
  let codec = null;
140
137
  let codecName = '';
141
138
  if (encoderCodec instanceof Codec) {
@@ -156,52 +153,6 @@ export class Encoder {
156
153
  // Allocate codec context
157
154
  const codecContext = new CodecContext();
158
155
  codecContext.allocContext3(codec);
159
- // It's StreamInfo - apply manually
160
- if (input.type === 'video' && codec.type === AVMEDIA_TYPE_VIDEO) {
161
- const videoInfo = input;
162
- const codecPixelformats = codec.pixelFormats;
163
- if (codecPixelformats && !codecPixelformats.includes(videoInfo.pixelFormat)) {
164
- codecContext.freeContext();
165
- const pixelFormatName = avGetPixFmtName(videoInfo.pixelFormat) ?? 'unknown';
166
- const codecPixFmtNames = codecPixelformats.map(avGetPixFmtName).filter(Boolean).join(', ');
167
- throw new Error(`Unsupported pixel format for '${codecName}' encoder: ${pixelFormatName}! Supported formats: ${codecPixFmtNames}`);
168
- }
169
- codecContext.width = videoInfo.width;
170
- codecContext.height = videoInfo.height;
171
- codecContext.pixelFormat = videoInfo.pixelFormat;
172
- // Set pkt_timebase and timeBase to input timebase
173
- codecContext.pktTimebase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
174
- codecContext.timeBase = new Rational(videoInfo.timeBase.num, videoInfo.timeBase.den);
175
- if (videoInfo.frameRate) {
176
- codecContext.framerate = new Rational(videoInfo.frameRate.num, videoInfo.frameRate.den);
177
- }
178
- if (videoInfo.sampleAspectRatio) {
179
- codecContext.sampleAspectRatio = new Rational(videoInfo.sampleAspectRatio.num, videoInfo.sampleAspectRatio.den);
180
- }
181
- }
182
- else if (input.type === 'audio' && codec.type === AVMEDIA_TYPE_AUDIO) {
183
- const audioInfo = input;
184
- const codecSampleFormats = codec.sampleFormats;
185
- if (codecSampleFormats && !codecSampleFormats.includes(audioInfo.sampleFormat)) {
186
- codecContext.freeContext();
187
- const sampleFormatName = avGetSampleFmtName(audioInfo.sampleFormat) ?? 'unknown';
188
- const supportedFormats = codecSampleFormats.map(avGetSampleFmtName).filter(Boolean).join(', ');
189
- throw new Error(`Unsupported sample format for '${codecName}' encoder: ${sampleFormatName}! Supported formats: ${supportedFormats}`);
190
- }
191
- codecContext.sampleRate = audioInfo.sampleRate;
192
- codecContext.sampleFormat = audioInfo.sampleFormat;
193
- codecContext.channelLayout = audioInfo.channelLayout;
194
- // Set both pkt_timebase and timeBase for audio
195
- codecContext.pktTimebase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
196
- codecContext.timeBase = new Rational(audioInfo.timeBase.num, audioInfo.timeBase.den);
197
- if (audioInfo.frameSize) {
198
- codecContext.frameSize = audioInfo.frameSize;
199
- }
200
- }
201
- else {
202
- codecContext.freeContext();
203
- throw new Error(`Unsupported codec type for encoder! Input type: ${input.type}, Codec type: ${codec.type}`);
204
- }
205
156
  // Apply encoder-specific options
206
157
  if (options.gopSize !== undefined) {
207
158
  codecContext.gopSize = options.gopSize;
@@ -214,32 +165,28 @@ export class Encoder {
214
165
  const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
215
166
  codecContext.bitRate = bitrate;
216
167
  }
217
- if (options.threads !== undefined) {
218
- codecContext.threadCount = options.threads;
168
+ if (options.minRate !== undefined) {
169
+ const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
170
+ codecContext.rcMinRate = minRate;
219
171
  }
220
- // Override timeBase if explicitly specified in options
221
- if (options.timeBase) {
222
- codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
172
+ if (options.maxRate !== undefined) {
173
+ const maxRate = typeof options.maxRate === 'string' ? parseBitrate(options.maxRate) : BigInt(options.maxRate);
174
+ codecContext.rcMaxRate = maxRate;
223
175
  }
224
- // Apply codec-specific options via AVOptions
225
- if (options.options) {
226
- for (const [key, value] of Object.entries(options.options)) {
227
- codecContext.setOption(key, value.toString());
228
- }
176
+ if (options.bufSize !== undefined) {
177
+ const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
178
+ codecContext.rcBufferSize = Number(bufSize);
229
179
  }
230
- const isHWEncoder = codec.isHardwareAcceleratedEncoder();
231
- if (isHWEncoder && !options.hardware) {
232
- codecContext.freeContext();
233
- throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
180
+ if (options.threads !== undefined) {
181
+ codecContext.threadCount = options.threads;
234
182
  }
235
- // Open codec
236
- const openRet = await codecContext.open2(codec, null);
237
- if (openRet < 0) {
238
- codecContext.freeContext();
239
- FFmpegError.throwIfError(openRet, 'Failed to open encoder');
183
+ codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
184
+ codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
185
+ if (options.frameRate) {
186
+ codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
240
187
  }
241
- const encoder = new Encoder(codecContext, codec, isHWEncoder ? options.hardware : undefined);
242
- return encoder;
188
+ const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
189
+ return new Encoder(codecContext, codec, opts);
243
190
  }
244
191
  /**
245
192
  * Check if encoder is open.
@@ -252,7 +199,25 @@ export class Encoder {
252
199
  * ```
253
200
  */
254
201
  get isEncoderOpen() {
255
- return this.isOpen;
202
+ return !this.isClosed;
203
+ }
204
+ /**
205
+ * Check if encoder has been initialized.
206
+ *
207
+ * Returns true after first frame has been processed and encoder opened.
208
+ * Useful for checking if encoder has received frame properties.
209
+ *
210
+ * @returns true if encoder has been initialized with frame data
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * if (!encoder.isEncoderInitialized) {
215
+ * console.log('Encoder will initialize on first frame');
216
+ * }
217
+ * ```
218
+ */
219
+ get isEncoderInitialized() {
220
+ return this.initialized;
256
221
  }
257
222
  /**
258
223
  * Check if encoder uses hardware acceleration.
@@ -269,14 +234,29 @@ export class Encoder {
269
234
  * @see {@link HardwareContext} For hardware setup
270
235
  */
271
236
  isHardware() {
272
- return !!this.hardware;
237
+ return this.codec.isHardwareAcceleratedEncoder();
238
+ }
239
+ /**
240
+ * Check if encoder is ready for processing.
241
+ *
242
+ * @returns true if initialized and ready
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * if (encoder.isReady()) {
247
+ * const packet = await encoder.encode(frame);
248
+ * }
249
+ * ```
250
+ */
251
+ isReady() {
252
+ return this.initialized && !this.isClosed;
273
253
  }
274
254
  /**
275
255
  * Encode a frame to a packet.
276
256
  *
277
257
  * Sends a frame to the encoder and attempts to receive an encoded packet.
258
+ * On first frame, automatically initializes encoder with frame properties.
278
259
  * Handles internal buffering - may return null if more frames needed.
279
- * Automatically manages encoder state and hardware context binding.
280
260
  *
281
261
  * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
282
262
  *
@@ -284,6 +264,7 @@ export class Encoder {
284
264
  * @returns Encoded packet or null if more data needed
285
265
  *
286
266
  * @throws {Error} If encoder is closed
267
+ *
287
268
  * @throws {FFmpegError} If encoding fails
288
269
  *
289
270
  * @example
@@ -313,21 +294,24 @@ export class Encoder {
313
294
  * @see {@link flush} For end-of-stream handling
314
295
  */
315
296
  async encode(frame) {
316
- if (!this.isOpen) {
297
+ if (this.isClosed) {
298
+ if (!frame) {
299
+ return null;
300
+ }
317
301
  throw new Error('Encoder is closed');
318
302
  }
319
- // Late binding of hw_frames_ctx for hardware encoders
320
- // Hardware encoders get hw_frames_ctx from the frames they receive
321
- if (this.hardware && frame?.hwFramesCtx && !this.codecContext.hwFramesCtx) {
322
- // Use the hw_frames_ctx from the frame
323
- this.codecContext.hwFramesCtx = frame.hwFramesCtx;
324
- this.codecContext.pixelFormat = this.hardware.devicePixelFormat;
303
+ // Open encoder if not already done
304
+ if (!this.initialized) {
305
+ if (!frame) {
306
+ return null;
307
+ }
308
+ await this.initialize(frame);
325
309
  }
326
310
  // Send frame to encoder
327
311
  const sendRet = await this.codecContext.sendFrame(frame);
328
312
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
329
313
  // Encoder might be full, try to receive first
330
- const packet = await this.receivePacket();
314
+ const packet = await this.receive();
331
315
  if (packet)
332
316
  return packet;
333
317
  // If still failing, it's an error
@@ -336,7 +320,7 @@ export class Encoder {
336
320
  }
337
321
  }
338
322
  // Try to receive packet
339
- return await this.receivePacket();
323
+ return await this.receive();
340
324
  }
341
325
  /**
342
326
  * Encode frame stream to packet stream.
@@ -347,8 +331,9 @@ export class Encoder {
347
331
  * Primary interface for stream-based encoding.
348
332
  *
349
333
  * @param frames - Async iterable of frames (freed automatically)
350
- * @yields Encoded packets (caller must free)
334
+ * @yields {Packet} Encoded packets (caller must free)
351
335
  * @throws {Error} If encoder is closed
336
+ *
352
337
  * @throws {FFmpegError} If encoding fails
353
338
  *
354
339
  * @example
@@ -365,11 +350,11 @@ export class Encoder {
365
350
  * // With frame filtering
366
351
  * async function* filteredFrames() {
367
352
  * for await (const frame of decoder.frames(input.packets())) {
368
- * await filter.filterFrame(frame);
369
- * const filtered = await filter.getFrame();
353
+ * const filtered = await filter.process(frame);
370
354
  * if (filtered) {
371
355
  * yield filtered;
372
356
  * }
357
+ * frame.free();
373
358
  * }
374
359
  * }
375
360
  *
@@ -397,9 +382,6 @@ export class Encoder {
397
382
  * @see {@link Decoder.frames} For frame source
398
383
  */
399
384
  async *packets(frames) {
400
- if (!this.isOpen) {
401
- throw new Error('Encoder is closed');
402
- }
403
385
  // Process frames
404
386
  for await (const frame of frames) {
405
387
  try {
@@ -414,29 +396,31 @@ export class Encoder {
414
396
  }
415
397
  }
416
398
  // Flush encoder after all frames
417
- let packet;
418
- while ((packet = await this.flush()) !== null) {
419
- yield packet;
399
+ await this.flush();
400
+ while (true) {
401
+ const remaining = await this.receive();
402
+ if (!remaining)
403
+ break;
404
+ yield remaining;
420
405
  }
421
406
  }
422
407
  /**
423
- * Flush encoder and get buffered packet.
408
+ * Flush encoder and signal end-of-stream.
424
409
  *
425
- * Signals end-of-stream and retrieves remaining packets.
426
- * Call repeatedly until null to get all buffered packets.
427
- * Essential for ensuring all frames are encoded.
410
+ * Sends null frame to encoder to signal end-of-stream.
411
+ * Does nothing if encoder was never initialized or is closed.
412
+ * Must call receive() to get remaining buffered packets.
428
413
  *
429
414
  * Direct mapping to avcodec_send_frame(NULL).
430
415
  *
431
- * @returns Buffered packet or null if none remaining
432
- *
433
- * @throws {Error} If encoder is closed
434
- *
435
416
  * @example
436
417
  * ```typescript
437
- * // Flush remaining packets
418
+ * // Signal end of stream
419
+ * await encoder.flush();
420
+ *
421
+ * // Then get remaining packets
438
422
  * let packet;
439
- * while ((packet = await encoder.flush()) !== null) {
423
+ * while ((packet = await encoder.receive()) !== null) {
440
424
  * console.log('Got buffered packet');
441
425
  * await output.writePacket(packet);
442
426
  * packet.free();
@@ -444,26 +428,28 @@ export class Encoder {
444
428
  * ```
445
429
  *
446
430
  * @see {@link flushPackets} For async iteration
447
- * @see {@link packets} For complete encoding pipeline
431
+ * @see {@link receive} For getting buffered packets
448
432
  */
449
433
  async flush() {
450
- if (!this.isOpen) {
451
- throw new Error('Encoder is closed');
434
+ if (this.isClosed || !this.initialized) {
435
+ return;
452
436
  }
453
437
  // Send flush frame (null)
454
- await this.codecContext.sendFrame(null);
455
- // Receive packet
456
- return await this.receivePacket();
438
+ const ret = await this.codecContext.sendFrame(null);
439
+ if (ret < 0 && ret !== AVERROR_EOF) {
440
+ if (ret !== AVERROR_EAGAIN) {
441
+ FFmpegError.throwIfError(ret, 'Failed to flush encoder');
442
+ }
443
+ }
457
444
  }
458
445
  /**
459
446
  * Flush all buffered packets as async generator.
460
447
  *
461
448
  * Convenient async iteration over remaining packets.
462
- * Automatically handles repeated flush calls.
463
- * Useful for end-of-stream processing.
449
+ * Automatically handles flush and repeated receive calls.
450
+ * Returns immediately if encoder was never initialized or is closed.
464
451
  *
465
- * @yields Buffered packets
466
- * @throws {Error} If encoder is closed
452
+ * @yields {Packet} Buffered packets
467
453
  *
468
454
  * @example
469
455
  * ```typescript
@@ -475,29 +461,86 @@ export class Encoder {
475
461
  * }
476
462
  * ```
477
463
  *
478
- * @see {@link flush} For single packet flush
464
+ * @see {@link flush} For signaling end-of-stream
479
465
  * @see {@link packets} For complete pipeline
480
466
  */
481
467
  async *flushPackets() {
482
- if (!this.isOpen) {
483
- throw new Error('Encoder is closed');
484
- }
468
+ // Send flush signal
469
+ await this.flush();
485
470
  let packet;
486
- while ((packet = await this.flush()) !== null) {
471
+ while ((packet = await this.receive()) !== null) {
487
472
  yield packet;
488
473
  }
489
474
  }
475
+ /**
476
+ * Receive packet from encoder.
477
+ *
478
+ * Gets encoded packets from the codec's internal buffer.
479
+ * Handles packet cloning and error checking.
480
+ * Returns null if encoder is closed, not initialized, or no packets available.
481
+ * Call repeatedly until null to drain all buffered packets.
482
+ *
483
+ * Direct mapping to avcodec_receive_packet().
484
+ *
485
+ * @returns Cloned packet or null if no packets available
486
+ *
487
+ * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * const packet = await encoder.receive();
492
+ * if (packet) {
493
+ * console.log(`Got packet with PTS: ${packet.pts}`);
494
+ * await output.writePacket(packet);
495
+ * packet.free();
496
+ * }
497
+ * ```
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * // Drain all buffered packets
502
+ * let packet;
503
+ * while ((packet = await encoder.receive()) !== null) {
504
+ * console.log(`Packet size: ${packet.size}`);
505
+ * await output.writePacket(packet);
506
+ * packet.free();
507
+ * }
508
+ * ```
509
+ *
510
+ * @see {@link encode} For sending frames and receiving packets
511
+ * @see {@link flush} For signaling end-of-stream
512
+ */
513
+ async receive() {
514
+ if (this.isClosed || !this.initialized) {
515
+ return null;
516
+ }
517
+ // Clear previous packet data
518
+ this.packet.unref();
519
+ const ret = await this.codecContext.receivePacket(this.packet);
520
+ if (ret === 0) {
521
+ // Got a packet, clone it for the user
522
+ return this.packet.clone();
523
+ }
524
+ else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
525
+ // Need more data or end of stream
526
+ return null;
527
+ }
528
+ else {
529
+ // Error
530
+ FFmpegError.throwIfError(ret, 'Failed to receive packet');
531
+ return null;
532
+ }
533
+ }
490
534
  /**
491
535
  * Close encoder and free resources.
492
536
  *
493
537
  * Releases codec context and internal packet buffer.
494
538
  * Safe to call multiple times.
495
- * Does NOT dispose hardware context - caller is responsible.
496
539
  * Automatically called by Symbol.dispose.
497
540
  *
498
541
  * @example
499
542
  * ```typescript
500
- * const encoder = await Encoder.create('libx264', streamInfo);
543
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
501
544
  * try {
502
545
  * // Use encoder
503
546
  * } finally {
@@ -508,11 +551,48 @@ export class Encoder {
508
551
  * @see {@link Symbol.dispose} For automatic cleanup
509
552
  */
510
553
  close() {
511
- if (!this.isOpen)
554
+ if (this.isClosed) {
512
555
  return;
556
+ }
557
+ this.isClosed = true;
513
558
  this.packet.free();
514
559
  this.codecContext.freeContext();
515
- this.isOpen = false;
560
+ this.initialized = false;
561
+ }
562
+ /**
563
+ * Initialize encoder from first frame.
564
+ *
565
+ * Sets codec context parameters from frame properties.
566
+ * Configures hardware context if present in frame.
567
+ * Opens encoder with accumulated options.
568
+ *
569
+ * @param frame - First frame to encode
570
+ *
571
+ * @throws {FFmpegError} If encoder open fails
572
+ *
573
+ * @internal
574
+ */
575
+ async initialize(frame) {
576
+ if (frame.isVideo()) {
577
+ this.codecContext.width = frame.width;
578
+ this.codecContext.height = frame.height;
579
+ this.codecContext.pixelFormat = frame.format;
580
+ this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
581
+ }
582
+ else {
583
+ this.codecContext.sampleRate = frame.sampleRate;
584
+ this.codecContext.sampleFormat = frame.format;
585
+ this.codecContext.channelLayout = frame.channelLayout;
586
+ }
587
+ this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
588
+ this.codecContext.hwFramesCtx = frame.hwFramesCtx;
589
+ // Open codec
590
+ const openRet = await this.codecContext.open2(this.codec, this.opts);
591
+ if (openRet < 0) {
592
+ this.codecContext.freeContext();
593
+ FFmpegError.throwIfError(openRet, 'Failed to open encoder');
594
+ }
595
+ this.initialized = true;
516
596
  }
517
597
  /**
518
598
  * Get encoder codec.
@@ -522,14 +602,9 @@ export class Encoder {
522
602
  *
523
603
  * @returns Codec instance
524
604
  *
525
- * @example
526
- * ```typescript
527
- * const codec = encoder.getCodec();
528
- * console.log(`Using codec: ${codec.name}`);
529
- * console.log(`Capabilities: ${codec.capabilities}`);
530
- * ```
605
+ * @internal
531
606
  *
532
- * @see {@link Codec} For codec properties
607
+ * @see {@link Codec} For codec details
533
608
  */
534
609
  getCodec() {
535
610
  return this.codec;
@@ -537,45 +612,18 @@ export class Encoder {
537
612
  /**
538
613
  * Get underlying codec context.
539
614
  *
540
- * Returns the internal codec context for advanced operations.
541
- * Returns null if encoder is closed.
615
+ * Returns the codec context for advanced operations.
616
+ * Useful for accessing low-level codec properties and settings.
617
+ * Returns null if encoder is closed or not initialized.
542
618
  *
543
- * @returns Codec context or null
619
+ * @returns Codec context or null if closed/not initialized
544
620
  *
545
621
  * @internal
546
- */
547
- getCodecContext() {
548
- return this.isOpen ? this.codecContext : null;
549
- }
550
- /**
551
- * Receive packet from encoder.
552
- *
553
- * Internal method to get encoded packets from codec.
554
- * Handles packet cloning and error checking.
555
- *
556
- * Direct mapping to avcodec_receive_packet().
557
- *
558
- * @returns Cloned packet or null
559
622
  *
560
- * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
623
+ * @see {@link CodecContext} For context details
561
624
  */
562
- async receivePacket() {
563
- // Clear previous packet data
564
- this.packet.unref();
565
- const ret = await this.codecContext.receivePacket(this.packet);
566
- if (ret === 0) {
567
- // Got a packet, clone it for the user
568
- return this.packet.clone();
569
- }
570
- else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
571
- // Need more data or end of stream
572
- return null;
573
- }
574
- else {
575
- // Error
576
- FFmpegError.throwIfError(ret, 'Failed to receive packet');
577
- return null;
578
- }
625
+ getCodecContext() {
626
+ return !this.isClosed && this.initialized ? this.codecContext : null;
579
627
  }
580
628
  /**
581
629
  * Dispose of encoder.
@@ -586,7 +634,7 @@ export class Encoder {
586
634
  * @example
587
635
  * ```typescript
588
636
  * {
589
- * using encoder = await Encoder.create('libx264', streamInfo);
637
+ * using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
590
638
  * // Encode frames...
591
639
  * } // Automatically closed
592
640
  * ```