node-av 1.3.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 (45) hide show
  1. package/README.md +37 -38
  2. package/dist/api/bitstream-filter.d.ts +2 -2
  3. package/dist/api/bitstream-filter.js +2 -2
  4. package/dist/api/decoder.d.ts +131 -120
  5. package/dist/api/decoder.js +191 -203
  6. package/dist/api/decoder.js.map +1 -1
  7. package/dist/api/encoder.d.ts +135 -77
  8. package/dist/api/encoder.js +235 -192
  9. package/dist/api/encoder.js.map +1 -1
  10. package/dist/api/filter-presets.d.ts +408 -1534
  11. package/dist/api/filter-presets.js +1005 -2058
  12. package/dist/api/filter-presets.js.map +1 -1
  13. package/dist/api/filter.d.ts +160 -165
  14. package/dist/api/filter.js +294 -374
  15. package/dist/api/filter.js.map +1 -1
  16. package/dist/api/hardware.d.ts +8 -31
  17. package/dist/api/hardware.js +19 -70
  18. package/dist/api/hardware.js.map +1 -1
  19. package/dist/api/index.d.ts +1 -1
  20. package/dist/api/index.js +1 -1
  21. package/dist/api/index.js.map +1 -1
  22. package/dist/api/media-input.d.ts +1 -1
  23. package/dist/api/media-input.js +3 -8
  24. package/dist/api/media-input.js.map +1 -1
  25. package/dist/api/media-output.d.ts +35 -128
  26. package/dist/api/media-output.js +136 -208
  27. package/dist/api/media-output.js.map +1 -1
  28. package/dist/api/pipeline.d.ts +17 -17
  29. package/dist/api/pipeline.js +19 -42
  30. package/dist/api/pipeline.js.map +1 -1
  31. package/dist/api/types.d.ts +17 -57
  32. package/dist/lib/dictionary.d.ts +2 -2
  33. package/dist/lib/dictionary.js +2 -2
  34. package/dist/lib/dictionary.js.map +1 -1
  35. package/dist/lib/filter-context.d.ts +19 -2
  36. package/dist/lib/filter-context.js +15 -0
  37. package/dist/lib/filter-context.js.map +1 -1
  38. package/dist/lib/format-context.d.ts +18 -18
  39. package/dist/lib/format-context.js +20 -20
  40. package/dist/lib/format-context.js.map +1 -1
  41. package/dist/lib/frame.d.ts +43 -1
  42. package/dist/lib/frame.js +53 -0
  43. package/dist/lib/frame.js.map +1 -1
  44. package/package.json +17 -17
  45. 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,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,25 +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 {Error} If encoder not found or timeBase not provided
95
96
  *
96
- * @throws {FFmpegError} If codec initialization fails
97
+ * @throws {FFmpegError} If codec allocation fails
97
98
  *
98
99
  * @example
99
100
  * ```typescript
100
101
  * // From decoder stream info
101
- * const streamInfo = decoder.getOutputStreamInfo();
102
- * const encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo, {
102
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
103
+ * timeBase: video.timeBase,
103
104
  * bitrate: '5M',
104
105
  * gopSize: 60,
105
106
  * options: {
@@ -113,12 +114,7 @@ export class Encoder {
113
114
  * ```typescript
114
115
  * // With custom stream info
115
116
  * 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
- * }, {
117
+ * timeBase: audio.timeBase,
122
118
  * bitrate: '192k'
123
119
  * });
124
120
  * ```
@@ -127,16 +123,16 @@ export class Encoder {
127
123
  * ```typescript
128
124
  * // Hardware encoder
129
125
  * const hw = HardwareContext.auto();
