node-av 6.2.0-beta.9 → 7.0.0-beta.1

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 (116) hide show
  1. package/README.md +7 -19
  2. package/dist/api/audio-frame-buffer.d.ts +378 -19
  3. package/dist/api/audio-frame-buffer.js +715 -101
  4. package/dist/api/audio-frame-buffer.js.map +1 -1
  5. package/dist/api/data/test_av1.ivf +0 -0
  6. package/dist/api/data/test_hevc.h265 +0 -0
  7. package/dist/api/data/test_vp9.ivf +0 -0
  8. package/dist/api/decoder.d.ts +117 -2
  9. package/dist/api/decoder.js +290 -23
  10. package/dist/api/decoder.js.map +1 -1
  11. package/dist/api/demuxer.d.ts +19 -3
  12. package/dist/api/demuxer.js +95 -29
  13. package/dist/api/demuxer.js.map +1 -1
  14. package/dist/api/encoder-pool.js +9 -1
  15. package/dist/api/encoder-pool.js.map +1 -1
  16. package/dist/api/encoder.d.ts +100 -6
  17. package/dist/api/encoder.js +249 -108
  18. package/dist/api/encoder.js.map +1 -1
  19. package/dist/api/filter-presets.d.ts +25 -1
  20. package/dist/api/filter-presets.js +67 -5
  21. package/dist/api/filter-presets.js.map +1 -1
  22. package/dist/api/filter.d.ts +62 -3
  23. package/dist/api/filter.js +118 -36
  24. package/dist/api/filter.js.map +1 -1
  25. package/dist/api/fmp4-stream.d.ts +179 -15
  26. package/dist/api/fmp4-stream.js +341 -120
  27. package/dist/api/fmp4-stream.js.map +1 -1
  28. package/dist/api/hardware.d.ts +2 -0
  29. package/dist/api/hardware.js +33 -10
  30. package/dist/api/hardware.js.map +1 -1
  31. package/dist/api/index.d.ts +2 -2
  32. package/dist/api/index.js.map +1 -1
  33. package/dist/api/io-stream.d.ts +3 -2
  34. package/dist/api/io-stream.js +23 -7
  35. package/dist/api/io-stream.js.map +1 -1
  36. package/dist/api/muxer.d.ts +276 -7
  37. package/dist/api/muxer.js +649 -116
  38. package/dist/api/muxer.js.map +1 -1
  39. package/dist/api/pipeline.d.ts +22 -0
  40. package/dist/api/pipeline.js +53 -0
  41. package/dist/api/pipeline.js.map +1 -1
  42. package/dist/api/rtp-stream.d.ts +54 -33
  43. package/dist/api/rtp-stream.js +232 -153
  44. package/dist/api/rtp-stream.js.map +1 -1
  45. package/dist/api/scaler.d.ts +39 -6
  46. package/dist/api/scaler.js +72 -18
  47. package/dist/api/scaler.js.map +1 -1
  48. package/dist/api/utilities/live-decoding.d.ts +15 -0
  49. package/dist/api/utilities/live-decoding.js +32 -0
  50. package/dist/api/utilities/live-decoding.js.map +1 -0
  51. package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
  52. package/dist/api/utilities/packet-age-watchdog.js +594 -0
  53. package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
  54. package/dist/api/utilities/scale-size.d.ts +51 -0
  55. package/dist/api/utilities/scale-size.js +55 -0
  56. package/dist/api/utilities/scale-size.js.map +1 -0
  57. package/dist/api/utilities/video-transcode.d.ts +218 -0
  58. package/dist/api/utilities/video-transcode.js +624 -0
  59. package/dist/api/utilities/video-transcode.js.map +1 -0
  60. package/dist/constants/bsf-options.d.ts +32 -8
  61. package/dist/constants/constants.d.ts +36 -114
  62. package/dist/constants/constants.js +32 -95
  63. package/dist/constants/constants.js.map +1 -1
  64. package/dist/constants/decoders.d.ts +74 -74
  65. package/dist/constants/decoders.js +79 -77
  66. package/dist/constants/decoders.js.map +1 -1
  67. package/dist/constants/encoders.d.ts +20 -21
  68. package/dist/constants/encoders.js +25 -24
  69. package/dist/constants/encoders.js.map +1 -1
  70. package/dist/constants/filter-options.d.ts +1378 -194
  71. package/dist/constants/format-options.d.ts +1342 -83
  72. package/dist/constants/formats.d.ts +2 -2
  73. package/dist/constants/options.d.ts +2261 -125
  74. package/dist/ffmpeg/utils.js +7 -4
  75. package/dist/ffmpeg/utils.js.map +1 -1
  76. package/dist/ffmpeg/version.d.ts +1 -1
  77. package/dist/ffmpeg/version.js +1 -1
  78. package/dist/ffmpeg/version.js.map +1 -1
  79. package/dist/lib/binding.d.ts +10 -1
  80. package/dist/lib/binding.js +25 -16
  81. package/dist/lib/binding.js.map +1 -1
  82. package/dist/lib/bitstream-filter-context.d.ts +2 -0
  83. package/dist/lib/bitstream-filter-context.js +2 -0
  84. package/dist/lib/bitstream-filter-context.js.map +1 -1
  85. package/dist/lib/filter-context.d.ts +3 -0
  86. package/dist/lib/filter-context.js +3 -0
  87. package/dist/lib/filter-context.js.map +1 -1
  88. package/dist/lib/filter-graph-segment.d.ts +3 -2
  89. package/dist/lib/filter-graph-segment.js +3 -2
  90. package/dist/lib/filter-graph-segment.js.map +1 -1
  91. package/dist/lib/filter-graph.d.ts +4 -4
  92. package/dist/lib/filter-graph.js +4 -4
  93. package/dist/lib/filter-inout.d.ts +4 -0
  94. package/dist/lib/filter-inout.js +4 -0
  95. package/dist/lib/filter-inout.js.map +1 -1
  96. package/dist/lib/format-context.d.ts +27 -3
  97. package/dist/lib/format-context.js +30 -6
  98. package/dist/lib/format-context.js.map +1 -1
  99. package/dist/lib/frame.d.ts +6 -2
  100. package/dist/lib/frame.js +11 -3
  101. package/dist/lib/frame.js.map +1 -1
  102. package/dist/lib/io-context.d.ts +4 -2
  103. package/dist/lib/io-context.js +4 -2
  104. package/dist/lib/io-context.js.map +1 -1
  105. package/dist/lib/native-types.d.ts +7 -3
  106. package/dist/lib/output-format.d.ts +42 -0
  107. package/dist/lib/output-format.js +46 -0
  108. package/dist/lib/output-format.js.map +1 -1
  109. package/dist/lib/stream.d.ts +5 -0
  110. package/dist/lib/stream.js +5 -0
  111. package/dist/lib/stream.js.map +1 -1
  112. package/dist/lib/utilities.d.ts +26 -0
  113. package/dist/lib/utilities.js +28 -0
  114. package/dist/lib/utilities.js.map +1 -1
  115. package/install/check.js +14 -16
  116. package/package.json +28 -44
