node-av 3.1.3 → 4.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 (156) hide show
  1. package/README.md +65 -52
  2. package/binding.gyp +4 -0
  3. package/dist/api/audio-frame-buffer.d.ts +201 -0
  4. package/dist/api/audio-frame-buffer.js +275 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +319 -78
  7. package/dist/api/bitstream-filter.js +680 -151
  8. package/dist/api/bitstream-filter.js.map +1 -1
  9. package/dist/api/constants.d.ts +44 -0
  10. package/dist/api/constants.js +45 -0
  11. package/dist/api/constants.js.map +1 -0
  12. package/dist/api/data/test_av1.ivf +0 -0
  13. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  14. package/dist/api/data/test_vp8.ivf +0 -0
  15. package/dist/api/data/test_vp9.ivf +0 -0
  16. package/dist/api/decoder.d.ts +279 -17
  17. package/dist/api/decoder.js +998 -209
  18. package/dist/api/decoder.js.map +1 -1
  19. package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
  20. package/dist/api/demuxer.js +1968 -0
  21. package/dist/api/demuxer.js.map +1 -0
  22. package/dist/api/encoder.d.ts +308 -50
  23. package/dist/api/encoder.js +1133 -111
  24. package/dist/api/encoder.js.map +1 -1
  25. package/dist/api/filter-presets.d.ts +12 -5
  26. package/dist/api/filter-presets.js +21 -7
  27. package/dist/api/filter-presets.js.map +1 -1
  28. package/dist/api/filter.d.ts +406 -40
  29. package/dist/api/filter.js +966 -139
  30. package/dist/api/filter.js.map +1 -1
  31. package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
  32. package/dist/api/fmp4-stream.js +539 -0
  33. package/dist/api/fmp4-stream.js.map +1 -0
  34. package/dist/api/hardware.d.ts +58 -6
  35. package/dist/api/hardware.js +127 -11
  36. package/dist/api/hardware.js.map +1 -1
  37. package/dist/api/index.d.ts +6 -4
  38. package/dist/api/index.js +14 -8
  39. package/dist/api/index.js.map +1 -1
  40. package/dist/api/io-stream.d.ts +3 -3
  41. package/dist/api/io-stream.js +5 -4
  42. package/dist/api/io-stream.js.map +1 -1
  43. package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
  44. package/dist/api/muxer.js +1934 -0
  45. package/dist/api/muxer.js.map +1 -0
  46. package/dist/api/pipeline.d.ts +77 -29
  47. package/dist/api/pipeline.js +435 -425
  48. package/dist/api/pipeline.js.map +1 -1
  49. package/dist/api/rtp-stream.d.ts +312 -0
  50. package/dist/api/rtp-stream.js +630 -0
  51. package/dist/api/rtp-stream.js.map +1 -0
  52. package/dist/api/types.d.ts +476 -55
  53. package/dist/api/utilities/async-queue.d.ts +91 -0
  54. package/dist/api/utilities/async-queue.js +162 -0
  55. package/dist/api/utilities/async-queue.js.map +1 -0
  56. package/dist/api/utilities/audio-sample.d.ts +1 -1
  57. package/dist/api/utilities/image.d.ts +1 -1
  58. package/dist/api/utilities/index.d.ts +2 -0
  59. package/dist/api/utilities/index.js +4 -0
  60. package/dist/api/utilities/index.js.map +1 -1
  61. package/dist/api/utilities/media-type.d.ts +1 -1
  62. package/dist/api/utilities/pixel-format.d.ts +1 -1
  63. package/dist/api/utilities/sample-format.d.ts +1 -1
  64. package/dist/api/utilities/scheduler.d.ts +169 -0
  65. package/dist/api/utilities/scheduler.js +136 -0
  66. package/dist/api/utilities/scheduler.js.map +1 -0
  67. package/dist/api/utilities/streaming.d.ts +74 -15
  68. package/dist/api/utilities/streaming.js +170 -12
  69. package/dist/api/utilities/streaming.js.map +1 -1
  70. package/dist/api/utilities/timestamp.d.ts +1 -1
  71. package/dist/api/webrtc-stream.d.ts +288 -0
  72. package/dist/api/webrtc-stream.js +440 -0
  73. package/dist/api/webrtc-stream.js.map +1 -0
  74. package/dist/constants/constants.d.ts +51 -1
  75. package/dist/constants/constants.js +47 -1
  76. package/dist/constants/constants.js.map +1 -1
  77. package/dist/constants/encoders.d.ts +2 -1
  78. package/dist/constants/encoders.js +4 -3
  79. package/dist/constants/encoders.js.map +1 -1
  80. package/dist/constants/hardware.d.ts +26 -0
  81. package/dist/constants/hardware.js +27 -0
  82. package/dist/constants/hardware.js.map +1 -0
  83. package/dist/constants/index.d.ts +1 -0
  84. package/dist/constants/index.js +1 -0
  85. package/dist/constants/index.js.map +1 -1
  86. package/dist/lib/binding.d.ts +19 -8
  87. package/dist/lib/binding.js.map +1 -1
  88. package/dist/lib/codec-context.d.ts +87 -0
  89. package/dist/lib/codec-context.js +125 -4
  90. package/dist/lib/codec-context.js.map +1 -1
  91. package/dist/lib/codec-parameters.d.ts +183 -1
  92. package/dist/lib/codec-parameters.js +209 -0
  93. package/dist/lib/codec-parameters.js.map +1 -1
  94. package/dist/lib/codec-parser.d.ts +23 -0
  95. package/dist/lib/codec-parser.js +25 -0
  96. package/dist/lib/codec-parser.js.map +1 -1
  97. package/dist/lib/codec.d.ts +26 -4
  98. package/dist/lib/codec.js +35 -0
  99. package/dist/lib/codec.js.map +1 -1
  100. package/dist/lib/dictionary.js +1 -0
  101. package/dist/lib/dictionary.js.map +1 -1
  102. package/dist/lib/error.js +1 -1
  103. package/dist/lib/error.js.map +1 -1
  104. package/dist/lib/filter-context.d.ts +52 -11
  105. package/dist/lib/filter-context.js +56 -12
  106. package/dist/lib/filter-context.js.map +1 -1
  107. package/dist/lib/filter-graph.d.ts +9 -0
  108. package/dist/lib/filter-graph.js +13 -0
  109. package/dist/lib/filter-graph.js.map +1 -1
  110. package/dist/lib/filter.d.ts +21 -0
  111. package/dist/lib/filter.js +28 -0
  112. package/dist/lib/filter.js.map +1 -1
  113. package/dist/lib/format-context.d.ts +48 -14
  114. package/dist/lib/format-context.js +76 -7
  115. package/dist/lib/format-context.js.map +1 -1
  116. package/dist/lib/frame.d.ts +168 -0
  117. package/dist/lib/frame.js +212 -0
  118. package/dist/lib/frame.js.map +1 -1
  119. package/dist/lib/hardware-device-context.d.ts +3 -2
  120. package/dist/lib/hardware-device-context.js.map +1 -1
  121. package/dist/lib/index.d.ts +1 -0
  122. package/dist/lib/index.js +2 -0
  123. package/dist/lib/index.js.map +1 -1
  124. package/dist/lib/input-format.d.ts +21 -0
  125. package/dist/lib/input-format.js +42 -2
  126. package/dist/lib/input-format.js.map +1 -1
  127. package/dist/lib/native-types.d.ts +48 -26
  128. package/dist/lib/option.d.ts +25 -13
  129. package/dist/lib/option.js +28 -0
  130. package/dist/lib/option.js.map +1 -1
  131. package/dist/lib/output-format.d.ts +22 -1
  132. package/dist/lib/output-format.js +28 -0
  133. package/dist/lib/output-format.js.map +1 -1
  134. package/dist/lib/packet.d.ts +35 -0
  135. package/dist/lib/packet.js +52 -2
  136. package/dist/lib/packet.js.map +1 -1
  137. package/dist/lib/stream.d.ts +126 -0
  138. package/dist/lib/stream.js +188 -5
  139. package/dist/lib/stream.js.map +1 -1
  140. package/dist/lib/sync-queue.d.ts +179 -0
  141. package/dist/lib/sync-queue.js +197 -0
  142. package/dist/lib/sync-queue.js.map +1 -0
  143. package/dist/lib/types.d.ts +27 -1
  144. package/dist/lib/utilities.d.ts +281 -53
  145. package/dist/lib/utilities.js +298 -55
  146. package/dist/lib/utilities.js.map +1 -1
  147. package/package.json +20 -19
  148. package/dist/api/fmp4.js +0 -710
  149. package/dist/api/fmp4.js.map +0 -1
  150. package/dist/api/media-input.js +0 -1075
  151. package/dist/api/media-input.js.map +0 -1
  152. package/dist/api/media-output.js +0 -1040
  153. package/dist/api/media-output.js.map +0 -1
  154. package/dist/api/webrtc.d.ts +0 -664
  155. package/dist/api/webrtc.js +0 -1132
  156. package/dist/api/webrtc.js.map +0 -1