130
- * const encoder = await Encoder.create('hevc_videotoolbox', streamInfo, {
131
- * hardware: hw,
126
+ * const encoderCodec = hw?.getEncoderCodec('h264') ?? FF_ENCODER_H264_VIDEOTOOLBOX;
127
+ * const encoder = await Encoder.create(encoderCodec, {
128
+ * timeBase: video.timeBase,
132
129
  * bitrate: '8M'
133
130
  * });
134
131
  * ```
135
132
  *
136
- * @see {@link Decoder.getOutputStreamInfo} For stream info source
137
133
  * @see {@link EncoderOptions} For configuration options
138
134
  */
139
- static async create(encoderCodec, input, options = {}) {
135
+ static async create(encoderCodec, options) {
140
136
  let codec = null;
141
137
  let codecName = '';
142
138
  if (encoderCodec instanceof Codec) {
@@ -157,52 +153,6 @@ export class Encoder {
157
153
  // Allocate codec context
158
154
  const codecContext = new CodecContext();
159
155
  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
156
  // Apply encoder-specific options
207
157
  if (options.gopSize !== undefined) {
208
158
  codecContext.gopSize = options.gopSize;
@@ -215,32 +165,28 @@ export class Encoder {
215
165
  const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
216
166
  codecContext.bitRate = bitrate;
217
167
  }
218
- if (options.threads !== undefined) {
219
- 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;
220
171
  }
221
- // Override timeBase if explicitly specified in options
222
- if (options.timeBase) {
223
- 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;
224
175
  }
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
- }
176
+ if (options.bufSize !== undefined) {
177
+ const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
178
+ codecContext.rcBufferSize = Number(bufSize);
230
179
  }
231
- const isHWEncoder = codec.isHardwareAcceleratedEncoder();
232
- if (isHWEncoder && !options.hardware) {
233
- codecContext.freeContext();
234
- throw new Error(`Hardware encoder '${codecName}' requires a hardware context`);
180
+ if (options.threads !== undefined) {
181
+ codecContext.threadCount = options.threads;
235
182
  }
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');
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);
241
187
  }
242
- const encoder = new Encoder(codecContext, codec, isHWEncoder ? options.hardware : undefined);
243
- return encoder;
188
+ const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
189
+ return new Encoder(codecContext, codec, opts);
244
190
  }
245
191
  /**
246
192
  * Check if encoder is open.
@@ -253,7 +199,25 @@ export class Encoder {
253
199
  * ```
254
200
  */
255
201
  get isEncoderOpen() {
256
- 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;
257
221
  }
258
222
  /**
259
223
  * Check if encoder uses hardware acceleration.
@@ -270,14 +234,29 @@ export class Encoder {
270
234
  * @see {@link HardwareContext} For hardware setup
271
235
  */
272
236
  isHardware() {
273
- 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;
274
253
  }
275
254
  /**
276
255
  * Encode a frame to a packet.
277
256
  *
278
257
  * Sends a frame to the encoder and attempts to receive an encoded packet.
258
+ * On first frame, automatically initializes encoder with frame properties.
279
259
  * Handles internal buffering - may return null if more frames needed.
280
- * Automatically manages encoder state and hardware context binding.
281
260
  *
282
261
  * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
283
262
  *
@@ -315,21 +294,24 @@ export class Encoder {
315
294
  * @see {@link flush} For end-of-stream handling
316
295
  */
317
296
  async encode(frame) {
318
- if (!this.isOpen) {
297
+ if (this.isClosed) {
298
+ if (!frame) {
299
+ return null;
300
+ }
319
301
  throw new Error('Encoder is closed');
320
302
  }
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;
303
+ // Open encoder if not already done
304
+ if (!this.initialized) {
305
+ if (!frame) {
306
+ return null;
307
+ }
308
+ await this.initialize(frame);
327
309
  }
328
310
  // Send frame to encoder
329
311
  const sendRet = await this.codecContext.sendFrame(frame);
330
312
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
331
313
  // Encoder might be full, try to receive first
332
- const packet = await this.receivePacket();
314
+ const packet = await this.receive();
333
315
  if (packet)
334
316
  return packet;
335
317
  // If still failing, it's an error
@@ -338,7 +320,7 @@ export class Encoder {
338
320
  }
339
321
  }
340
322
  // Try to receive packet
341
- return await this.receivePacket();
323
+ return await this.receive();
342
324
  }
343
325
  /**
344
326
  * Encode frame stream to packet stream.
@@ -349,7 +331,7 @@ export class Encoder {
349
331
  * Primary interface for stream-based encoding.
350
332
  *
351
333
  * @param frames - Async iterable of frames (freed automatically)
352
- * @yields Encoded packets (caller must free)
334
+ * @yields {Packet} Encoded packets (caller must free)
353
335
  * @throws {Error} If encoder is closed
354
336
  *
355
337
  * @throws {FFmpegError} If encoding fails
@@ -368,11 +350,11 @@ export class Encoder {
368
350
  * // With frame filtering
369
351
  * async function* filteredFrames() {
370
352
  * for await (const frame of decoder.frames(input.packets())) {
371
- * await filter.filterFrame(frame);
372
- * const filtered = await filter.getFrame();
353
+ * const filtered = await filter.process(frame);
373
354
  * if (filtered) {
374
355
  * yield filtered;
375
356
  * }
357
+ * frame.free();
376
358
  * }
377
359
  * }
378
360
  *
@@ -400,9 +382,6 @@ export class Encoder {
400
382
  * @see {@link Decoder.frames} For frame source
401
383
  */
402
384
  async *packets(frames) {
403
- if (!this.isOpen) {
404
- throw new Error('Encoder is closed');
405
- }
406
385
  // Process frames
407
386
  for await (const frame of frames) {
408
387
  try {
@@ -417,29 +396,31 @@ export class Encoder {
417
396
  }
418
397
  }
419
398
  // Flush encoder after all frames
420
- let packet;
421
- while ((packet = await this.flush()) !== null) {
422
- yield packet;
399
+ await this.flush();
400
+ while (true) {
401
+ const remaining = await this.receive();
402
+ if (!remaining)
403
+ break;
404
+ yield remaining;
423
405
  }
424
406
  }
425
407
  /**
426
- * Flush encoder and get buffered packet.
408
+ * Flush encoder and signal end-of-stream.
427
409
  *
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.
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.
431
413
  *
432
414
  * Direct mapping to avcodec_send_frame(NULL).
433
415
  *
434
- * @returns Buffered packet or null if none remaining
435
- *
436
- * @throws {Error} If encoder is closed
437
- *
438
416
  * @example
439
417
  * ```typescript
440
- * // Flush remaining packets
418
+ * // Signal end of stream
419
+ * await encoder.flush();
420
+ *
421
+ * // Then get remaining packets
441
422
  * let packet;
442
- * while ((packet = await encoder.flush()) !== null) {
423
+ * while ((packet = await encoder.receive()) !== null) {
443
424
  * console.log('Got buffered packet');
444
425
  * await output.writePacket(packet);
445
426
  * packet.free();
@@ -447,26 +428,28 @@ export class Encoder {
447
428
  * ```
448
429
  *
449
430
  * @see {@link flushPackets} For async iteration
450
- * @see {@link packets} For complete encoding pipeline
431
+ * @see {@link receive} For getting buffered packets
451
432
  */
452
433
  async flush() {
453
- if (!this.isOpen) {
454
- throw new Error('Encoder is closed');
434
+ if (this.isClosed || !this.initialized) {
435
+ return;
455
436
  }
456
437
  // Send flush frame (null)
457
- await this.codecContext.sendFrame(null);
458
- // Receive packet
459
- 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
+ }
460
444
  }
461
445
  /**
462
446
  * Flush all buffered packets as async generator.
463
447
  *
464
448
  * Convenient async iteration over remaining packets.
465
- * Automatically handles repeated flush calls.
466
- * 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.
467
451
  *
468
- * @yields Buffered packets
469
- * @throws {Error} If encoder is closed
452
+ * @yields {Packet} Buffered packets
470
453
  *
471
454
  * @example
472
455
  * ```typescript
@@ -478,29 +461,86 @@ export class Encoder {
478
461
  * }
479
462
  * ```
480
463
  *
481
- * @see {@link flush} For single packet flush
464
+ * @see {@link flush} For signaling end-of-stream
482
465
  * @see {@link packets} For complete pipeline
483
466
  */
484
467
  async *flushPackets() {
485
- if (!this.isOpen) {
486
- throw new Error('Encoder is closed');
487
- }
468
+ // Send flush signal
469
+ await this.flush();
488
470
  let packet;
489
- while ((packet = await this.flush()) !== null) {
471
+ while ((packet = await this.receive()) !== null) {
490
472
  yield packet;
491
473
  }
492
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
+ }
493
534
  /**
494
535
  * Close encoder and free resources.
495
536
  *
496
537
  * Releases codec context and internal packet buffer.
497
538
  * Safe to call multiple times.
498
- * Does NOT dispose hardware context - caller is responsible.
499
539
  * Automatically called by Symbol.dispose.
500
540
  *
501
541
  * @example
502
542
  * ```typescript
503
- * const encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo);
543
+ * const encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
504
544
  * try {
505
545
  * // Use encoder
506
546
  * } finally {
@@ -511,11 +551,48 @@ export class Encoder {
511
551
  * @see {@link Symbol.dispose} For automatic cleanup
512
552
  */
513
553
  close() {
514
- if (!this.isOpen)
554
+ if (this.isClosed) {
515
555
  return;
556
+ }
557
+ this.isClosed = true;
516
558
  this.packet.free();
517
559
  this.codecContext.freeContext();
518
- 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;
519
596
  }
520
597
  /**
521
598
  * Get encoder codec.
@@ -525,14 +602,9 @@ export class Encoder {
525
602
  *
526
603
  * @returns Codec instance
527
604
  *
528
- * @example
529
- * ```typescript
530
- * const codec = encoder.getCodec();
531
- * console.log(`Using codec: ${codec.name}`);
532
- * console.log(`Capabilities: ${codec.capabilities}`);
533
- * ```
605
+ * @internal
534
606
  *
535
- * @see {@link Codec} For codec properties
607
+ * @see {@link Codec} For codec details
536
608
  */
537
609
  getCodec() {
538
610
  return this.codec;
@@ -540,47 +612,18 @@ export class Encoder {
540
612
  /**
541
613
  * Get underlying codec context.
542
614
  *
543
- * Returns the internal codec context for advanced operations.
544
- * 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.
545
618
  *
546
- * @returns Codec context or null
619
+ * @returns Codec context or null if closed/not initialized
547
620
  *
548
621
  * @internal
549
- */
550
- getCodecContext() {
551
- return this.isOpen ? this.codecContext : null;
552
- }
553
- /**
554
- * Receive packet from encoder.
555
- *
556
- * Internal method to get encoded packets from codec.
557
- * Handles packet cloning and error checking.
558
- *
559
- * Direct mapping to avcodec_receive_packet().
560
- *
561
- * @returns Cloned packet or null
562
- *
563
- * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
564
622
  *
565
- * @internal
623
+ * @see {@link CodecContext} For context details
566
624
  */
567
- async receivePacket() {
568
- // Clear previous packet data
569
- this.packet.unref();
570
- const ret = await this.codecContext.receivePacket(this.packet);
571
- if (ret === 0) {
572
- // Got a packet, clone it for the user
573
- return this.packet.clone();
574
- }
575
- else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
576
- // Need more data or end of stream
577
- return null;
578
- }
579
- else {
580
- // Error
581
- FFmpegError.throwIfError(ret, 'Failed to receive packet');
582
- return null;
583
- }
625
+ getCodecContext() {
626
+ return !this.isClosed && this.initialized ? this.codecContext : null;
584
627
  }
585
628
  /**
586
629
  * Dispose of encoder.
@@ -591,7 +634,7 @@ export class Encoder {
591
634
  * @example
592
635
  * ```typescript
593
636
  * {
594
- * using encoder = await Encoder.create(FF_ENCODER_LIBX264, streamInfo);
637
+ * using encoder = await Encoder.create(FF_ENCODER_LIBX264, { ... });
595
638
  * // Encode frames...
596
639
  * } // Automatically closed
597
640
  * ```