@@ -51,18 +51,19 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  /* eslint-disable @stylistic/indent-binary-ops */
54
- import { AV_CHANNEL_ORDER_UNSPEC, 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_NOPTS_VALUE, AV_PICTURE_TYPE_NONE, AV_PIX_FMT_NONE, AV_PKT_FLAG_TRUSTED, AVCHROMA_LOC_UNSPECIFIED, AVERROR_EAGAIN, AVERROR_ENCODER_NOT_FOUND, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, EOF, SWS_BILINEAR, } from '../constants/constants.js';
54
+ import { AV_CHANNEL_ORDER_UNSPEC, 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_LOG_WARNING, AV_NOPTS_VALUE, AV_PICTURE_TYPE_I, AV_PICTURE_TYPE_NONE, AV_PIX_FMT_NONE, AV_PKT_FLAG_TRUSTED, AV_ROUND_NEAR_INF, AVCHROMA_LOC_UNSPECIFIED, AVERROR_EAGAIN, AVERROR_ENCODER_NOT_FOUND, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, EOF, SWS_BILINEAR, } from '../constants/constants.js';
55
55
  import { CodecContext } from '../lib/codec-context.js';
56
56
  import { Codec } from '../lib/codec.js';
57
57
  import { Dictionary } from '../lib/dictionary.js';
58
58
  import { FFmpegError } from '../lib/error.js';
59
59
  import { Frame } from '../lib/frame.js';
60
+ import { Log } from '../lib/log.js';
60
61
  import { Packet } from '../lib/packet.js';
61
62
  import { Rational } from '../lib/rational.js';
62
63
  import { SoftwareResampleContext } from '../lib/software-resample-context.js';
63
64
  import { SoftwareScaleContext } from '../lib/software-scale-context.js';
64
- import { avChannelLayoutDefault, avGetPixFmtName, avGetSampleFmtName, avRescaleQ } from '../lib/utilities.js';
65
- import { AudioFrameBuffer } from './audio-frame-buffer.js';
65
+ import { avChannelLayoutDefault, avGetPixFmtName, avGetSampleFmtName, avRescaleQ, avRescaleRnd } from '../lib/utilities.js';
66
+ import { assertAudioFrameBufferOptions, AudioFrameBuffer } from './audio-frame-buffer.js';
66
67
  import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
67
68
  import { AsyncQueue } from './utilities/async-queue.js';
68
69
  import { pickSupportedLayout, pickSupportedPixelFormat, pickSupportedRate, pickSupportedSampleFormat } from './utilities/codec-format.js';
@@ -87,6 +88,30 @@ const BITRATE_FIELDS = ['bitRate', 'rcMinRate', 'rcMaxRate', 'rcBufferSize'];
87
88
  function normalizeBitrate(value) {
88
89
  return typeof value === 'string' ? parseBitrate(value) : BigInt(value);
89
90
  }