@@ -1,5 +1,67 @@
1
- import { AVERROR_EAGAIN, AVERROR_EOF, AVMEDIA_TYPE_AUDIO } from '../constants/constants.js';
2
- import { Codec, CodecContext, Dictionary, FFmpegError, Packet, Rational } from '../lib/index.js';
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, AV_CODEC_CAP_PARAM_CHANGE, AV_CODEC_FLAG_COPY_OPAQUE, AV_CODEC_FLAG_FRAME_DURATION, AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, AV_PIX_FMT_NONE, AV_PKT_FLAG_TRUSTED, AVCHROMA_LOC_UNSPECIFIED, AVERROR_EAGAIN, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, } from '../constants/constants.js';
54
+ import { CodecContext } from '../lib/codec-context.js';
55
+ import { Codec } from '../lib/codec.js';
56
+ import { Dictionary } from '../lib/dictionary.js';
57
+ import { FFmpegError } from '../lib/error.js';
58
+ import { Packet } from '../lib/packet.js';
59
+ import { Rational } from '../lib/rational.js';
60
+ import { avRescaleQ } from '../lib/utilities.js';
61
+ import { AudioFrameBuffer } from './audio-frame-buffer.js';
62
+ import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
63
+ import { AsyncQueue } from './utilities/async-queue.js';
64
+ import { SchedulerControl } from './utilities/scheduler.js';
3
65
  import { parseBitrate } from './utils.js';
4
66
  /**
5
67
  * High-level encoder for audio and video streams.
@@ -56,17 +118,24 @@ import { parseBitrate } from './utils.js';
56
118
  * ```
57
119
  *
58
120
  * @see {@link Decoder} For decoding packets to frames
59
- * @see {@link MediaOutput} For writing encoded packets
121
+ * @see {@link Muxer} For writing encoded packets
60
122
  * @see {@link HardwareContext} For GPU acceleration
61
123
  */