91
+ /**
92
+ * Highest rate taken as a frame rate, above it a rate is a stream clock.
93
+ *
94
+ * @internal
95
+ */
96
+ const MAX_FRAME_RATE = 240;
97
+ /**
98
+ * Frame rate declared when nothing knows the rate, FFmpeg CLI's default.
99
+ *
100
+ * @internal
101
+ */
102
+ const FALLBACK_FRAME_RATE = 25;
103
+ /**
104
+ * Whether a rate is a plausible frame rate rather than a stream clock.
105
+ *
106
+ * @param rate - The rate
107
+ *
108
+ * @returns True for a positive rate up to {@link MAX_FRAME_RATE}
109
+ *
110
+ * @internal
111
+ */
112
+ function isFrameRate(rate) {
113
+ return rate.num > 0 && rate.den > 0 && rate.num / rate.den <= MAX_FRAME_RATE;
114
+ }
90
115
  /**
91
116
  * High-level encoder for audio and video streams.
92
117
  *
@@ -159,7 +184,7 @@ export class Encoder {
159
184
  audioResampler;
160
185
  resampledFrame;
161
186
  resampleChangesRate = false;
162
- resampledPts = 0n;
187
+ resampleInputRate = 0;
163
188
  audioInputLayout;
164
189
  autoFormat;
165
190
  videoScaler;
@@ -167,6 +192,9 @@ export class Encoder {
167
192
  videoTargetFormat;
168
193
  supportsParamChange;
169
194
  encoderChannels;
195
+ nextVideoPts;
196
+ droppedKeyframe = false;
197
+ keyframeRequested = false;
170
198
  // Worker pattern for push-based processing
171
199
  inputQueue;
172
200
  outputQueue;
@@ -213,6 +241,8 @@ export class Encoder {
213
241
  *
214
242
  * @throws {Error} If encoder not found
215
243
  *
244
+ * @throws {RangeError} If an audioFrameBuffer option is out of range
245
+ *
216
246
  * @example
217
247
  * ```typescript
218
248
  * // From decoder stream info
@@ -251,6 +281,8 @@ export class Encoder {
251
281
  * @see {@link createSync} For synchronous version
252
282
  */
253
283
  static async create(encoderCodec, options = {}) {
284
+ // Copied: defaults (e.g. bitrate) are filled in below
285
+ options = { ...options };
254
286
  let codec = null;
255
287
  if (encoderCodec instanceof Codec) {
256
288
  codec = encoderCodec;
@@ -264,6 +296,9 @@ export class Encoder {
264
296
  if (!codec) {
265
297
  throw new FFmpegError(AVERROR_ENCODER_NOT_FOUND);
266
298
  }
299
+ if (options.audioFrameBuffer) {
300
+ assertAudioFrameBufferOptions(options.audioFrameBuffer);
301
+ }
267
302
  // Allocate codec context
268
303
  const codecContext = new CodecContext();
269
304
  codecContext.allocContext3(codec);
@@ -274,14 +309,15 @@ export class Encoder {
274
309
  if (options.maxBFrames !== undefined) {
275
310
  codecContext.maxBFrames = options.maxBFrames;
276
311
  }
277
- // Apply common options with codec-type-specific defaults
278
- if (options.bitrate === undefined) {
279
- // Set codec-type-specific default bitrate
280
- const isAudio = codec.type === AVMEDIA_TYPE_AUDIO;
281
- options.bitrate = isAudio ? 128_000 : 1_000_000;
312
+ // Audio bitrate is left to the codec's default like FFmpeg CLI does: it
313
+ // depends on codec, channels and rate (AAC 69k per mono channel, G.726 32k),
314
+ // and some codecs only open at specific rates (G.723.1: 6.3k/5.3k)
315
+ if (options.bitrate === undefined && codec.type !== AVMEDIA_TYPE_AUDIO) {
316
+ options.bitrate = 1_000_000;
317
+ }
318
+ if (options.bitrate !== undefined) {
319
+ codecContext.bitRate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
282
320
  }
283
- const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
284
- codecContext.bitRate = bitrate;
285
321
  if (options.minRate !== undefined) {
286
322
  const minRate = typeof options.minRate === 'string' ? parseBitrate(options.minRate) : BigInt(options.minRate);
287
323
  codecContext.rcMinRate = minRate;
@@ -330,6 +366,8 @@ export class Encoder {
330
366
  *
331
367
  * @throws {FFmpegError} If codec allocation fails
332
368
  *
369
+ * @throws {RangeError} If an audioFrameBuffer option is out of range
370
+ *
333
371
  * @example
334
372
  * ```typescript
335
373
  * // From decoder stream info
@@ -368,6 +406,8 @@ export class Encoder {
368
406
  * @see {@link create} For async version
369
407
  */
370
408
  static createSync(encoderCodec, options = {}) {
409
+ // Copied: defaults (e.g. bitrate) are filled in below
410
+ options = { ...options };
371
411
  let codec = null;
372
412
  if (encoderCodec instanceof Codec) {
373
413
  codec = encoderCodec;
@@ -381,17 +421,18 @@ export class Encoder {
381
421
  if (!codec) {
382
422
  throw new FFmpegError(AVERROR_ENCODER_NOT_FOUND);
383
423
  }
424
+ if (options.audioFrameBuffer) {
425
+ assertAudioFrameBufferOptions(options.audioFrameBuffer);
426
+ }
384
427
  // Allocate codec context
385
428
  const codecContext = new CodecContext();
386
429
  codecContext.allocContext3(codec);
387
- // Apply common options with codec-type-specific defaults
388
- if (options.bitrate === undefined) {
389
- // Set codec-type-specific default bitrate
390
- const isAudio = codec.type === AVMEDIA_TYPE_AUDIO;
391
- options.bitrate = isAudio ? 128_000 : 1_000_000;
392
- }
393
- const bitrate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
394
- codecContext.bitRate = bitrate;
430
+ if (options.bitrate === undefined && codec.type !== AVMEDIA_TYPE_AUDIO) {
431
+ options.bitrate = 1_000_000;
432
+ }
433
+ if (options.bitrate !== undefined) {
434
+ codecContext.bitRate = typeof options.bitrate === 'string' ? parseBitrate(options.bitrate) : BigInt(options.bitrate);
435
+ }
395
436
  if (options.gopSize !== undefined) {
396
437
  codecContext.gopSize = options.gopSize;
397
438
  }
@@ -706,6 +747,24 @@ export class Encoder {
706
747
  isReady() {
707
748
  return this.initialized && !this.isClosed;
708
749
  }
750
+ /**
751
+ * Encode the next frame as a keyframe.
752
+ *
753
+ * Answers receiver feedback such as an RTCP PLI, so a receiver that lost
754
+ * packets resyncs on the next frame instead of waiting for the next GOP.
755
+ * Set the encoder's `forced-idr` option where it has one, otherwise some
756
+ * encoders (libx264, NVENC, QSV, AMF) emit an I frame that is not an IDR.
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * sender.onPictureLossIndication.subscribe(() => {
761
+ * encoder.requestKeyframe();
762
+ * });
763
+ * ```
764
+ */
765
+ requestKeyframe() {
766
+ this.keyframeRequested = true;
767
+ }
709
768
  /**
710
769
  * Send a frame to the encoder.
711
770
  *
@@ -717,6 +776,10 @@ export class Encoder {
717
776
  * **Important**: This method only SENDS the frame to the encoder.
718
777
  * You must call {@link receive} separately (potentially multiple times) to get encoded packets.
719
778
  *
779
+ * Like FFmpeg CLI, a video frame whose timestamp lands on the previous frame's
780
+ * in the encoder time base moves to the next tick, and one that lies entirely
781
+ * before it is dropped, since encoders need strictly increasing timestamps.
782
+ *
720
783
  * Direct mapping to avcodec_send_frame().
721
784
  *
722
785
  * @param frame - Raw frame to send to encoder, or null to flush
@@ -780,8 +843,11 @@ export class Encoder {
780
843
  }
781
844
  // Convert to the codec's format first (audio resample / video pixfmt).
782
845
  const input = this.audioResampler ? this.resampleAudio(frame) : this.videoScaler ? this.scaleVideo(frame) : frame;
783
- // Prepare frame for encoding (set quality, validate channel count)
784
- this.prepareFrameForEncoding(input);
846
+ // Prepare frame for encoding (timestamps, quality, channel count); a video
847
+ // frame that falls behind the previous one is dropped
848
+ if (!this.prepareFrameForEncoding(input)) {
849
+ return;
850
+ }
785
851
  const encode = async (newFrame) => {
786
852
  const sendRet = await this.codecContext.sendFrame(newFrame);
787
853
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -809,6 +875,10 @@ export class Encoder {
809
875
  * **Important**: This method only SENDS the frame to the encoder.
810
876
  * You must call {@link receiveSync} separately (potentially multiple times) to get encoded packets.
811
877
  *
878
+ * Like FFmpeg CLI, a video frame whose timestamp lands on the previous frame's
879
+ * in the encoder time base moves to the next tick, and one that lies entirely
880
+ * before it is dropped, since encoders need strictly increasing timestamps.
881
+ *
812
882
  * Direct mapping to avcodec_send_frame().
813
883
  *
814
884
  * @param frame - Raw frame to send to encoder, or null to flush
@@ -855,8 +925,9 @@ export class Encoder {
855
925
  }
856
926
  // Convert to the codec's format first (audio resample / video pixfmt).
857
927
  const input = this.audioResampler ? this.resampleAudio(frame) : this.videoScaler ? this.scaleVideo(frame) : frame;
858
- // Prepare frame for encoding (set quality, validate channel count)
859
- this.prepareFrameForEncoding(input);
928
+ if (!this.prepareFrameForEncoding(input)) {
929
+ return;
930
+ }
860
931
  const encode = (newFrame) => {
861
932
  const sendRet = this.codecContext.sendFrameSync(newFrame);
862
933
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -1503,12 +1574,18 @@ export class Encoder {
1503
1574
  }
1504
1575
  // Clear previous packet data
1505
1576
  this.packet.unref();
1506
- if (this.audioFrameBuffer?.hasFrame()) {
1577
+ // Skip frames the codec rejects (e.g. aac "Input contains (near) NaN/+-Inf"). Pulling a single
1578
+ // frame per call would leave the rest in the FIFO while every encode() adds more.
1579
+ while (this.audioFrameBuffer?.hasFrame()) {
1507
1580
  const env_9 = { stack: [], error: void 0, hasError: false };
1508
1581
  try {
1509
1582
  const bufferedFrame = __addDisposableResource(env_9, await this.audioFrameBuffer.pull(), false);
1510
- if (bufferedFrame) {
1511
- await this.codecContext.sendFrame(bufferedFrame);
1583
+ if (!bufferedFrame) {
1584
+ break;
1585
+ }
1586
+ const sendRet = await this.codecContext.sendFrame(bufferedFrame);
1587
+ if (sendRet >= 0 || sendRet === AVERROR_EAGAIN) {
1588
+ break;
1512
1589
  }
1513
1590
  }
1514
1591
  catch (e_9) {
@@ -1608,12 +1685,17 @@ export class Encoder {
1608
1685
  }
1609
1686
  // Clear previous packet data
1610
1687
  this.packet.unref();
1611
- if (this.audioFrameBuffer?.hasFrame()) {
1688
+ // Skip frames the codec rejects, see receive()
1689
+ while (this.audioFrameBuffer?.hasFrame()) {
1612
1690
  const env_10 = { stack: [], error: void 0, hasError: false };
1613
1691
  try {
1614
1692
  const bufferedFrame = __addDisposableResource(env_10, this.audioFrameBuffer.pullSync(), false);
1615
- if (bufferedFrame) {
1616
- this.codecContext.sendFrameSync(bufferedFrame);
1693
+ if (!bufferedFrame) {
1694
+ break;
1695
+ }
1696
+ const sendRet = this.codecContext.sendFrameSync(bufferedFrame);
1697
+ if (sendRet >= 0 || sendRet === AVERROR_EAGAIN) {
1698
+ break;
1617
1699
  }
1618
1700
  }
1619
1701
  catch (e_10) {
@@ -1714,7 +1796,6 @@ export class Encoder {
1714
1796
  this.outputQueue.close();
1715
1797
  this.inputQueue.clear();
1716
1798
  this.outputQueue.clear();
1717
- this.packet.free();
1718
1799
  this.audioFrameBuffer?.[Symbol.dispose]();
1719
1800
  this.audioFrameBuffer = undefined;
1720
1801
  this.audioResampler?.[Symbol.dispose]();
@@ -1726,6 +1807,7 @@ export class Encoder {
1726
1807
  this.scaledFrame?.free();
1727
1808
  this.scaledFrame = undefined;
1728
1809
  this.codecContext.freeContext();
1810
+ this.packet.free();
1729
1811
  this.initialized = false;
1730
1812
  }
1731
1813
  /**
@@ -1778,8 +1860,6 @@ export class Encoder {
1778
1860
  this.initializePromise ??= this.initialize(frame);
1779
1861
  }
1780
1862
  await this.initializePromise;
1781
- // Prepare frame for encoding (set quality, validate channel count)
1782
- this.prepareFrameForEncoding(frame);
1783
1863
  await this.encode(frame);
1784
1864
  // Receive packets
1785
1865
  while (!this.outputQueue.isClosed) {
@@ -1891,6 +1971,58 @@ export class Encoder {
1891
1971
  }
1892
1972
  applyContextOptions(this.codecContext, resolved);
1893
1973
  }
1974
+ /**
1975
+ * Set the frame rate and time base of a video encoder from its first frame.
1976
+ *
1977
+ * Shared by both initialize variants.
1978
+ *
1979
+ * @param frame - First frame to encode
1980
+ *
1981
+ * @internal
1982
+ */
1983
+ setupVideoTiming(frame) {
1984
+ // Get framerate from filter if available, otherwise from decoder
1985
+ // This matches FFmpeg CLI behavior where encoder gets frame_rate_filter from FrameData
1986
+ const filterFrameRate = this.options.filter?.frameRate;
1987
+ if (filterFrameRate) {
1988
+ this.codecContext.framerate = new Rational(filterFrameRate.num, filterFrameRate.den);
1989
+ }
1990
+ // If no filter framerate, try to get from decoder stream
1991
+ if ((!this.codecContext.framerate || this.codecContext.framerate.num === 0) && this.options.decoder) {
1992
+ const decoderCtx = this.options.decoder.getCodecContext();
1993
+ if (decoderCtx?.framerate && decoderCtx.framerate.num > 0) {
1994
+ this.codecContext.framerate = decoderCtx.framerate;
1995
+ }
1996
+ else {
1997
+ // The bitstream carries no rate (e.g. MJPEG, or an H.264 camera without
1998
+ // VUI timing): guess it from the stream like FFmpeg CLI does. A stream
1999
+ // probed too briefly to see two frames (RTPStream reads with
2000
+ // analyzeduration 0) offers only its clock as rate (e.g. 90000/1).
2001
+ const guessed = this.options.decoder.getGuessedFrameRate();
2002
+ if (isFrameRate(guessed)) {
2003
+ this.codecContext.framerate = new Rational(guessed.num, guessed.den);
2004
+ }
2005
+ }
2006
+ }
2007
+ // FFmpeg CLI sets encoder time_base to 1/framerate (inverse of framerate)
2008
+ // This allows encoder to produce sequential PTS (0, 1, 2, 3...) which enables
2009
+ // proper B-frame DTS generation (negative DTS values)
2010
+ if (this.codecContext.framerate && this.codecContext.framerate.num > 0) {
2011
+ // Use inverse of framerate (e.g., framerate=30/1 → timebase=1/30)
2012
+ this.codecContext.timeBase = new Rational(this.codecContext.framerate.den, this.codecContext.framerate.num);
2013
+ return;
2014
+ }
2015
+ // Fallback: use frame timebase if framerate not available
2016
+ this.codecContext.timeBase = frame.timeBase;
2017
+ // Without a framerate encoders read the inverse time base as one: for a
2018
+ // 90 kHz clock x264 picks level 6.2 and VAAPI and QSV fail to encode.
2019
+ // Declare FFmpeg CLI's default rate and keep the clock, so the timestamps
2020
+ // stay apart whatever the real rate is.
2021
+ if (!isFrameRate({ num: frame.timeBase.den, den: frame.timeBase.num })) {
2022
+ Log.log(AV_LOG_WARNING, `No information about the input frame rate is available, encoding as ${FALLBACK_FRAME_RATE} fps\n`);
2023
+ this.codecContext.framerate = new Rational(FALLBACK_FRAME_RATE, 1);
2024
+ }
2025
+ }
1894
2026
  /**
1895
2027
  * Initialize encoder from first frame.
1896
2028
  *
@@ -1912,33 +2044,8 @@ export class Encoder {
1912
2044
  this.codecContext.bitsPerRawSample = decoderCtx.bitsPerRawSample;
1913
2045
  }
1914
2046
  }
1915
- // Get framerate from filter if available, otherwise from decoder
1916
- // This matches FFmpeg CLI behavior where encoder gets frame_rate_filter from FrameData
1917
- if (this.options.filter && frame.isVideo()) {
1918
- const filterFrameRate = this.options.filter.frameRate;
1919
- if (filterFrameRate) {
1920
- this.codecContext.framerate = new Rational(filterFrameRate.num, filterFrameRate.den);
1921
- }
1922
- }
1923
- // If no filter framerate, try to get from decoder stream
1924
- if ((!this.codecContext.framerate || this.codecContext.framerate.num === 0) && this.options.decoder && frame.isVideo()) {
1925
- const decoderCtx = this.options.decoder.getCodecContext();
1926
- if (decoderCtx?.framerate && decoderCtx.framerate.num > 0) {
1927
- this.codecContext.framerate = decoderCtx.framerate;
1928
- }
1929
- }
1930
2047
  if (frame.isVideo()) {
1931
- // FFmpeg CLI sets encoder time_base to 1/framerate (inverse of framerate)
1932
- // This allows encoder to produce sequential PTS (0, 1, 2, 3...) which enables
1933
- // proper B-frame DTS generation (negative DTS values)
1934
- if (this.codecContext.framerate && this.codecContext.framerate.num > 0) {
1935
- // Use inverse of framerate (e.g., framerate=30/1 → timebase=1/30)
1936
- this.codecContext.timeBase = new Rational(this.codecContext.framerate.den, this.codecContext.framerate.num);
1937
- }
1938
- else {
1939
- // Fallback: use frame timebase if framerate not available
1940
- this.codecContext.timeBase = frame.timeBase;
1941
- }
2048
+ this.setupVideoTiming(frame);
1942
2049
  this.codecContext.width = frame.width;
1943
2050
  this.codecContext.height = frame.height;
1944
2051
  // Pick a codec-supported pixel format (converting on demand when autoFormat).
@@ -1976,7 +2083,7 @@ export class Encoder {
1976
2083
  // Check if encoder requires fixed frame size (e.g., Opus, AAC, MP3)
1977
2084
  // If so, create AudioFrameBuffer to automatically chunk frames
1978
2085
  if (frame.isAudio() && this.codecContext.frameSize > 0) {
1979
- this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels);
2086
+ this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels, this.options.audioFrameBuffer);
1980
2087
  }
1981
2088
  this.initialized = true;
1982
2089
  }
@@ -2004,33 +2111,8 @@ export class Encoder {
2004
2111
  this.codecContext.bitsPerRawSample = decoderCtx.bitsPerRawSample;
2005
2112
  }
2006
2113
  }
2007
- // Get framerate from filter if available, otherwise from decoder
2008
- // This matches FFmpeg CLI behavior where encoder gets frame_rate_filter from FrameData
2009
- if (this.options.filter && frame.isVideo()) {
2010
- const filterFrameRate = this.options.filter.frameRate;
2011
- if (filterFrameRate) {
2012
- this.codecContext.framerate = new Rational(filterFrameRate.num, filterFrameRate.den);
2013
- }
2014
- }
2015
- // If no filter framerate, try to get from decoder stream
2016
- if ((!this.codecContext.framerate || this.codecContext.framerate.num === 0) && this.options.decoder && frame.isVideo()) {
2017
- const decoderCtx = this.options.decoder.getCodecContext();
2018
- if (decoderCtx?.framerate && decoderCtx.framerate.num > 0) {
2019
- this.codecContext.framerate = decoderCtx.framerate;
2020
- }
2021
- }
2022
2114
  if (frame.isVideo()) {
2023
- // FFmpeg CLI sets encoder time_base to 1/framerate (inverse of framerate)
2024
- // This allows encoder to produce sequential PTS (0, 1, 2, 3...) which enables
2025
- // proper B-frame DTS generation (negative DTS values)
2026
- if (this.codecContext.framerate && this.codecContext.framerate.num > 0) {
2027
- // Use inverse of framerate (e.g., framerate=30/1 → timebase=1/30)
2028
- this.codecContext.timeBase = new Rational(this.codecContext.framerate.den, this.codecContext.framerate.num);
2029
- }
2030
- else {
2031
- // Fallback: use frame timebase if framerate not available
2032
- this.codecContext.timeBase = frame.timeBase;
2033
- }
2115
+ this.setupVideoTiming(frame);
2034
2116
  this.codecContext.width = frame.width;
2035
2117
  this.codecContext.height = frame.height;
2036
2118
  // Pick a codec-supported pixel format (converting on demand when autoFormat).
@@ -2069,7 +2151,7 @@ export class Encoder {
2069
2151
  // Check if encoder requires fixed frame size (e.g., Opus, AAC, MP3)
2070
2152
  // If so, create AudioFrameBuffer to automatically chunk frames
2071
2153
  if (frame.isAudio() && this.codecContext.frameSize > 0) {
2072
- this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels);
2154
+ this.audioFrameBuffer = AudioFrameBuffer.create(this.codecContext.frameSize, this.codecContext.sampleFormat, this.codecContext.sampleRate, this.codecContext.channelLayout, this.codecContext.channels, this.options.audioFrameBuffer);
2073
2155
  }
2074
2156
  this.initialized = true;
2075
2157
  }
@@ -2172,13 +2254,11 @@ export class Encoder {
2172
2254
  const targetRate = pickSupportedRate(inRate, this.codec.supportedSamplerates);
2173
2255
  const targetFmt = pickSupportedSampleFormat(inFmt, this.codec.sampleFormats);
2174
2256
  const targetLayout = pickSupportedLayout(inLayout, this.codec.channelLayouts);
2175
- // When the codec forces a different sample rate, the frames that reach it
2176
- // carry PTS as a running sample counter at that rate (resampler and FIFO
2177
- // paths), so the codec timebase must be 1/target_rate - the input timebase
2178
- // would misread those counts (e.g. 44.1 kHz in -> Opus-forced 48 kHz would
2179
- // play ~8.8% slow). Without rate conversion, keep the frame timebase
2180
- // (typically 1/sample_rate).
2257
+ // When the codec forces a different sample rate, the resampler stamps its
2258
+ // output in 1/target_rate, so that is the codec timebase. Without rate
2259
+ // conversion, keep the frame timebase (typically 1/sample_rate).
2181
2260
  this.resampleChangesRate = targetRate !== inRate;
2261
+ this.resampleInputRate = inRate;
2182
2262
  this.codecContext.timeBase = this.resampleChangesRate ? new Rational(1, targetRate) : frame.timeBase;
2183
2263
  const needsResample = targetRate !== inRate || targetFmt !== inFmt || targetLayout.nbChannels !== inLayout.nbChannels;
2184
2264
  if (needsResample && !this.autoResample) {
@@ -2228,13 +2308,16 @@ export class Encoder {
2228
2308
  * Resample an incoming audio frame to the codec's target format.
2229
2309
  *
2230
2310
  * Reuses a single output frame; `swr_convert_frame` allocates/sizes its buffer.
2231
- * The (fixed-frame-size) audio FIFO copies the samples and re-stamps PTS, so the
2232
- * reused frame and its carried timing are only relevant on the non-FIFO path.
2311
+ * The output keeps the input timing: with rate conversion it is stamped with the
2312
+ * time of its first output sample in 1/target_rate, otherwise it carries the input
2313
+ * frame's PTS and timebase.
2233
2314
  *
2234
2315
  * @param frame - Source audio frame
2235
2316
  *
2236
2317
  * @returns The resampled frame (owned by the encoder, reused across calls)
2237
2318
  *
2319
+ * @throws {FFmpegError} If resampling fails
2320
+ *
2238
2321
  * @internal
2239
2322
  */
2240
2323
  resampleAudio(frame) {
@@ -2243,15 +2326,14 @@ export class Encoder {
2243
2326
  out.format = this.codecContext.sampleFormat;
2244
2327
  out.sampleRate = this.codecContext.sampleRate;
2245
2328
  out.channelLayout = this.codecContext.channelLayout;
2329
+ // Rate conversion holds samples back inside swr, so this call's first output
2330
+ // sample is older than the input frame's first sample. swr_next_pts() accounts
2331
+ // for that delay (as the aresample filter does) and must run before converting.
2332
+ const pts = this.resampleChangesRate ? this.nextResampledPts(frame) : AV_NOPTS_VALUE;
2246
2333
  FFmpegError.throwIfError(this.audioResampler.convertFrame(out, frame), 'Failed to resample audio frame');
2247
2334
  if (this.resampleChangesRate) {
2248
- // Rate conversion changes the sample count, so the input PTS no longer
2249
- // matches the output. Stamp from a running output-sample counter in
2250
- // 1/out_rate (the codec timebase) - the same scheme the FIFO path uses
2251
- // when re-stamping - instead of replicating swr_next_pts() delay tracking.
2252
2335
  out.timeBase = this.codecContext.timeBase;
2253
- out.pts = this.resampledPts;
2254
- this.resampledPts += BigInt(out.nbSamples);
2336
+ out.pts = pts;
2255
2337
  }
2256
2338
  else {
2257
2339
  out.timeBase = frame.timeBase;
@@ -2276,18 +2358,46 @@ export class Encoder {
2276
2358
  out.format = this.codecContext.sampleFormat;
2277
2359
  out.sampleRate = this.codecContext.sampleRate;
2278
2360
  out.channelLayout = this.codecContext.channelLayout;
2361
+ const pts = this.resampleChangesRate ? this.nextResampledPts(null) : AV_NOPTS_VALUE;
2279
2362
  const ret = this.audioResampler.convertFrame(out, null);
2280
2363
  if (ret < 0 || out.nbSamples <= 0) {
2281
2364
  return null;
2282
2365
  }
2283
2366
  if (this.resampleChangesRate) {
2284
- // Continue the running output-sample counter (see resampleAudio)
2285
2367
  out.timeBase = this.codecContext.timeBase;
2286
- out.pts = this.resampledPts;
2287
- this.resampledPts += BigInt(out.nbSamples);
2368
+ out.pts = pts;
2288
2369
  }
2289
2370
  return out;
2290
2371
  }
2372
+ /**
2373
+ * Timestamp of the resampler's next output sample in 1/target_rate.
2374
+ *
2375
+ * Mirrors the aresample filter: the input PTS goes to swr_next_pts() in
2376
+ * 1/(in_rate * out_rate), which subtracts the samples still buffered in the
2377
+ * resampler. Without an input PTS (or when draining) swr continues from the
2378
+ * samples it has output so far.
2379
+ *
2380
+ * @param frame - Input frame about to be converted, or null when draining
2381
+ *
2382
+ * @returns Output timestamp in 1/target_rate
2383
+ *
2384
+ * @internal
2385
+ */
2386
+ nextResampledPts(frame) {
2387
+ const inRate = BigInt(this.resampleInputRate);
2388
+ const outRate = BigInt(this.codecContext.sampleRate);
2389
+ let inPts = AV_NOPTS_VALUE;
2390
+ if (frame && frame.pts !== AV_NOPTS_VALUE) {
2391
+ const tb = frame.timeBase;
2392
+ if (tb.num > 0 && tb.den > 0) {
2393
+ inPts = avRescaleRnd(frame.pts, BigInt(tb.num) * inRate * outRate, BigInt(tb.den), AV_ROUND_NEAR_INF);
2394
+ }
2395
+ }
2396
+ // Rounded division back to 1/out_rate (ROUNDED_DIV in af_aresample)
2397
+ const outPts = this.audioResampler.nextPts(inPts);
2398
+ const half = inRate / 2n;
2399
+ return (outPts >= 0n ? outPts + half : outPts - half) / inRate;
2400
+ }
2291
2401
  /**
2292
2402
  * Configure the codec context's pixel format from the first video frame.
2293
2403
  *
@@ -2374,21 +2484,31 @@ export class Encoder {
2374
2484
  * Prepare frame for encoding.
2375
2485
  *
2376
2486
  * Implements FFmpeg's frame_encode() pre-encoding logic:
2377
- * 1. Video: Sets frame.quality from encoder's globalQuality (like -qscale)
2378
- * 2. Audio: Validates channel count consistency for encoders without PARAM_CHANGE capability
2487
+ * 1. Video: Keeps timestamps strictly increasing like the CLI's VFR sync
2488
+ * 2. Video: Sets frame.quality from encoder's globalQuality (like -qscale)
2489
+ * 3. Audio: Validates channel count consistency for encoders without PARAM_CHANGE capability
2379
2490
  *
2380
2491
  * This matches FFmpeg CLI behavior where these properties are automatically managed.
2381
2492
  *
2382
2493
  * @param frame - Frame to prepare for encoding
2383
2494
  *
2495
+ * @returns False if the frame must be dropped
2496
+ *
2384
2497
  * @throws {Error} If audio channel count changed and encoder doesn't support parameter changes
2385
2498
  *
2386
2499
  * @internal
2387
2500
  */
2388
2501
  prepareFrameForEncoding(frame) {
2389
- // Clear pict_type - encoder will determine frame types based on its own settings
2390
- // Input stream's frame type hints are irrelevant when re-encoding
2391
- frame.pictType = AV_PICTURE_TYPE_NONE;
2502
+ // Clear pict_type - encoder will determine frame types based on its own settings.
2503
+ // Input stream's frame type hints are irrelevant when re-encoding. Opt out with
2504
+ // { preserveFramePictType: true } to force keyframes via frame.pictType = I.
2505
+ if (!this.options.preserveFramePictType) {
2506
+ frame.pictType = AV_PICTURE_TYPE_NONE;
2507
+ }
2508
+ if (this.keyframeRequested) {
2509
+ this.keyframeRequested = false;
2510
+ frame.pictType = AV_PICTURE_TYPE_I;
2511
+ }
2392
2512
  // Adjust frame PTS and timebase to encoder timebase
2393
2513
  // This matches FFmpeg's adjust_frame_pts_to_encoder_tb() behavior which:
2394
2514
  // 1. Converts PTS from frame's timebase to encoder's timebase (av_rescale_q)
@@ -2433,6 +2553,26 @@ export class Encoder {
2433
2553
  frame.duration = frameDuration;
2434
2554
  const codecType = this.codecContext.codecType;
2435
2555
  if (codecType === AVMEDIA_TYPE_VIDEO) {
2556
+ // Encoders need strictly increasing pts, but frames arriving in a burst can
2557
+ // round onto the same tick of the encoder time base (e.g. 1/25). Like FFmpeg
2558
+ // CLI's VFR sync (video_sync_process): a frame that still reaches the next
2559
+ // free tick moves there, one that ends before it is dropped.
2560
+ if (frame.pts !== AV_NOPTS_VALUE) {
2561
+ const nextPts = this.nextVideoPts;
2562
+ if (nextPts !== undefined && frame.pts < nextPts) {
2563
+ if (frame.pts + frameDuration < nextPts) {
2564
+ // A requested keyframe goes to the next frame instead, as in the CLI
2565
+ this.droppedKeyframe ||= frame.pictType === AV_PICTURE_TYPE_I;
2566
+ return false;
2567
+ }
2568
+ frame.pts = nextPts;
2569
+ }
2570
+ this.nextVideoPts = frame.pts + 1n;
2571
+ }
2572
+ if (this.droppedKeyframe) {
2573
+ frame.pictType = AV_PICTURE_TYPE_I;
2574
+ this.droppedKeyframe = false;
2575
+ }
2436
2576
  // Video: Set frame quality from encoder's global quality
2437
2577
  // Only set if encoder has globalQuality configured and frame doesn't already have quality set
2438
2578
  const globalQuality = this.codecContext.globalQuality;
@@ -2454,6 +2594,7 @@ export class Encoder {
2454
2594
  }
2455
2595
  }
2456
2596
  }
2597
+ return true;
2457
2598
  }
2458
2599
  /**
2459
2600
  * Dispose of encoder.