62
124
  export class Encoder {
63
125
  codecContext;
64
126
  packet;
65
127
  codec;
128
+ initializePromise = null;
66
129
  initialized = false;
67
130
  isClosed = false;
68
131
  opts;
69
132
  options;
133
+ audioFrameBuffer;
134
+ // Worker pattern for push-based processing
135
+ inputQueue;
136
+ outputQueue;
137
+ workerPromise = null;
138
+ pipeToPromise = null;
70
139
  /**
71
140
  * @param codecContext - Configured codec context
72
141
  *
@@ -85,6 +154,8 @@ export class Encoder {
85
154
  this.opts = opts;
86
155
  this.packet = new Packet();
87
156
  this.packet.alloc();
157
+ this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
158
+ this.outputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE);
88
159
  }
89
160
  /**
90
161
  * Create an encoder with specified codec and options.
@@ -97,7 +168,7 @@ export class Encoder {
97
168
  *
98
169
  * @param encoderCodec - Codec name, ID, or instance to use for encoding
99
170
  *
100
- * @param options - Encoder configuration options including required timeBase
171
+ * @param options - Optional encoder configuration options including required timeBase
101
172
  *
102
173
  * @returns Configured encoder instance
103
174
  *
@@ -138,8 +209,9 @@ export class Encoder {
138
209
  * ```
139
210
  *
140
211
  * @see {@link EncoderOptions} For configuration options
212
+ * @see {@link createSync} For synchronous version
141
213
  */
142
- static async create(encoderCodec, options) {
214
+ static async create(encoderCodec, options = {}) {
143
215
  let codec = null;
144
216
  let codecName = '';
145
217
  if (encoderCodec instanceof Codec) {
@@ -187,14 +259,6 @@ export class Encoder {
187
259
  const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
188
260
  codecContext.rcBufferSize = Number(bufSize);
189
261
  }
190
- if (options.threads !== undefined) {
191
- codecContext.threadCount = options.threads;
192
- }
193
- codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
194
- codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
195
- if (options.frameRate) {
196
- codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
197
- }
198
262
  const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
199
263
  return new Encoder(codecContext, codec, options, opts);
200
264
  }
@@ -210,7 +274,7 @@ export class Encoder {
210
274
  *
211
275
  * @param encoderCodec - Codec name, ID, or instance to use for encoding
212
276
  *
213
- * @param options - Encoder configuration options including required timeBase
277
+ * @param options - Optional encoder configuration options including required timeBase
214
278
  *
215
279
  * @returns Configured encoder instance
216
280
  *
@@ -252,9 +316,10 @@ export class Encoder {
252
316
  * });
253
317
  * ```
254
318
  *
319
+ * @see {@link EncoderOptions} For configuration options
255
320
  * @see {@link create} For async version
256
321
  */
257
- static createSync(encoderCodec, options) {
322
+ static createSync(encoderCodec, options = {}) {
258
323
  let codec = null;
259
324
  let codecName = '';
260
325
  if (encoderCodec instanceof Codec) {
@@ -301,14 +366,6 @@ export class Encoder {
301
366
  const bufSize = typeof options.bufSize === 'string' ? parseBitrate(options.bufSize) : BigInt(options.bufSize);
302
367
  codecContext.rcBufferSize = Number(bufSize);
303
368
  }
304
- if (options.threads !== undefined) {
305
- codecContext.threadCount = options.threads;
306
- }
307
- if (options.frameRate) {
308
- codecContext.framerate = new Rational(options.frameRate.num, options.frameRate.den);
309
- }
310
- codecContext.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
311
- codecContext.pktTimebase = new Rational(options.timeBase.num, options.timeBase.den);
312
369
  const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
313
370
  return new Encoder(codecContext, codec, options, opts);
314
371
  }
@@ -343,6 +400,115 @@ export class Encoder {
343
400
  get isEncoderInitialized() {
344
401
  return this.initialized;
345
402
  }
403
+ /**
404
+ * Codec flags.
405
+ *
406
+ * @returns Current codec flags
407
+ *
408
+ * @throws {Error} If encoder is closed
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * const flags = encoder.codecFlags;
413
+ * console.log('Current flags:', flags);
414
+ * ```
415
+ *
416
+ * @see {@link setCodecFlags} To set flags
417
+ * @see {@link clearCodecFlags} To clear flags
418
+ * @see {@link hasCodecFlags} To check flags
419
+ */
420
+ get codecFlags() {
421
+ if (this.isClosed) {
422
+ throw new Error('Cannot get flags on closed encoder');
423
+ }
424
+ return this.codecContext.flags;
425
+ }
426
+ /**
427
+ * Set codec flags.
428
+ *
429
+ * @param flags - One or more flag values to set
430
+ *
431
+ * @throws {Error} If encoder is already initialized or closed
432
+ *
433
+ * @example
434
+ * ```typescript
435
+ * import { AV_CODEC_FLAG_GLOBAL_HEADER, AV_CODEC_FLAG_QSCALE } from 'node-av/constants';
436
+ *
437
+ * // Set multiple flags before initialization
438
+ * encoder.setCodecFlags(AV_CODEC_FLAG_GLOBAL_HEADER, AV_CODEC_FLAG_QSCALE);
439
+ * ```
440
+ *
441
+ * @see {@link clearCodecFlags} To clear flags
442
+ * @see {@link hasCodecFlags} To check flags
443
+ * @see {@link codecFlags} For direct flag access
444
+ */
445
+ setCodecFlags(...flags) {
446
+ if (this.isClosed) {
447
+ throw new Error('Cannot set flags on closed encoder');
448
+ }
449
+ if (this.initialized) {
450
+ throw new Error('Cannot set flags on already initialized encoder');
451
+ }
452
+ this.codecContext.setFlags(...flags);
453
+ }
454
+ /**
455
+ * Clear codec flags.
456
+ *
457
+ * @param flags - One or more flag values to clear
458
+ *
459
+ * @throws {Error} If encoder is already initialized or closed
460
+ *
461
+ * @example
462
+ * ```typescript
463
+ * import { AV_CODEC_FLAG_QSCALE } from 'node-av/constants';
464
+ *
465
+ * // Clear specific flag before initialization
466
+ * encoder.clearCodecFlags(AV_CODEC_FLAG_QSCALE);
467
+ * ```
468
+ *
469
+ * @see {@link setCodecFlags} To set flags
470
+ * @see {@link hasCodecFlags} To check flags
471
+ * @see {@link codecFlags} For direct flag access
472
+ */
473
+ clearCodecFlags(...flags) {
474
+ if (this.isClosed) {
475
+ throw new Error('Cannot clear flags on closed encoder');
476
+ }
477
+ if (this.initialized) {
478
+ throw new Error('Cannot clear flags on already initialized encoder');
479
+ }
480
+ this.codecContext.clearFlags(...flags);
481
+ }
482
+ /**
483
+ * Check if codec has specific flags.
484
+ *
485
+ * Tests whether all specified codec flags are set using bitwise AND.
486
+ *
487
+ * @param flags - One or more flag values to check
488
+ *
489
+ * @returns true if all specified flags are set, false otherwise
490
+ *
491
+ * @throws {Error} If encoder is closed
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * import { AV_CODEC_FLAG_GLOBAL_HEADER } from 'node-av/constants';
496
+ *
497
+ * if (encoder.hasCodecFlags(AV_CODEC_FLAG_GLOBAL_HEADER)) {
498
+ * console.log('Global header flag is set');
499
+ * }
500
+ * ```
501
+ *
502
+ * @see {@link setCodecFlags} To set flags
503
+ * @see {@link clearCodecFlags} To clear flags
504
+ * @see {@link codecFlags} For direct flag access
505
+ */
506
+ hasCodecFlags(...flags) {
507
+ if (this.isClosed) {
508
+ throw new Error('Cannot check flags on closed encoder');
509
+ }
510
+ return this.codecContext.hasFlags(...flags);
511
+ }
346
512
  /**
347
513
  * Check if encoder uses hardware acceleration.
348
514
  *
@@ -382,6 +548,10 @@ export class Encoder {
382
548
  * On first frame, automatically initializes encoder with frame properties.
383
549
  * Handles internal buffering - may return null if more frames needed.
384
550
  *
551
+ * **Note**: This method receives only ONE packet per call.
552
+ * A single frame can produce multiple packets (e.g., B-frames, codec buffering).
553
+ * To receive all packets from a frame, use {@link encodeAll} or {@link packets} instead.
554
+ *
385
555
  * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
386
556
  *
387
557
  * @param frame - Raw frame to encode (or null to flush)
@@ -413,8 +583,10 @@ export class Encoder {
413
583
  * }
414
584
  * ```
415
585
  *
586
+ * @see {@link encodeAll} For multiple packet encoding
416
587
  * @see {@link packets} For automatic frame iteration
417
588
  * @see {@link flush} For end-of-stream handling
589
+ * @see {@link encodeSync} For synchronous version
418
590
  */
419
591
  async encode(frame) {
420
592
  if (this.isClosed) {
@@ -425,20 +597,30 @@ export class Encoder {
425
597
  if (!frame) {
426
598
  return null;
427
599
  }
428
- await this.initialize(frame);
600
+ this.initializePromise ??= this.initialize(frame);
601
+ }
602
+ await this.initializePromise;
603
+ // Prepare frame for encoding (set quality, validate channel count)
604
+ if (frame) {
605
+ this.prepareFrameForEncoding(frame);
429
606
  }
430
607
  // Send frame to encoder
431
608
  const sendRet = await this.codecContext.sendFrame(frame);
432
- if (sendRet < 0 && sendRet !== AVERROR_EOF) {
433
- // Encoder might be full, try to receive first
609
+ // Handle EAGAIN: encoder buffer is full, need to read packets first
610
+ // Unlike FFmpeg CLI which reads ALL packets in a loop, our encode() returns
611
+ // only one packet at a time. This means the encoder can still have packets
612
+ // from previous frames when we try to send a new frame.
613
+ if (sendRet === AVERROR_EAGAIN) {
614
+ // Encoder is full, receive a packet first
434
615
  const packet = await this.receive();
435
616
  if (packet) {
436
617
  return packet;
437
618
  }
438
- // If still failing, it's an error
439
- if (sendRet !== AVERROR_EAGAIN) {
440
- FFmpegError.throwIfError(sendRet, 'Failed to send frame');
441
- }
619
+ // If receive() returned null, this is unexpected - treat as error
620
+ throw new Error('Encoder returned EAGAIN but no packet available');
621
+ }
622
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
623
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
442
624
  }
443
625
  // Try to receive packet
444
626
  return await this.receive();
@@ -451,6 +633,10 @@ export class Encoder {
451
633
  * On first frame, automatically initializes encoder with frame properties.
452
634
  * Handles internal buffering - may return null if more frames needed.
453
635
  *
636
+ * **Note**: This method receives only ONE packet per call.
637
+ * A single frame can produce multiple packets (e.g., B-frames, codec buffering).
638
+ * To receive all packets from a frame, use {@link encodeAllSync} or {@link packetsSync} instead.
639
+ *
454
640
  * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
455
641
  *
456
642
  * @param frame - Raw frame to encode (or null to flush)
@@ -482,6 +668,9 @@ export class Encoder {
482
668
  * }
483
669
  * ```
484
670
  *
671
+ * @see {@link encodeAllSync} For multiple packet encoding
672
+ * @see {@link packetsSync} For automatic frame iteration
673
+ * @see {@link flushSync} For end-of-stream handling
485
674
  * @see {@link encode} For async version
486
675
  */
487
676
  encodeSync(frame) {
@@ -495,21 +684,251 @@ export class Encoder {
495
684
  }
496
685
  this.initializeSync(frame);
497
686
  }
687
+ // Prepare frame for encoding (set quality, validate channel count)
688
+ if (frame) {
689
+ this.prepareFrameForEncoding(frame);
690
+ }
498
691
  // Send frame to encoder
499
692
  const sendRet = this.codecContext.sendFrameSync(frame);
500
- if (sendRet < 0 && sendRet !== AVERROR_EOF) {
501
- // Encoder might be full, try to receive first
693
+ // Handle EAGAIN: encoder buffer is full, need to read packets first
694
+ // Unlike FFmpeg CLI which reads ALL packets in a loop, our encode() returns
695
+ // only one packet at a time. This means the encoder can still have packets
696
+ // from previous frames when we try to send a new frame.
697
+ if (sendRet === AVERROR_EAGAIN) {
698
+ // Encoder is full, receive a packet first
502
699
  const packet = this.receiveSync();
503
- if (packet)
700
+ if (packet) {
504
701
  return packet;
505
- // If still failing, it's an error
506
- if (sendRet !== AVERROR_EAGAIN) {
507
- FFmpegError.throwIfError(sendRet, 'Failed to send frame');
508
702
  }
703
+ // If receive() returned null, this is unexpected - treat as error
704
+ throw new Error('Encoder returned EAGAIN but no packet available');
705
+ }
706
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
707
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
509
708
  }
510
709
  // Try to receive packet
511
710
  return this.receiveSync();
512
711
  }
712
+ /**
713
+ * Encode a frame to packets.
714
+ *
715
+ * Sends a frame to the encoder and receives all available encoded packets.
716
+ * Returns array of packets - may be empty if encoder needs more data.
717
+ * On first frame, automatically initializes encoder with frame properties.
718
+ * One frame can produce zero, one, or multiple packets depending on codec.
719
+ *
720
+ * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
721
+ *
722
+ * @param frame - Raw frame to encode (or null to flush)
723
+ *
724
+ * @returns Array of encoded packets (empty if more data needed or encoder is closed)
725
+ *
726
+ * @throws {FFmpegError} If encoding fails
727
+ *
728
+ * @example
729
+ * ```typescript
730
+ * const packets = await encoder.encodeAll(frame);
731
+ * for (const packet of packets) {
732
+ * console.log(`Encoded packet with PTS: ${packet.pts}`);
733
+ * await output.writePacket(packet);
734
+ * packet.free();
735
+ * }
736
+ * ```
737
+ *
738
+ * @example
739
+ * ```typescript
740
+ * // Encode loop
741
+ * for await (const frame of decoder.frames(input.packets())) {
742
+ * const packets = await encoder.encodeAll(frame);
743
+ * for (const packet of packets) {
744
+ * await output.writePacket(packet);
745
+ * packet.free();
746
+ * }
747
+ * frame.free();
748
+ * }
749
+ * ```
750
+ *
751
+ * @see {@link encode} For single packet encoding
752
+ * @see {@link packets} For automatic frame iteration
753
+ * @see {@link flush} For end-of-stream handling
754
+ * @see {@link encodeAllSync} For synchronous version
755
+ */
756
+ async encodeAll(frame) {
757
+ if (this.isClosed) {
758
+ return [];
759
+ }
760
+ // Open encoder if not already done
761
+ if (!this.initialized) {
762
+ if (!frame) {
763
+ return [];
764
+ }
765
+ this.initializePromise ??= this.initialize(frame);
766
+ }
767
+ await this.initializePromise;
768
+ // Prepare frame for encoding (set quality, validate channel count)
769
+ if (frame) {
770
+ this.prepareFrameForEncoding(frame);
771
+ }
772
+ // If audio encoder with fixed frame size, use AudioFrameBuffer
773
+ if (this.audioFrameBuffer && frame) {
774
+ // Push frame into buffer
775
+ await this.audioFrameBuffer.push(frame);
776
+ // Pull and encode all available fixed-size frames
777
+ const packets = [];
778
+ let _bufferedFrame;
779
+ while (!this.isClosed && (_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
780
+ const env_1 = { stack: [], error: void 0, hasError: false };
781
+ try {
782
+ const bufferedFrame = __addDisposableResource(env_1, _bufferedFrame, false);
783
+ // Send buffered frame to encoder
784
+ const sendRet = await this.codecContext.sendFrame(bufferedFrame);
785
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
786
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
787
+ }
788
+ // Receive packets
789
+ while (true) {
790
+ const packet = await this.receive();
791
+ if (!packet)
792
+ break;
793
+ packets.push(packet);
794
+ }
795
+ }
796
+ catch (e_1) {
797
+ env_1.error = e_1;
798
+ env_1.hasError = true;
799
+ }
800
+ finally {
801
+ __disposeResources(env_1);
802
+ }
803
+ }
804
+ return packets;
805
+ }
806
+ // Send frame first, error immediately if send fails
807
+ const sendRet = await this.codecContext.sendFrame(frame);
808
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
809
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame to encoder');
810
+ return [];
811
+ }
812
+ // Receive all available packets
813
+ const packets = [];
814
+ while (true) {
815
+ const packet = await this.receive();
816
+ if (!packet)
817
+ break;
818
+ packets.push(packet);
819
+ }
820
+ return packets;
821
+ }
822
+ /**
823
+ * Encode a frame to packets synchronously.
824
+ * Synchronous version of encodeAll.
825
+ *
826
+ * Sends a frame to the encoder and receives all available encoded packets.
827
+ * Returns array of packets - may be empty if encoder needs more data.
828
+ * On first frame, automatically initializes encoder with frame properties.
829
+ * One frame can produce zero, one, or multiple packets depending on codec.
830
+ *
831
+ * Direct mapping to avcodec_send_frame() and avcodec_receive_packet().
832
+ *
833
+ * @param frame - Raw frame to encode (or null to flush)
834
+ *
835
+ * @returns Array of encoded packets (empty if more data needed or encoder is closed)
836
+ *
837
+ * @throws {FFmpegError} If encoding fails
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * const packets = encoder.encodeAllSync(frame);
842
+ * for (const packet of packets) {
843
+ * console.log(`Encoded packet with PTS: ${packet.pts}`);
844
+ * output.writePacketSync(packet);
845
+ * packet.free();
846
+ * }
847
+ * ```
848
+ *
849
+ * @example
850
+ * ```typescript
851
+ * // Encode loop
852
+ * for (const frame of decoder.framesSync(packets)) {
853
+ * const packets = encoder.encodeAllSync(frame);
854
+ * for (const packet of packets) {
855
+ * output.writePacketSync(packet);
856
+ * packet.free();
857
+ * }
858
+ * frame.free();
859
+ * }
860
+ * ```
861
+ *
862
+ * @see {@link encodeSync} For single packet encoding
863
+ * @see {@link packetsSync} For automatic frame iteration
864
+ * @see {@link flushSync} For end-of-stream handling
865
+ * @see {@link encodeAll} For async version
866
+ */
867
+ encodeAllSync(frame) {
868
+ if (this.isClosed) {
869
+ return [];
870
+ }
871
+ // Open encoder if not already done
872
+ if (!this.initialized) {
873
+ if (!frame) {
874
+ return [];
875
+ }
876
+ this.initializeSync(frame);
877
+ }
878
+ // Prepare frame for encoding (set quality, validate channel count)
879
+ if (frame) {
880
+ this.prepareFrameForEncoding(frame);
881
+ }
882
+ // If audio encoder with fixed frame size, use AudioFrameBuffer
883
+ if (this.audioFrameBuffer && frame) {
884
+ // Push frame into buffer
885
+ this.audioFrameBuffer.pushSync(frame);
886
+ // Pull and encode all available fixed-size frames
887
+ const packets = [];
888
+ let _bufferedFrame;
889
+ while (!this.isClosed && (_bufferedFrame = this.audioFrameBuffer.pullSync()) !== null) {
890
+ const env_2 = { stack: [], error: void 0, hasError: false };
891
+ try {
892
+ const bufferedFrame = __addDisposableResource(env_2, _bufferedFrame, false);
893
+ // Send buffered frame to encoder
894
+ const sendRet = this.codecContext.sendFrameSync(bufferedFrame);
895
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
896
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
897
+ }
898
+ // Receive packets
899
+ while (true) {
900
+ const packet = this.receiveSync();
901
+ if (!packet)
902
+ break;
903
+ packets.push(packet);
904
+ }
905
+ }
906
+ catch (e_2) {
907
+ env_2.error = e_2;
908
+ env_2.hasError = true;
909
+ }
910
+ finally {
911
+ __disposeResources(env_2);
912
+ }
913
+ }
914
+ return packets;
915
+ }
916
+ // Send frame first, error immediately if send fails
917
+ const sendRet = this.codecContext.sendFrameSync(frame);
918
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
919
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame to encoder');
920
+ return [];
921
+ }
922
+ // Receive all available packets
923
+ const packets = [];
924
+ while (true) {
925
+ const packet = this.receiveSync();
926
+ if (!packet)
927
+ break;
928
+ packets.push(packet);
929
+ }
930
+ return packets;
931
+ }
513
932
  /**
514
933
  * Encode frame stream to packet stream.
515
934
  *
@@ -568,29 +987,106 @@ export class Encoder {
568
987
  *
569
988
  * @see {@link encode} For single frame encoding
570
989
  * @see {@link Decoder.frames} For frame source
990
+ * @see {@link packetsSync} For sync version
571
991
  */
572
992
  async *packets(frames) {
573
993
  // Process frames
574
- for await (const frame of frames) {
994
+ for await (const frame_1 of frames) {
995
+ const env_3 = { stack: [], error: void 0, hasError: false };
575
996
  try {
576
- const packet = await this.encode(frame);
577
- if (packet) {
578
- yield packet;
997
+ const frame = __addDisposableResource(env_3, frame_1, false);
998
+ // Handle EOF signal
999
+ if (frame === null) {
1000
+ // Flush encoder (audio frame buffer doesn't need explicit flush)
1001
+ await this.flush();
1002
+ while (true) {
1003
+ const remaining = await this.receive();
1004
+ if (!remaining)
1005
+ break;
1006
+ yield remaining;
1007
+ }
1008
+ // Signal EOF and stop processing
1009
+ yield null;
1010
+ return;
1011
+ }
1012
+ if (this.isClosed) {
1013
+ break;
1014
+ }
1015
+ // Open encoder if not already done
1016
+ if (!this.initialized) {
1017
+ this.initializePromise ??= this.initialize(frame);
1018
+ }
1019
+ await this.initializePromise;
1020
+ // Prepare frame for encoding (set quality, validate channel count)
1021
+ if (frame) {
1022
+ this.prepareFrameForEncoding(frame);
579
1023
  }
1024
+ // If audio encoder with fixed frame size, use AudioFrameBuffer
1025
+ if (this.audioFrameBuffer) {
1026
+ // Push frame into buffer
1027
+ await this.audioFrameBuffer.push(frame);
1028
+ // Pull and encode all available fixed-size frames
1029
+ let _bufferedFrame;
1030
+ while (!this.isClosed && (_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
1031
+ const env_4 = { stack: [], error: void 0, hasError: false };
1032
+ try {
1033
+ const bufferedFrame = __addDisposableResource(env_4, _bufferedFrame, false);
1034
+ // Send buffered frame to encoder
1035
+ const sendRet = await this.codecContext.sendFrame(bufferedFrame);
1036
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1037
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1038
+ }
1039
+ // Receive packets
1040
+ while (true) {
1041
+ const packet = await this.receive();
1042
+ if (!packet)
1043
+ break;
1044
+ yield packet;
1045
+ }
1046
+ }
1047
+ catch (e_3) {
1048
+ env_4.error = e_3;
1049
+ env_4.hasError = true;
1050
+ }
1051
+ finally {
1052
+ __disposeResources(env_4);
1053
+ }
1054
+ }
1055
+ }
1056
+ else {
1057
+ // Send frame to encoder
1058
+ const sendRet = await this.codecContext.sendFrame(frame);
1059
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1060
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1061
+ }
1062
+ // Receive ALL packets
1063
+ // A single frame can produce multiple packets (e.g., B-frames, lookahead)
1064
+ while (true) {
1065
+ const packet = await this.receive();
1066
+ if (!packet)
1067
+ break;
1068
+ yield packet;
1069
+ }
1070
+ }
1071
+ }
1072
+ catch (e_4) {
1073
+ env_3.error = e_4;
1074
+ env_3.hasError = true;
580
1075
  }
581
1076
  finally {
582
- // Free the input frame after encoding
583
- frame.free();
1077
+ __disposeResources(env_3);
584
1078
  }
585
1079
  }
586
- // Flush encoder after all frames
1080
+ // Flush encoder after all frames (fallback if no null was sent)
587
1081
  await this.flush();
588
- while (!this.isClosed) {
1082
+ while (true) {
589
1083
  const remaining = await this.receive();
590
1084
  if (!remaining)
591
1085
  break;
592
1086
  yield remaining;
593
1087
  }
1088
+ // Signal EOF
1089
+ yield null;
594
1090
  }
595
1091
  /**
596
1092
  * Encode frame stream to packet stream synchronously.
@@ -635,30 +1131,107 @@ export class Encoder {
635
1131
  * }
636
1132
  * ```
637
1133
  *
1134
+ * @see {@link encodeSync} For single frame encoding
1135
+ * @see {@link Decoder.framesSync} For frame source
638
1136
  * @see {@link packets} For async version
639
1137
  */
640
1138
  *packetsSync(frames) {
641
1139
  // Process frames
642
- for (const frame of frames) {
1140
+ for (const frame_2 of frames) {
1141
+ const env_5 = { stack: [], error: void 0, hasError: false };
643
1142
  try {
644
- const packet = this.encodeSync(frame);
645
- if (packet) {
646
- yield packet;
1143
+ const frame = __addDisposableResource(env_5, frame_2, false);
1144
+ // Handle EOF signal
1145
+ if (frame === null) {
1146
+ // Flush encoder (audio frame buffer doesn't need explicit flush)
1147
+ this.flushSync();
1148
+ while (true) {
1149
+ const remaining = this.receiveSync();
1150
+ if (!remaining)
1151
+ break;
1152
+ yield remaining;
1153
+ }
1154
+ // Signal EOF and stop processing
1155
+ yield null;
1156
+ return;
1157
+ }
1158
+ if (this.isClosed) {
1159
+ break;
1160
+ }
1161
+ // Open encoder if not already done
1162
+ if (!this.initialized) {
1163
+ this.initializeSync(frame);
1164
+ }
1165
+ // Prepare frame for encoding (set quality, validate channel count)
1166
+ if (frame) {
1167
+ this.prepareFrameForEncoding(frame);
647
1168
  }
1169
+ // If audio encoder with fixed frame size, use AudioFrameBuffer
1170
+ if (this.audioFrameBuffer) {
1171
+ // Push frame into buffer
1172
+ this.audioFrameBuffer.pushSync(frame);
1173
+ // Pull and encode all available fixed-size frames
1174
+ let _bufferedFrame;
1175
+ while (!this.isClosed && (_bufferedFrame = this.audioFrameBuffer.pullSync()) !== null) {
1176
+ const env_6 = { stack: [], error: void 0, hasError: false };
1177
+ try {
1178
+ const bufferedFrame = __addDisposableResource(env_6, _bufferedFrame, false);
1179
+ // Send buffered frame to encoder
1180
+ const sendRet = this.codecContext.sendFrameSync(bufferedFrame);
1181
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1182
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1183
+ }
1184
+ // Receive packets
1185
+ while (true) {
1186
+ const packet = this.receiveSync();
1187
+ if (!packet)
1188
+ break;
1189
+ yield packet;
1190
+ }
1191
+ }
1192
+ catch (e_5) {
1193
+ env_6.error = e_5;
1194
+ env_6.hasError = true;
1195
+ }
1196
+ finally {
1197
+ __disposeResources(env_6);
1198
+ }
1199
+ }
1200
+ }
1201
+ else {
1202
+ // Send frame to encoder
1203
+ const sendRet = this.codecContext.sendFrameSync(frame);
1204
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1205
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1206
+ }
1207
+ // Receive ALL packets
1208
+ // A single frame can produce multiple packets (e.g., B-frames, lookahead)
1209
+ while (true) {
1210
+ const packet = this.receiveSync();
1211
+ if (!packet)
1212
+ break;
1213
+ yield packet;
1214
+ }
1215
+ }
1216
+ }
1217
+ catch (e_6) {
1218
+ env_5.error = e_6;
1219
+ env_5.hasError = true;
648
1220
  }
649
1221
  finally {
650
- // Free the input frame after encoding
651
- frame.free();
1222
+ __disposeResources(env_5);
652
1223
  }
653
1224
  }
654
- // Flush encoder after all frames
1225
+ // Flush encoder after all frames (fallback if no null was sent)
655
1226
  this.flushSync();
656
- while (!this.isClosed) {
1227
+ while (true) {
657
1228
  const remaining = this.receiveSync();
658
1229
  if (!remaining)
659
1230
  break;
660
1231
  yield remaining;
661
1232
  }
1233
+ // Signal EOF
1234
+ yield null;
662
1235
  }
663
1236
  /**
664
1237
  * Flush encoder and signal end-of-stream.
@@ -685,11 +1258,32 @@ export class Encoder {
685
1258
  *
686
1259
  * @see {@link flushPackets} For async iteration
687
1260
  * @see {@link receive} For getting buffered packets
1261
+ * @see {@link flushSync} For synchronous version
688
1262
  */
689
1263
  async flush() {
690
1264
  if (this.isClosed || !this.initialized) {
691
1265
  return;
692
1266
  }
1267
+ // If using AudioFrameBuffer, flush remaining buffered samples first
1268
+ if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1269
+ // Pull any remaining partial frame (may be less than frameSize)
1270
+ // For the final frame, we pad or truncate as needed
1271
+ let _bufferedFrame;
1272
+ while (!this.isClosed && (_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
1273
+ const env_7 = { stack: [], error: void 0, hasError: false };
1274
+ try {
1275
+ const bufferedFrame = __addDisposableResource(env_7, _bufferedFrame, false);
1276
+ await this.codecContext.sendFrame(bufferedFrame);
1277
+ }
1278
+ catch (e_7) {
1279
+ env_7.error = e_7;
1280
+ env_7.hasError = true;
1281
+ }
1282
+ finally {
1283
+ __disposeResources(env_7);
1284
+ }
1285
+ }
1286
+ }
693
1287
  // Send flush frame (null)
694
1288
  const ret = await this.codecContext.sendFrame(null);
695
1289
  if (ret < 0 && ret !== AVERROR_EOF) {
@@ -722,12 +1316,34 @@ export class Encoder {
722
1316
  * }
723
1317
  * ```
724
1318
  *
1319
+ * @see {@link flushPacketsSync} For sync iteration
1320
+ * @see {@link receiveSync} For getting buffered packets
725
1321
  * @see {@link flush} For async version
726
1322
  */
727
1323
  flushSync() {
728
1324
  if (this.isClosed || !this.initialized) {
729
1325
  return;
730
1326
  }
1327
+ // If using AudioFrameBuffer, flush remaining buffered samples first
1328
+ if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1329
+ // Pull any remaining partial frame (may be less than frameSize)
1330
+ // For the final frame, we pad or truncate as needed
1331
+ let _bufferedFrame;
1332
+ while (!this.isClosed && (_bufferedFrame = this.audioFrameBuffer.pullSync()) !== null) {
1333
+ const env_8 = { stack: [], error: void 0, hasError: false };
1334
+ try {
1335
+ const bufferedFrame = __addDisposableResource(env_8, _bufferedFrame, false);
1336
+ this.codecContext.sendFrameSync(bufferedFrame);
1337
+ }
1338
+ catch (e_8) {
1339
+ env_8.error = e_8;
1340
+ env_8.hasError = true;
1341
+ }
1342
+ finally {
1343
+ __disposeResources(env_8);
1344
+ }
1345
+ }
1346
+ }
731
1347
  // Send flush frame (null)
732
1348
  const ret = this.codecContext.sendFrameSync(null);
733
1349
  if (ret < 0 && ret !== AVERROR_EOF) {
@@ -755,14 +1371,17 @@ export class Encoder {
755
1371
  * }
756
1372
  * ```
757
1373
  *
1374
+ * @see {@link encode} For sending frames and receiving packets
758
1375
  * @see {@link flush} For signaling end-of-stream
759
- * @see {@link packets} For complete pipeline
1376
+ * @see {@link flushPacketsSync} For synchronous version
760
1377
  */
761
1378
  async *flushPackets() {
762
1379
  // Send flush signal
763
1380
  await this.flush();
764
- let packet;
765
- while ((packet = await this.receive()) !== null) {
1381
+ while (true) {
1382
+ const packet = await this.receive();
1383
+ if (!packet)
1384
+ break;
766
1385
  yield packet;
767
1386
  }
768
1387
  }
@@ -786,13 +1405,17 @@ export class Encoder {
786
1405
  * }
787
1406
  * ```
788
1407
  *
1408
+ * @see {@link encodeSync} For sending frames and receiving packets
1409
+ * @see {@link flushSync} For signaling end-of-stream
789
1410
  * @see {@link flushPackets} For async version
790
1411
  */
791
1412
  *flushPacketsSync() {
792
1413
  // Send flush signal
793
1414
  this.flushSync();
794
- let packet;
795
- while ((packet = this.receiveSync()) !== null) {
1415
+ while (true) {
1416
+ const packet = this.receiveSync();
1417
+ if (!packet)
1418
+ break;
796
1419
  yield packet;
797
1420
  }
798
1421
  }
@@ -833,6 +1456,7 @@ export class Encoder {
833
1456
  *
834
1457
  * @see {@link encode} For sending frames and receiving packets
835
1458
  * @see {@link flush} For signaling end-of-stream
1459
+ * @see {@link receiveSync} For synchronous version
836
1460
  */
837
1461
  async receive() {
838
1462
  if (this.isClosed || !this.initialized) {
@@ -842,6 +1466,10 @@ export class Encoder {
842
1466
  this.packet.unref();
843
1467
  const ret = await this.codecContext.receivePacket(this.packet);
844
1468
  if (ret === 0) {
1469
+ // Set packet timebase to codec timebase
1470
+ this.packet.timeBase = this.codecContext.timeBase;
1471
+ // Mark packet as trusted (from encoder)
1472
+ this.packet.setFlags(AV_PKT_FLAG_TRUSTED);
845
1473
  // Got a packet, clone it for the user
846
1474
  return this.packet.clone();
847
1475
  }
@@ -891,6 +1519,8 @@ export class Encoder {
891
1519
  * }
892
1520
  * ```
893
1521
  *
1522
+ * @see {@link encodeSync} For sending frames and receiving packets
1523
+ * @see {@link flushSync} For signaling end-of-stream
894
1524
  * @see {@link receive} For async version
895
1525
  */
896
1526
  receiveSync() {
@@ -901,6 +1531,10 @@ export class Encoder {
901
1531
  this.packet.unref();
902
1532
  const ret = this.codecContext.receivePacketSync(this.packet);
903
1533
  if (ret === 0) {
1534
+ // Set packet timebase to codec timebase
1535
+ this.packet.timeBase = this.codecContext.timeBase;
1536
+ // Mark packet as trusted (from encoder)
1537
+ this.packet.setFlags(AV_PKT_FLAG_TRUSTED);
904
1538
  // Got a packet, clone it for the user
905
1539
  return this.packet.clone();
906
1540
  }
@@ -914,6 +1548,35 @@ export class Encoder {
914
1548
  return null;
915
1549
  }
916
1550
  }
1551
+ /**
1552
+ * Pipe encoded packets to muxer.
1553
+ *
1554
+ * @param target - Media output component to write packets to
1555
+ *
1556
+ * @param streamIndex - Stream index to write packets to
1557
+ *
1558
+ * @returns Scheduler for continued chaining
1559
+ *
1560
+ * @example
1561
+ * ```typescript
1562
+ * decoder.pipeTo(filter).pipeTo(encoder)
1563
+ * ```
1564
+ */
1565
+ pipeTo(target, streamIndex) {
1566
+ // Start worker if not already running
1567
+ this.workerPromise ??= this.runWorker();
1568
+ // Start pipe task: encoder.outputQueue -> output
1569
+ this.pipeToPromise = (async () => {
1570
+ while (true) {
1571
+ const packet = await this.receiveFromQueue();
1572
+ if (!packet)
1573
+ break;
1574
+ await target.writePacket(packet, streamIndex);
1575
+ }
1576
+ })();
1577
+ // Return control without pipeTo (terminal stage)
1578
+ return new SchedulerControl(this);
1579
+ }
917
1580
  /**
918
1581
  * Close encoder and free resources.
919
1582
  *
@@ -938,10 +1601,184 @@ export class Encoder {
938
1601
  return;
939
1602
  }
940
1603
  this.isClosed = true;
1604
+ // Close queues
1605
+ this.inputQueue.close();
1606
+ this.outputQueue.close();
941
1607
  this.packet.free();
942
1608
  this.codecContext.freeContext();
943
1609
  this.initialized = false;
944
1610
  }
1611
+ /**
1612
+ * Get encoder codec.
1613
+ *
1614
+ * Returns the codec used by this encoder.
1615
+ * Useful for checking codec capabilities and properties.
1616
+ *
1617
+ * @returns Codec instance
1618
+ *
1619
+ * @internal
1620
+ *
1621
+ * @see {@link Codec} For codec details
1622
+ */
1623
+ getCodec() {
1624
+ return this.codec;
1625
+ }
1626
+ /**
1627
+ * Get underlying codec context.
1628
+ *
1629
+ * Returns the codec context for advanced operations.
1630
+ * Useful for accessing low-level codec properties and settings.
1631
+ * Returns null if encoder is closed or not initialized.
1632
+ *
1633
+ * @returns Codec context or null if closed/not initialized
1634
+ *
1635
+ * @internal
1636
+ *
1637
+ * @see {@link CodecContext} For context details
1638
+ */
1639
+ getCodecContext() {
1640
+ return !this.isClosed && this.initialized ? this.codecContext : null;
1641
+ }
1642
+ /**
1643
+ * Worker loop for push-based processing.
1644
+ *
1645
+ * @internal
1646
+ */
1647
+ async runWorker() {
1648
+ try {
1649
+ // Outer loop - receive frames
1650
+ while (!this.inputQueue.isClosed) {
1651
+ const env_9 = { stack: [], error: void 0, hasError: false };
1652
+ try {
1653
+ const frame = __addDisposableResource(env_9, await this.inputQueue.receive(), false);
1654
+ if (!frame)
1655
+ break;
1656
+ // Open encoder if not already done
1657
+ if (!this.initialized) {
1658
+ this.initializePromise ??= this.initialize(frame);
1659
+ }
1660
+ await this.initializePromise;
1661
+ // Prepare frame for encoding (set quality, validate channel count)
1662
+ this.prepareFrameForEncoding(frame);
1663
+ // If audio encoder with fixed frame size, use AudioFrameBuffer
1664
+ if (this.audioFrameBuffer) {
1665
+ // Push frame into buffer
1666
+ await this.audioFrameBuffer.push(frame);
1667
+ // Pull and encode all available fixed-size frames
1668
+ let _bufferedFrame;
1669
+ while ((_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
1670
+ const env_10 = { stack: [], error: void 0, hasError: false };
1671
+ try {
1672
+ const bufferedFrame = __addDisposableResource(env_10, _bufferedFrame, false);
1673
+ // Send buffered frame to encoder
1674
+ const sendRet = await this.codecContext.sendFrame(bufferedFrame);
1675
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1676
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1677
+ }
1678
+ // Receive packets
1679
+ while (true) {
1680
+ const packet = await this.receive();
1681
+ if (!packet)
1682
+ break;
1683
+ await this.outputQueue.send(packet);
1684
+ }
1685
+ }
1686
+ catch (e_9) {
1687
+ env_10.error = e_9;
1688
+ env_10.hasError = true;
1689
+ }
1690
+ finally {
1691
+ __disposeResources(env_10);
1692
+ }
1693
+ }
1694
+ }
1695
+ else {
1696
+ // Send frame to encoder
1697
+ const sendRet = await this.codecContext.sendFrame(frame);
1698
+ if (sendRet < 0 && sendRet !== AVERROR_EOF && sendRet !== AVERROR_EAGAIN) {
1699
+ FFmpegError.throwIfError(sendRet, 'Failed to send frame');
1700
+ }
1701
+ // Receive ALL packets
1702
+ // A single frame can produce multiple packets (e.g., B-frames, lookahead)
1703
+ while (!this.outputQueue.isClosed) {
1704
+ const packet = await this.receive();
1705
+ if (!packet)
1706
+ break;
1707
+ await this.outputQueue.send(packet);
1708
+ }
1709
+ }
1710
+ }
1711
+ catch (e_10) {
1712
+ env_9.error = e_10;
1713
+ env_9.hasError = true;
1714
+ }
1715
+ finally {
1716
+ __disposeResources(env_9);
1717
+ }
1718
+ }
1719
+ // Flush encoder at end
1720
+ await this.flush();
1721
+ while (!this.outputQueue.isClosed) {
1722
+ const packet = await this.receive();
1723
+ if (!packet)
1724
+ break;
1725
+ await this.outputQueue.send(packet);
1726
+ }
1727
+ }
1728
+ catch {
1729
+ // Ignore error ?
1730
+ }
1731
+ finally {
1732
+ // Close output queue when done
1733
+ this.outputQueue?.close();
1734
+ }
1735
+ }
1736
+ /**
1737
+ * Send frame to input queue.
1738
+ *
1739
+ * @param frame - Frame to send
1740
+ *
1741
+ * @internal
1742
+ */
1743
+ async sendToQueue(frame) {
1744
+ await this.inputQueue.send(frame);
1745
+ }
1746
+ /**
1747
+ * Receive packet from output queue.
1748
+ *
1749
+ * @returns Packet from output queue
1750
+ *
1751
+ * @internal
1752
+ */
1753
+ async receiveFromQueue() {
1754
+ return await this.outputQueue.receive();
1755
+ }
1756
+ /**
1757
+ * Flush the entire filter pipeline.
1758
+ *
1759
+ * Propagates flush through worker, output queue, and next component.
1760
+ *
1761
+ * @internal
1762
+ */
1763
+ async flushPipeline() {
1764
+ // Close input queue to signal end of stream to worker
1765
+ this.inputQueue.close();
1766
+ // Wait for worker to finish processing all frames (if exists)
1767
+ if (this.workerPromise) {
1768
+ await this.workerPromise;
1769
+ }
1770
+ // Flush encoder at end
1771
+ await this.flush();
1772
+ while (true) {
1773
+ const packet = await this.receive();
1774
+ if (!packet)
1775
+ break;
1776
+ await this.outputQueue.send(packet);
1777
+ }
1778
+ if (this.pipeToPromise) {
1779
+ await this.pipeToPromise;
1780
+ }
1781
+ }
945
1782
  /**
946
1783
  * Initialize encoder from first frame.
947
1784
  *
@@ -956,25 +1793,80 @@ export class Encoder {
956
1793
  * @internal
957
1794
  */
958
1795
  async initialize(frame) {
1796
+ // Get bits_per_raw_sample from decoder if available
1797
+ if (this.options.decoder) {
1798
+ const decoderCtx = this.options.decoder.getCodecContext();
1799
+ if (decoderCtx && decoderCtx.bitsPerRawSample > 0) {
1800
+ this.codecContext.bitsPerRawSample = decoderCtx.bitsPerRawSample;
1801
+ }
1802
+ }
1803
+ // Get framerate from filter if available, otherwise from decoder
1804
+ // This matches FFmpeg CLI behavior where encoder gets frame_rate_filter from FrameData
1805
+ if (this.options.filter && frame.isVideo()) {
1806
+ const filterFrameRate = this.options.filter.frameRate;
1807
+ if (filterFrameRate) {
1808
+ this.codecContext.framerate = new Rational(filterFrameRate.num, filterFrameRate.den);
1809
+ }
1810
+ }
1811
+ // If no filter framerate, try to get from decoder stream
1812
+ if ((!this.codecContext.framerate || this.codecContext.framerate.num === 0) && this.options.decoder && frame.isVideo()) {
1813
+ const decoderCtx = this.options.decoder.getCodecContext();
1814
+ if (decoderCtx?.framerate && decoderCtx.framerate.num > 0) {
1815
+ this.codecContext.framerate = decoderCtx.framerate;
1816
+ }
1817
+ }
959
1818
  if (frame.isVideo()) {
1819
+ // FFmpeg CLI sets encoder time_base to 1/framerate (inverse of framerate)
1820
+ // This allows encoder to produce sequential PTS (0, 1, 2, 3...) which enables
1821
+ // proper B-frame DTS generation (negative DTS values)
1822
+ if (this.codecContext.framerate && this.codecContext.framerate.num > 0) {
1823
+ // Use inverse of framerate (e.g., framerate=30/1 → timebase=1/30)
1824
+ this.codecContext.timeBase = new Rational(this.codecContext.framerate.den, this.codecContext.framerate.num);
1825
+ }
1826
+ else {
1827
+ // Fallback: use frame timebase if framerate not available
1828
+ this.codecContext.timeBase = frame.timeBase;
1829
+ }
960
1830
  this.codecContext.width = frame.width;
961
1831
  this.codecContext.height = frame.height;
962
1832
  this.codecContext.pixelFormat = frame.format;
963
1833
  this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
1834
+ this.codecContext.colorRange = frame.colorRange;
1835
+ this.codecContext.colorPrimaries = frame.colorPrimaries;
1836
+ this.codecContext.colorTrc = frame.colorTrc;
1837
+ this.codecContext.colorSpace = frame.colorSpace;
1838
+ // Only set chroma location if unspecified
1839
+ if (this.codecContext.chromaLocation === AVCHROMA_LOC_UNSPECIFIED) {
1840
+ this.codecContext.chromaLocation = frame.chromaLocation;
1841
+ }
964
1842
  }
965
1843
  else {
1844
+ // Audio: Always use frame timebase (which is typically 1/sample_rate)
1845
+ // This ensures correct PTS progression for audio frames
1846
+ this.codecContext.timeBase = frame.timeBase;
966
1847
  this.codecContext.sampleRate = frame.sampleRate;
967
1848
  this.codecContext.sampleFormat = frame.format;
968
1849
  this.codecContext.channelLayout = frame.channelLayout;
969
1850
  }
970
- this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
971
- this.codecContext.hwFramesCtx = frame.hwFramesCtx;
1851
+ // Setup hardware acceleration with validation
1852
+ this.setupHardwareAcceleration(frame);
1853
+ // AV_CODEC_FLAG_COPY_OPAQUE: Copy opaque data from frames to packets if supported
1854
+ if (this.codec.hasCapabilities(AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE)) {
1855
+ this.codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
1856
+ }
1857
+ // AV_CODEC_FLAG_FRAME_DURATION: Signal that frame duration matters for timestamps
1858
+ this.codecContext.setFlags(AV_CODEC_FLAG_FRAME_DURATION);
972
1859
  // Open codec
973
1860
  const openRet = await this.codecContext.open2(this.codec, this.opts);
974
1861
  if (openRet < 0) {
975
1862
  this.codecContext.freeContext();
976
1863
  FFmpegError.throwIfError(openRet, 'Failed to open encoder');
977
1864
  }
1865
+ // Check if encoder requires fixed frame size (e.g., Opus, AAC, MP3)
1866
+ // If so, create AudioFrameBuffer to automatically chunk frames
1867
+ if (frame.isAudio() && this.codecContext.frameSize > 0) {
1868
+ this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels);
1869
+ }
978
1870
  this.initialized = true;
979
1871
  }
980
1872
  /**
@@ -994,95 +1886,225 @@ export class Encoder {
994
1886
  * @see {@link initialize} For async version
995
1887
  */
996
1888
  initializeSync(frame) {
1889
+ // Get bits_per_raw_sample from decoder if available
1890
+ if (this.options.decoder) {
1891
+ const decoderCtx = this.options.decoder.getCodecContext();
1892
+ if (decoderCtx && decoderCtx.bitsPerRawSample > 0) {
1893
+ this.codecContext.bitsPerRawSample = decoderCtx.bitsPerRawSample;
1894
+ }
1895
+ }
1896
+ // Get framerate from filter if available, otherwise from decoder
1897
+ // This matches FFmpeg CLI behavior where encoder gets frame_rate_filter from FrameData
1898
+ if (this.options.filter && frame.isVideo()) {
1899
+ const filterFrameRate = this.options.filter.frameRate;
1900
+ if (filterFrameRate) {
1901
+ this.codecContext.framerate = new Rational(filterFrameRate.num, filterFrameRate.den);
1902
+ }
1903
+ }
1904
+ // If no filter framerate, try to get from decoder stream
1905
+ if ((!this.codecContext.framerate || this.codecContext.framerate.num === 0) && this.options.decoder && frame.isVideo()) {
1906
+ const decoderCtx = this.options.decoder.getCodecContext();
1907
+ if (decoderCtx?.framerate && decoderCtx.framerate.num > 0) {
1908
+ this.codecContext.framerate = decoderCtx.framerate;
1909
+ }
1910
+ }
997
1911
  if (frame.isVideo()) {
1912
+ // FFmpeg CLI sets encoder time_base to 1/framerate (inverse of framerate)
1913
+ // This allows encoder to produce sequential PTS (0, 1, 2, 3...) which enables
1914
+ // proper B-frame DTS generation (negative DTS values)
1915
+ if (this.codecContext.framerate && this.codecContext.framerate.num > 0) {
1916
+ // Use inverse of framerate (e.g., framerate=30/1 → timebase=1/30)
1917
+ this.codecContext.timeBase = new Rational(this.codecContext.framerate.den, this.codecContext.framerate.num);
1918
+ }
1919
+ else {
1920
+ // Fallback: use frame timebase if framerate not available
1921
+ this.codecContext.timeBase = frame.timeBase;
1922
+ }
998
1923
  this.codecContext.width = frame.width;
999
1924
  this.codecContext.height = frame.height;
1000
1925
  this.codecContext.pixelFormat = frame.format;
1001
1926
  this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
1927
+ this.codecContext.colorRange = frame.colorRange;
1928
+ this.codecContext.colorPrimaries = frame.colorPrimaries;
1929
+ this.codecContext.colorTrc = frame.colorTrc;
1930
+ this.codecContext.colorSpace = frame.colorSpace;
1931
+ // Only set chroma location if unspecified
1932
+ if (this.codecContext.chromaLocation === AVCHROMA_LOC_UNSPECIFIED) {
1933
+ this.codecContext.chromaLocation = frame.chromaLocation;
1934
+ }
1002
1935
  }
1003
1936
  else {
1937
+ // Audio: Always use frame timebase (which is typically 1/sample_rate)
1938
+ // This ensures correct PTS progression for audio frames
1939
+ this.codecContext.timeBase = frame.timeBase;
1004
1940
  this.codecContext.sampleRate = frame.sampleRate;
1005
1941
  this.codecContext.sampleFormat = frame.format;
1006
1942
  this.codecContext.channelLayout = frame.channelLayout;
1007
1943
  }
1008
- this.codecContext.hwDeviceCtx = frame.hwFramesCtx?.deviceRef ?? null;
1009
- this.codecContext.hwFramesCtx = frame.hwFramesCtx;
1944
+ // Setup hardware acceleration with validation
1945
+ this.setupHardwareAcceleration(frame);
1946
+ // Set codec flags
1947
+ // AV_CODEC_FLAG_COPY_OPAQUE: Copy opaque data from frames to packets if supported
1948
+ if (this.codec.hasCapabilities(AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE)) {
1949
+ this.codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
1950
+ }
1951
+ // AV_CODEC_FLAG_FRAME_DURATION: Signal that frame duration matters for timestamps
1952
+ this.codecContext.setFlags(AV_CODEC_FLAG_FRAME_DURATION);
1010
1953
  // Open codec
1011
1954
  const openRet = this.codecContext.open2Sync(this.codec, this.opts);
1012
1955
  if (openRet < 0) {
1013
1956
  this.codecContext.freeContext();
1014
1957
  FFmpegError.throwIfError(openRet, 'Failed to open encoder');
1015
1958
  }
1959
+ // Check if encoder requires fixed frame size (e.g., Opus, AAC, MP3)
1960
+ // If so, create AudioFrameBuffer to automatically chunk frames
1961
+ if (frame.isAudio() && this.codecContext.frameSize > 0) {
1962
+ this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels);
1963
+ }
1016
1964
  this.initialized = true;
1017
1965
  }
1018
1966
  /**
1019
- * Get encoder codec.
1967
+ * Setup hardware acceleration for encoder.
1020
1968
  *
1021
- * Returns the codec used by this encoder.
1022
- * Useful for checking codec capabilities and properties.
1969
+ * Implements FFmpeg's hw_device_setup_for_encode logic.
1970
+ * Validates hardware frames context format and codec support.
1971
+ * Falls back to device context if frames context is incompatible.
1023
1972
  *
1024
- * @returns Codec instance
1973
+ * @param frame - Frame to get hardware context from
1025
1974
  *
1026
1975
  * @internal
1027
- *
1028
- * @see {@link Codec} For codec details
1029
1976
  */
1030
- getCodec() {
1031
- return this.codec;
1977
+ setupHardwareAcceleration(frame) {
1978
+ if (!frame.hwFramesCtx) {
1979
+ // Software encoding
1980
+ return;
1981
+ }
1982
+ const hwFramesCtx = frame.hwFramesCtx;
1983
+ const framesFormat = hwFramesCtx.format;
1984
+ const encoderFormat = this.codecContext.pixelFormat;
1985
+ // Check 1: Format validation
1986
+ if (framesFormat !== encoderFormat) {
1987
+ this.codecContext.hwDeviceCtx = hwFramesCtx.deviceRef;
1988
+ this.codecContext.hwFramesCtx = null;
1989
+ return;
1990
+ }
1991
+ // Check 2: Codec supports HW_FRAMES_CTX?
1992
+ let supportsFramesCtx = false;
1993
+ for (let i = 0;; i++) {
1994
+ const config = this.codec.getHwConfig(i);
1995
+ if (!config)
1996
+ break;
1997
+ // Check if codec supports HW_FRAMES_CTX method
1998
+ if (config.methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX) {
1999
+ // Check if pixel format matches or is unspecified
2000
+ if (config.pixFmt === AV_PIX_FMT_NONE || config.pixFmt === encoderFormat) {
2001
+ supportsFramesCtx = true;
2002
+ break;
2003
+ }
2004
+ }
2005
+ }
2006
+ if (supportsFramesCtx) {
2007
+ // Use hw_frames_ctx (best performance - zero copy)
2008
+ this.codecContext.hwFramesCtx = hwFramesCtx;
2009
+ this.codecContext.hwDeviceCtx = hwFramesCtx.deviceRef;
2010
+ }
2011
+ else {
2012
+ // Fallback to hw_device_ctx (still uses HW, but may copy)
2013
+ // Check if codec supports HW_DEVICE_CTX as fallback
2014
+ let supportsDeviceCtx = false;
2015
+ for (let i = 0;; i++) {
2016
+ const config = this.codec.getHwConfig(i);
2017
+ if (!config)
2018
+ break;
2019
+ if (config.methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) {
2020
+ supportsDeviceCtx = true;
2021
+ break;
2022
+ }
2023
+ }
2024
+ if (supportsDeviceCtx) {
2025
+ this.codecContext.hwDeviceCtx = hwFramesCtx.deviceRef;
2026
+ this.codecContext.hwFramesCtx = null;
2027
+ }
2028
+ else {
2029
+ // No hardware support at all - software encoding
2030
+ this.codecContext.hwDeviceCtx = null;
2031
+ this.codecContext.hwFramesCtx = null;
2032
+ }
2033
+ }
1032
2034
  }
1033
2035
  /**
1034
- * Get underlying codec context.
1035
- *
1036
- * Returns the codec context for advanced operations.
1037
- * Useful for accessing low-level codec properties and settings.
1038
- * Returns null if encoder is closed or not initialized.
1039
- *
1040
- * @returns Codec context or null if closed/not initialized
1041
- *
1042
- * @internal
2036
+ * Prepare frame for encoding.
1043
2037
  *
1044
- * @see {@link CodecContext} For context details
1045
- */
1046
- getCodecContext() {
1047
- return !this.isClosed && this.initialized ? this.codecContext : null;
1048
- }
1049
- /**
1050
- * Get codec flags even before encoder initialization.
2038
+ * Implements FFmpeg's frame_encode() pre-encoding logic:
2039
+ * 1. Video: Sets frame.quality from encoder's globalQuality (like -qscale)
2040
+ * 2. Audio: Validates channel count consistency for encoders without PARAM_CHANGE capability
1051
2041
  *
1052
- * Unlike getCodecContext(), this works before initialization.
2042
+ * This matches FFmpeg CLI behavior where these properties are automatically managed.
1053
2043
  *
1054
- * @returns Current codec flags
2044
+ * @param frame - Frame to prepare for encoding
1055
2045
  *
1056
- * @throws {Error} If encoder is closed
2046
+ * @throws {Error} If audio channel count changed and encoder doesn't support parameter changes
1057
2047
  *
1058
2048
  * @internal
1059
2049
  */
1060
- getCodecFlags() {
1061
- if (this.isClosed) {
1062
- throw new Error('Cannot get flags on closed encoder');
2050
+ prepareFrameForEncoding(frame) {
2051
+ // Adjust frame PTS and timebase to encoder timebase
2052
+ // This matches FFmpeg's adjust_frame_pts_to_encoder_tb() behavior which:
2053
+ // 1. Converts PTS from frame's timebase to encoder's timebase (av_rescale_q)
2054
+ // 2. Sets frame->time_base = tb_dst (so encoder gets correct timebase)
2055
+ // Note: prepareFrameForEncoding is always called AFTER initialize(),
2056
+ // so codecContext.timeBase is already set correctly:
2057
+ // - Video: 1/framerate (if available)
2058
+ // - Audio: frame.timeBase from first frame (typically 1/sample_rate)
2059
+ const encoderTimebase = this.codecContext.timeBase;
2060
+ const oldTimebase = frame.timeBase;
2061
+ // IMPORTANT: Calculate duration BEFORE converting frame timebase
2062
+ // This matches FFmpeg's video_sync_process() which calculates:
2063
+ // duration = frame->duration * av_q2d(frame->time_base) / av_q2d(ofp->tb_out)
2064
+ // We need the OLD timebase to convert duration properly
2065
+ let frameDuration;
2066
+ if (frame.duration && frame.duration > 0n) {
2067
+ // Convert duration from frame timebase to encoder timebase
2068
+ // This ensures encoder gets correct frame duration for timestamps
2069
+ frameDuration = avRescaleQ(frame.duration, oldTimebase, encoderTimebase);
1063
2070
  }
1064
- return this.codecContext.flags;
1065
- }
1066
- /**
1067
- * Set codec flags before encoder initialization.
1068
- *
1069
- * This allows setting flags on the codec context before the encoder is opened,
1070
- * which is necessary for flags that affect initialization behavior (like GLOBAL_HEADER).
1071
- *
1072
- * @param flags - The flags to set
1073
- *
1074
- * @throws {Error} If encoder is already initialized or closed
1075
- *
1076
- * @internal
1077
- */
1078
- setCodecFlags(flags) {
1079
- if (this.isClosed) {
1080
- throw new Error('Cannot set flags on closed encoder');
2071
+ else {
2072
+ // Default to 1 (constant frame rate behavior)
2073
+ // Matches FFmpeg's CFR mode: frame->duration = 1
2074
+ frameDuration = 1n;
1081
2075
  }
1082
- if (this.initialized) {
1083
- throw new Error('Cannot set flags on already initialized encoder');
2076
+ if (frame.pts !== null && frame.pts !== undefined) {
2077
+ // Convert PTS to encoder timebase
2078
+ frame.pts = avRescaleQ(frame.pts, oldTimebase, encoderTimebase);
2079
+ // IMPORTANT: Set frame timebase to encoder timebase
2080
+ // FFmpeg does this in adjust_frame_pts_to_encoder_tb(): frame->time_base = tb_dst
2081
+ // This ensures encoder gets frames with correct timebase (1/framerate for video, 1/sample_rate for audio)
2082
+ frame.timeBase = encoderTimebase;
2083
+ }
2084
+ // Set frame duration in encoder timebase
2085
+ // This matches FFmpeg's video_sync_process() which sets frame->duration
2086
+ // based on vsync_method (CFR: 1, VFR: calculated, PASSTHROUGH: calculated)
2087
+ // Since we don't have automatic filter like FFmpeg, we always set it here
2088
+ frame.duration = frameDuration;
2089
+ if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
2090
+ // Video: Set frame quality from encoder's global quality
2091
+ // Only set if encoder has globalQuality configured and frame doesn't already have quality set
2092
+ if (this.codecContext.globalQuality > 0 && frame.quality <= 0) {
2093
+ frame.quality = this.codecContext.globalQuality;
2094
+ }
2095
+ }
2096
+ else if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
2097
+ // Audio: Validate channel count consistency
2098
+ // If encoder doesn't support AV_CODEC_CAP_PARAM_CHANGE, channel count must remain constant
2099
+ const supportsParamChange = this.codec.hasCapabilities(AV_CODEC_CAP_PARAM_CHANGE);
2100
+ if (!supportsParamChange) {
2101
+ const encoderChannels = this.codecContext.channelLayout.nbChannels;
2102
+ const frameChannels = frame.channelLayout?.nbChannels ?? 0;
2103
+ if (encoderChannels !== frameChannels) {
2104
+ throw new Error(`Audio channel count changed (${encoderChannels} -> ${frameChannels}) and encoder '${this.codec.name}' does not support parameter changes`);
2105
+ }
2106
+ }
1084
2107
  }
1085
- this.codecContext.flags = flags;
1086
2108
  }
1087
2109
  /**
1088
2110
  * Dispose of encoder.