node-av 5.2.4 → 6.0.0-beta.11

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 (111) hide show
  1. package/README.md +15 -1
  2. package/dist/api/bitstream-filter.d.ts +110 -87
  3. package/dist/api/bitstream-filter.js +161 -103
  4. package/dist/api/bitstream-filter.js.map +1 -1
  5. package/dist/api/decoder.d.ts +177 -15
  6. package/dist/api/decoder.js +335 -28
  7. package/dist/api/decoder.js.map +1 -1
  8. package/dist/api/demuxer.d.ts +30 -24
  9. package/dist/api/demuxer.js +4 -5
  10. package/dist/api/demuxer.js.map +1 -1
  11. package/dist/api/device.js.map +1 -1
  12. package/dist/api/encoder-pool.d.ts +220 -0
  13. package/dist/api/encoder-pool.js +285 -0
  14. package/dist/api/encoder-pool.js.map +1 -0
  15. package/dist/api/encoder.d.ts +194 -7
  16. package/dist/api/encoder.js +431 -71
  17. package/dist/api/encoder.js.map +1 -1
  18. package/dist/api/filter-complex.d.ts +2 -1
  19. package/dist/api/filter-complex.js +11 -7
  20. package/dist/api/filter-complex.js.map +1 -1
  21. package/dist/api/filter-presets.d.ts +130 -654
  22. package/dist/api/filter-presets.js +180 -858
  23. package/dist/api/filter-presets.js.map +1 -1
  24. package/dist/api/filter.js +12 -9
  25. package/dist/api/filter.js.map +1 -1
  26. package/dist/api/fmp4-stream.js +1 -1
  27. package/dist/api/fmp4-stream.js.map +1 -1
  28. package/dist/api/index.d.ts +6 -6
  29. package/dist/api/index.js +8 -8
  30. package/dist/api/index.js.map +1 -1
  31. package/dist/api/muxer.d.ts +43 -15
  32. package/dist/api/muxer.js +79 -27
  33. package/dist/api/muxer.js.map +1 -1
  34. package/dist/api/pipeline.d.ts +50 -0
  35. package/dist/api/pipeline.js +138 -22
  36. package/dist/api/pipeline.js.map +1 -1
  37. package/dist/api/probe.d.ts +128 -0
  38. package/dist/api/probe.js +227 -0
  39. package/dist/api/probe.js.map +1 -0
  40. package/dist/api/rtp-stream.d.ts +14 -11
  41. package/dist/api/rtp-stream.js +23 -48
  42. package/dist/api/rtp-stream.js.map +1 -1
  43. package/dist/api/scaler.d.ts +431 -0
  44. package/dist/api/scaler.js +620 -0
  45. package/dist/api/scaler.js.map +1 -0
  46. package/dist/api/utilities/async-queue.d.ts +27 -1
  47. package/dist/api/utilities/async-queue.js +38 -3
  48. package/dist/api/utilities/async-queue.js.map +1 -1
  49. package/dist/api/utilities/codec-format.d.ts +87 -0
  50. package/dist/api/utilities/codec-format.js +117 -0
  51. package/dist/api/utilities/codec-format.js.map +1 -0
  52. package/dist/api/utilities/electron-shared-texture.d.ts +41 -1
  53. package/dist/api/utilities/electron-shared-texture.js +41 -4
  54. package/dist/api/utilities/electron-shared-texture.js.map +1 -1
  55. package/dist/api/utilities/index.d.ts +2 -1
  56. package/dist/api/utilities/index.js +2 -0
  57. package/dist/api/utilities/index.js.map +1 -1
  58. package/dist/api/webrtc-stream.d.ts +0 -1
  59. package/dist/api/webrtc-stream.js +0 -1
  60. package/dist/api/webrtc-stream.js.map +1 -1
  61. package/dist/constants/bsf-options.d.ts +333 -0
  62. package/dist/constants/bsf-options.js +7 -0
  63. package/dist/constants/bsf-options.js.map +1 -0
  64. package/dist/constants/constants.d.ts +109 -0
  65. package/dist/constants/constants.js +110 -0
  66. package/dist/constants/constants.js.map +1 -1
  67. package/dist/constants/decoders.d.ts +636 -618
  68. package/dist/constants/decoders.js +1 -3
  69. package/dist/constants/decoders.js.map +1 -1
  70. package/dist/constants/encoders.d.ts +300 -282
  71. package/dist/constants/encoders.js +0 -2
  72. package/dist/constants/encoders.js.map +1 -1
  73. package/dist/constants/filter-options.d.ts +10915 -0
  74. package/dist/constants/filter-options.js +7 -0
  75. package/dist/constants/filter-options.js.map +1 -0
  76. package/dist/constants/format-options.d.ts +3056 -0
  77. package/dist/constants/format-options.js +7 -0
  78. package/dist/constants/format-options.js.map +1 -0
  79. package/dist/constants/formats.d.ts +18 -0
  80. package/dist/constants/formats.js +7 -0
  81. package/dist/constants/formats.js.map +1 -0
  82. package/dist/constants/index.d.ts +5 -0
  83. package/dist/constants/options.d.ts +4073 -0
  84. package/dist/constants/options.js +7 -0
  85. package/dist/constants/options.js.map +1 -0
  86. package/dist/lib/binding.d.ts +5 -1
  87. package/dist/lib/binding.js.map +1 -1
  88. package/dist/lib/codec.d.ts +36 -5
  89. package/dist/lib/codec.js +37 -4
  90. package/dist/lib/codec.js.map +1 -1
  91. package/dist/lib/dictionary.d.ts +1 -1
  92. package/dist/lib/dictionary.js.map +1 -1
  93. package/dist/lib/error.d.ts +69 -0
  94. package/dist/lib/error.js +92 -0
  95. package/dist/lib/error.js.map +1 -1
  96. package/dist/lib/frame.d.ts +55 -3
  97. package/dist/lib/frame.js +59 -3
  98. package/dist/lib/frame.js.map +1 -1
  99. package/dist/lib/index.d.ts +1 -1
  100. package/dist/lib/index.js.map +1 -1
  101. package/dist/lib/native-types.d.ts +68 -0
  102. package/dist/lib/packet.d.ts +22 -3
  103. package/dist/lib/packet.js +24 -3
  104. package/dist/lib/packet.js.map +1 -1
  105. package/dist/lib/utilities.d.ts +45 -0
  106. package/dist/lib/utilities.js +49 -0
  107. package/dist/lib/utilities.js.map +1 -1
  108. package/dist/webrtc/index.d.ts +3 -0
  109. package/dist/webrtc/index.js +7 -0
  110. package/dist/webrtc/index.js.map +1 -0
  111. package/package.json +34 -23
@@ -50,7 +50,8 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
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_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, } from '../constants/constants.js';
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_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
55
  import { CodecContext } from '../lib/codec-context.js';
55
56
  import { Codec } from '../lib/codec.js';
56
57
  import { Dictionary } from '../lib/dictionary.js';
@@ -58,10 +59,13 @@ import { FFmpegError } from '../lib/error.js';
58
59
  import { Frame } from '../lib/frame.js';
59
60
  import { Packet } from '../lib/packet.js';
60
61
  import { Rational } from '../lib/rational.js';
61
- import { avRescaleQ } from '../lib/utilities.js';
62
+ import { SoftwareResampleContext } from '../lib/software-resample-context.js';
63
+ import { SoftwareScaleContext } from '../lib/software-scale-context.js';
64
+ import { avChannelLayoutDefault, avGetPixFmtName, avGetSampleFmtName, avRescaleQ } from '../lib/utilities.js';
62
65
  import { AudioFrameBuffer } from './audio-frame-buffer.js';
63
66
  import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
64
67
  import { AsyncQueue } from './utilities/async-queue.js';
68
+ import { pickSupportedLayout, pickSupportedPixelFormat, pickSupportedRate, pickSupportedSampleFormat } from './utilities/codec-format.js';
65
69
  import { SchedulerControl } from './utilities/scheduler.js';
66
70
  import { parseBitrate } from './utils.js';
67
71
  /**
@@ -132,6 +136,14 @@ export class Encoder {
132
136
  opts;
133
137
  options;
134
138
  audioFrameBuffer;
139
+ autoResample;
140
+ audioResampler;
141
+ resampledFrame;
142
+ audioInputLayout;
143
+ autoFormat;
144
+ videoScaler;
145
+ scaledFrame;
146
+ videoTargetFormat;
135
147
  // Worker pattern for push-based processing
136
148
  inputQueue;
137
149
  outputQueue;
@@ -154,10 +166,12 @@ export class Encoder {
154
166
  this.codec = codec;
155
167
  this.options = options;
156
168
  this.opts = opts;
169
+ this.autoResample = options.autoResample ?? false;
170
+ this.autoFormat = options.autoFormat ?? false;
157
171
  this.packet = new Packet();
158
172
  this.packet.alloc();
159
- this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
160
- this.outputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE);
173
+ this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE, (f) => f.free());
174
+ this.outputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE, (p) => p.free());
161
175
  }
162
176
  /**
163
177
  * Create an encoder with specified codec and options.
@@ -262,8 +276,11 @@ export class Encoder {
262
276
  if (options.threadType !== undefined) {
263
277
  codecContext.threadType = options.threadType;
264
278
  }
265
- const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
266
- const encoder = new Encoder(codecContext, codec, options, opts);
279
+ // Loose view for internal use: the public signature narrows `options` to the
280
+ // codec, but internally codec options are handled as a generic dictionary.
281
+ const looseOptions = options;
282
+ const opts = looseOptions.options ? Dictionary.fromObject(looseOptions.options) : undefined;
283
+ const encoder = new Encoder(codecContext, codec, looseOptions, opts);
267
284
  if (options.signal) {
268
285
  options.signal.throwIfAborted();
269
286
  encoder.signal = options.signal;
@@ -375,14 +392,125 @@ export class Encoder {
375
392
  if (options.threadType !== undefined) {
376
393
  codecContext.threadType = options.threadType;
377
394
  }
378
- const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
379
- const encoder = new Encoder(codecContext, codec, options, opts);
395
+ // Loose view for internal use: the public signature narrows `options` to the
396
+ // codec, but internally codec options are handled as a generic dictionary.
397
+ const looseOptions = options;
398
+ const opts = looseOptions.options ? Dictionary.fromObject(looseOptions.options) : undefined;
399
+ const encoder = new Encoder(codecContext, codec, looseOptions, opts);
380
400
  if (options.signal) {
381
401
  options.signal.throwIfAborted();
382
402
  encoder.signal = options.signal;
383
403
  }
384
404
  return encoder;
385
405
  }
406
+ /**
407
+ * Encode a single frame into a self-contained image buffer.
408
+ *
409
+ * One-shot, stateless helper for intra-only image codecs (MJPEG, PNG, WebP, ...).
410
+ * Creates a fresh encoder, encodes the frame, flushes and frees everything in one call.
411
+ * The encoder adopts dimensions, pixel format and hardware context from the frame,
412
+ * so any frame size works without reconfiguration.
413
+ *
414
+ * @param encoderCodec - Encoder codec (name, ID, branded constant, or Codec)
415
+ *
416
+ * @param frame - Frame to encode
417
+ *
418
+ * @param options - Optional encoder configuration (e.g. `{ options: { q: 3 } }` for MJPEG quality)
419
+ *
420
+ * @returns Encoded image bytes
421
+ *
422
+ * @throws {FFmpegError} If the encoder is not found or encoding fails
423
+ *
424
+ * @throws {Error} If the encoder produced no output
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * const jpeg = await Encoder.encodeOne(FF_ENCODER_MJPEG, frame, { options: { q: 3 } });
429
+ * ```
430
+ *
431
+ * @see {@link EncoderPool} For reusing encoders across recurring resolutions
432
+ */
433
+ static async encodeOne(encoderCodec, frame, options = {}) {
434
+ const env_1 = { stack: [], error: void 0, hasError: false };
435
+ try {
436
+ const encoder = __addDisposableResource(env_1, await Encoder.create(encoderCodec, options), false);
437
+ const packets = [...(await encoder.encodeAll(frame)), ...(await encoder.encodeAll(null))];
438
+ try {
439
+ const data = packets[0]?.data;
440
+ if (!data) {
441
+ throw new Error(`Encoder '${encoder.getCodec().name}' produced no output for frame`);
442
+ }
443
+ return data;
444
+ }
445
+ finally {
446
+ for (const packet of packets) {
447
+ packet.free();
448
+ }
449
+ }
450
+ }
451
+ catch (e_1) {
452
+ env_1.error = e_1;
453
+ env_1.hasError = true;
454
+ }
455
+ finally {
456
+ __disposeResources(env_1);
457
+ }
458
+ }
459
+ /**
460
+ * Encode a single frame into a self-contained image buffer synchronously.
461
+ * Synchronous version of encodeOne.
462
+ *
463
+ * One-shot, stateless helper for intra-only image codecs (MJPEG, PNG, WebP, ...).
464
+ * Creates a fresh encoder, encodes the frame, flushes and frees everything in one call.
465
+ * The encoder adopts dimensions, pixel format and hardware context from the frame,
466
+ * so any frame size works without reconfiguration.
467
+ *
468
+ * @param encoderCodec - Encoder codec (name, ID, branded constant, or Codec)
469
+ *
470
+ * @param frame - Frame to encode
471
+ *
472
+ * @param options - Optional encoder configuration (e.g. `{ options: { q: 3 } }` for MJPEG quality)
473
+ *
474
+ * @returns Encoded image bytes
475
+ *
476
+ * @throws {FFmpegError} If the encoder is not found or encoding fails
477
+ *
478
+ * @throws {Error} If the encoder produced no output
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * const jpeg = Encoder.encodeOneSync(FF_ENCODER_MJPEG, frame, { options: { q: 3 } });
483
+ * ```
484
+ *
485
+ * @see {@link encodeOne} For async version
486
+ * @see {@link EncoderPool} For reusing encoders across recurring resolutions
487
+ */
488
+ static encodeOneSync(encoderCodec, frame, options = {}) {
489
+ const env_2 = { stack: [], error: void 0, hasError: false };
490
+ try {
491
+ const encoder = __addDisposableResource(env_2, Encoder.createSync(encoderCodec, options), false);
492
+ const packets = [...encoder.encodeAllSync(frame), ...encoder.encodeAllSync(null)];
493
+ try {
494
+ const data = packets[0]?.data;
495
+ if (!data) {
496
+ throw new Error(`Encoder '${encoder.getCodec().name}' produced no output for frame`);
497
+ }
498
+ return data;
499
+ }
500
+ finally {
501
+ for (const packet of packets) {
502
+ packet.free();
503
+ }
504
+ }
505
+ }
506
+ catch (e_2) {
507
+ env_2.error = e_2;
508
+ env_2.hasError = true;
509
+ }
510
+ finally {
511
+ __disposeResources(env_2);
512
+ }
513
+ }
386
514
  /**
387
515
  * Check if encoder is open.
388
516
  *
@@ -622,8 +750,15 @@ export class Encoder {
622
750
  // Open encoder if not already done
623
751
  this.initializePromise ??= this.initialize(frame);
624
752
  await this.initializePromise;
753
+ // Give an unspecified-layout frame the concrete native layout the codec was
754
+ // opened with (and the resampler configured for), so both accept it.
755
+ if (this.audioInputLayout) {
756
+ frame.channelLayout = this.audioInputLayout;
757
+ }
758
+ // Convert to the codec's format first (audio resample / video pixfmt).
759
+ const input = this.audioResampler ? this.resampleAudio(frame) : this.videoScaler ? this.scaleVideo(frame) : frame;
625
760
  // Prepare frame for encoding (set quality, validate channel count)
626
- this.prepareFrameForEncoding(frame);
761
+ this.prepareFrameForEncoding(input);
627
762
  const encode = async (newFrame) => {
628
763
  const sendRet = await this.codecContext.sendFrame(newFrame);
629
764
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -633,10 +768,10 @@ export class Encoder {
633
768
  };
634
769
  if (this.audioFrameBuffer) {
635
770
  // Push frame into buffer - actual sending happens in receive()
636
- await this.audioFrameBuffer.push(frame);
771
+ await this.audioFrameBuffer.push(input);
637
772
  }
638
773
  else {
639
- await encode(frame);
774
+ await encode(input);
640
775
  }
641
776
  }
642
777
  /**
@@ -690,8 +825,15 @@ export class Encoder {
690
825
  if (!this.initialized) {
691
826
  this.initializeSync(frame);
692
827
  }
828
+ // Give an unspecified-layout frame the concrete native layout the codec was
829
+ // opened with (and the resampler configured for), so both accept it.
830
+ if (this.audioInputLayout) {
831
+ frame.channelLayout = this.audioInputLayout;
832
+ }
833
+ // Convert to the codec's format first (audio resample / video pixfmt).
834
+ const input = this.audioResampler ? this.resampleAudio(frame) : this.videoScaler ? this.scaleVideo(frame) : frame;
693
835
  // Prepare frame for encoding (set quality, validate channel count)
694
- this.prepareFrameForEncoding(frame);
836
+ this.prepareFrameForEncoding(input);
695
837
  const encode = (newFrame) => {
696
838
  const sendRet = this.codecContext.sendFrameSync(newFrame);
697
839
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -701,10 +843,10 @@ export class Encoder {
701
843
  };
702
844
  if (this.audioFrameBuffer) {
703
845
  // Push frame into buffer - actual sending happens in receiveSync()
704
- this.audioFrameBuffer.pushSync(frame);
846
+ this.audioFrameBuffer.pushSync(input);
705
847
  }
706
848
  else {
707
- encode(frame);
849
+ encode(input);
708
850
  }
709
851
  }
710
852
  /**
@@ -906,9 +1048,9 @@ export class Encoder {
906
1048
  return;
907
1049
  }
908
1050
  for await (const frame_1 of frames) {
909
- const env_1 = { stack: [], error: void 0, hasError: false };
1051
+ const env_3 = { stack: [], error: void 0, hasError: false };
910
1052
  try {
911
- const frame = __addDisposableResource(env_1, frame_1, false);
1053
+ const frame = __addDisposableResource(env_3, frame_1, false);
912
1054
  this.signal?.throwIfAborted();
913
1055
  if (frame === null) {
914
1056
  yield* finalize();
@@ -916,12 +1058,12 @@ export class Encoder {
916
1058
  }
917
1059
  yield* processFrame(frame);
918
1060
  }
919
- catch (e_1) {
920
- env_1.error = e_1;
921
- env_1.hasError = true;
1061
+ catch (e_3) {
1062
+ env_3.error = e_3;
1063
+ env_3.hasError = true;
922
1064
  }
923
1065
  finally {
924
- __disposeResources(env_1);
1066
+ __disposeResources(env_3);
925
1067
  }
926
1068
  }
927
1069
  }
@@ -1017,9 +1159,9 @@ export class Encoder {
1017
1159
  }
1018
1160
  // Case 3: Iterable of frames
1019
1161
  for (const frame_2 of frames) {
1020
- const env_2 = { stack: [], error: void 0, hasError: false };
1162
+ const env_4 = { stack: [], error: void 0, hasError: false };
1021
1163
  try {
1022
- const frame = __addDisposableResource(env_2, frame_2, false);
1164
+ const frame = __addDisposableResource(env_4, frame_2, false);
1023
1165
  // Check for EOF signal from upstream
1024
1166
  if (frame === null) {
1025
1167
  yield* finalize();
@@ -1027,12 +1169,12 @@ export class Encoder {
1027
1169
  }
1028
1170
  yield* processFrame(frame);
1029
1171
  }
1030
- catch (e_2) {
1031
- env_2.error = e_2;
1032
- env_2.hasError = true;
1172
+ catch (e_4) {
1173
+ env_4.error = e_4;
1174
+ env_4.hasError = true;
1033
1175
  }
1034
1176
  finally {
1035
- __disposeResources(env_2);
1177
+ __disposeResources(env_4);
1036
1178
  }
1037
1179
  }
1038
1180
  // No fallback flush - only flush on explicit EOF
@@ -1069,23 +1211,33 @@ export class Encoder {
1069
1211
  if (this.isClosed || !this.initialized) {
1070
1212
  return;
1071
1213
  }
1214
+ // Drain samples buffered inside the resampler into the FIFO/encoder first.
1215
+ const drained = this.drainResampler();
1216
+ if (drained) {
1217
+ if (this.audioFrameBuffer) {
1218
+ await this.audioFrameBuffer.push(drained);
1219
+ }
1220
+ else {
1221
+ await this.codecContext.sendFrame(drained);
1222
+ }
1223
+ }
1072
1224
  // If using AudioFrameBuffer, flush remaining buffered samples first
1073
1225
  if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1074
1226
  // Pull any remaining partial frame (may be less than frameSize)
1075
1227
  // For the final frame, we pad or truncate as needed
1076
1228
  let _bufferedFrame;
1077
1229
  while (!this.isClosed && (_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
1078
- const env_3 = { stack: [], error: void 0, hasError: false };
1230
+ const env_5 = { stack: [], error: void 0, hasError: false };
1079
1231
  try {
1080
- const bufferedFrame = __addDisposableResource(env_3, _bufferedFrame, false);
1232
+ const bufferedFrame = __addDisposableResource(env_5, _bufferedFrame, false);
1081
1233
  await this.codecContext.sendFrame(bufferedFrame);
1082
1234
  }
1083
- catch (e_3) {
1084
- env_3.error = e_3;
1085
- env_3.hasError = true;
1235
+ catch (e_5) {
1236
+ env_5.error = e_5;
1237
+ env_5.hasError = true;
1086
1238
  }
1087
1239
  finally {
1088
- __disposeResources(env_3);
1240
+ __disposeResources(env_5);
1089
1241
  }
1090
1242
  }
1091
1243
  }
@@ -1129,23 +1281,33 @@ export class Encoder {
1129
1281
  if (this.isClosed || !this.initialized) {
1130
1282
  return;
1131
1283
  }
1284
+ // Drain samples buffered inside the resampler into the FIFO/encoder first.
1285
+ const drained = this.drainResampler();
1286
+ if (drained) {
1287
+ if (this.audioFrameBuffer) {
1288
+ this.audioFrameBuffer.pushSync(drained);
1289
+ }
1290
+ else {
1291
+ this.codecContext.sendFrameSync(drained);
1292
+ }
1293
+ }
1132
1294
  // If using AudioFrameBuffer, flush remaining buffered samples first
1133
1295
  if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1134
1296
  // Pull any remaining partial frame (may be less than frameSize)
1135
1297
  // For the final frame, we pad or truncate as needed
1136
1298
  let _bufferedFrame;
1137
1299
  while (!this.isClosed && (_bufferedFrame = this.audioFrameBuffer.pullSync()) !== null) {
1138
- const env_4 = { stack: [], error: void 0, hasError: false };
1300
+ const env_6 = { stack: [], error: void 0, hasError: false };
1139
1301
  try {
1140
- const bufferedFrame = __addDisposableResource(env_4, _bufferedFrame, false);
1302
+ const bufferedFrame = __addDisposableResource(env_6, _bufferedFrame, false);
1141
1303
  this.codecContext.sendFrameSync(bufferedFrame);
1142
1304
  }
1143
- catch (e_4) {
1144
- env_4.error = e_4;
1145
- env_4.hasError = true;
1305
+ catch (e_6) {
1306
+ env_6.error = e_6;
1307
+ env_6.hasError = true;
1146
1308
  }
1147
1309
  finally {
1148
- __disposeResources(env_4);
1310
+ __disposeResources(env_6);
1149
1311
  }
1150
1312
  }
1151
1313
  }
@@ -1288,19 +1450,19 @@ export class Encoder {
1288
1450
  // Clear previous packet data
1289
1451
  this.packet.unref();
1290
1452
  if (this.audioFrameBuffer?.hasFrame()) {
1291
- const env_5 = { stack: [], error: void 0, hasError: false };
1453
+ const env_7 = { stack: [], error: void 0, hasError: false };
1292
1454
  try {
1293
- const bufferedFrame = __addDisposableResource(env_5, await this.audioFrameBuffer.pull(), false);
1455
+ const bufferedFrame = __addDisposableResource(env_7, await this.audioFrameBuffer.pull(), false);
1294
1456
  if (bufferedFrame) {
1295
1457
  await this.codecContext.sendFrame(bufferedFrame);
1296
1458
  }
1297
1459
  }
1298
- catch (e_5) {
1299
- env_5.error = e_5;
1300
- env_5.hasError = true;
1460
+ catch (e_7) {
1461
+ env_7.error = e_7;
1462
+ env_7.hasError = true;
1301
1463
  }
1302
1464
  finally {
1303
- __disposeResources(env_5);
1465
+ __disposeResources(env_7);
1304
1466
  }
1305
1467
  }
1306
1468
  const ret = await this.codecContext.receivePacket(this.packet);
@@ -1393,19 +1555,19 @@ export class Encoder {
1393
1555
  // Clear previous packet data
1394
1556
  this.packet.unref();
1395
1557
  if (this.audioFrameBuffer?.hasFrame()) {
1396
- const env_6 = { stack: [], error: void 0, hasError: false };
1558
+ const env_8 = { stack: [], error: void 0, hasError: false };
1397
1559
  try {
1398
- const bufferedFrame = __addDisposableResource(env_6, this.audioFrameBuffer.pullSync(), false);
1560
+ const bufferedFrame = __addDisposableResource(env_8, this.audioFrameBuffer.pullSync(), false);
1399
1561
  if (bufferedFrame) {
1400
1562
  this.codecContext.sendFrameSync(bufferedFrame);
1401
1563
  }
1402
1564
  }
1403
- catch (e_6) {
1404
- env_6.error = e_6;
1405
- env_6.hasError = true;
1565
+ catch (e_8) {
1566
+ env_8.error = e_8;
1567
+ env_8.hasError = true;
1406
1568
  }
1407
1569
  finally {
1408
- __disposeResources(env_6);
1570
+ __disposeResources(env_8);
1409
1571
  }
1410
1572
  }
1411
1573
  const ret = this.codecContext.receivePacketSync(this.packet);
@@ -1488,10 +1650,21 @@ export class Encoder {
1488
1650
  return;
1489
1651
  }
1490
1652
  this.isClosed = true;
1491
- // Close queues
1492
1653
  this.inputQueue.close();
1493
1654
  this.outputQueue.close();
1655
+ this.inputQueue.clear();
1656
+ this.outputQueue.clear();
1494
1657
  this.packet.free();
1658
+ this.audioFrameBuffer?.[Symbol.dispose]();
1659
+ this.audioFrameBuffer = undefined;
1660
+ this.audioResampler?.[Symbol.dispose]();
1661
+ this.audioResampler = undefined;
1662
+ this.resampledFrame?.free();
1663
+ this.resampledFrame = undefined;
1664
+ this.videoScaler?.[Symbol.dispose]();
1665
+ this.videoScaler = undefined;
1666
+ this.scaledFrame?.free();
1667
+ this.scaledFrame = undefined;
1495
1668
  this.codecContext.freeContext();
1496
1669
  this.initialized = false;
1497
1670
  }
@@ -1535,9 +1708,9 @@ export class Encoder {
1535
1708
  try {
1536
1709
  // Outer loop - receive frames
1537
1710
  while (!this.inputQueue.isClosed) {
1538
- const env_7 = { stack: [], error: void 0, hasError: false };
1711
+ const env_9 = { stack: [], error: void 0, hasError: false };
1539
1712
  try {
1540
- const frame = __addDisposableResource(env_7, await this.inputQueue.receive(), false);
1713
+ const frame = __addDisposableResource(env_9, await this.inputQueue.receive(), false);
1541
1714
  if (!frame)
1542
1715
  break;
1543
1716
  // Open encoder if not already done
@@ -1556,12 +1729,12 @@ export class Encoder {
1556
1729
  await this.outputQueue.send(packet); // Only send actual packets
1557
1730
  }
1558
1731
  }
1559
- catch (e_7) {
1560
- env_7.error = e_7;
1561
- env_7.hasError = true;
1732
+ catch (e_9) {
1733
+ env_9.error = e_9;
1734
+ env_9.hasError = true;
1562
1735
  }
1563
1736
  finally {
1564
- __disposeResources(env_7);
1737
+ __disposeResources(env_9);
1565
1738
  }
1566
1739
  }
1567
1740
  // Flush encoder at end
@@ -1685,7 +1858,8 @@ export class Encoder {
1685
1858
  }
1686
1859
  this.codecContext.width = frame.width;
1687
1860
  this.codecContext.height = frame.height;
1688
- this.codecContext.pixelFormat = frame.format;
1861
+ // Pick a codec-supported pixel format (converting on demand when autoFormat).
1862
+ this.setupVideoFormat(frame);
1689
1863
  this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
1690
1864
  this.codecContext.colorRange = frame.colorRange;
1691
1865
  this.codecContext.colorPrimaries = frame.colorPrimaries;
@@ -1697,12 +1871,8 @@ export class Encoder {
1697
1871
  }
1698
1872
  }
1699
1873
  else {
1700
- // Audio: Always use frame timebase (which is typically 1/sample_rate)
1701
- // This ensures correct PTS progression for audio frames
1702
- this.codecContext.timeBase = frame.timeBase;
1703
- this.codecContext.sampleRate = frame.sampleRate;
1704
- this.codecContext.sampleFormat = frame.format;
1705
- this.codecContext.channelLayout = frame.channelLayout;
1874
+ // Audio: pick codec-supported sample rate/format/layout (resampling on demand).
1875
+ this.setupAudioParams(frame);
1706
1876
  }
1707
1877
  // Setup hardware acceleration with validation
1708
1878
  this.setupHardwareAcceleration(frame);
@@ -1778,7 +1948,8 @@ export class Encoder {
1778
1948
  }
1779
1949
  this.codecContext.width = frame.width;
1780
1950
  this.codecContext.height = frame.height;
1781
- this.codecContext.pixelFormat = frame.format;
1951
+ // Pick a codec-supported pixel format (converting on demand when autoFormat).
1952
+ this.setupVideoFormat(frame);
1782
1953
  this.codecContext.sampleAspectRatio = frame.sampleAspectRatio;
1783
1954
  this.codecContext.colorRange = frame.colorRange;
1784
1955
  this.codecContext.colorPrimaries = frame.colorPrimaries;
@@ -1790,12 +1961,8 @@ export class Encoder {
1790
1961
  }
1791
1962
  }
1792
1963
  else {
1793
- // Audio: Always use frame timebase (which is typically 1/sample_rate)
1794
- // This ensures correct PTS progression for audio frames
1795
- this.codecContext.timeBase = frame.timeBase;
1796
- this.codecContext.sampleRate = frame.sampleRate;
1797
- this.codecContext.sampleFormat = frame.format;
1798
- this.codecContext.channelLayout = frame.channelLayout;
1964
+ // Audio: pick codec-supported sample rate/format/layout (resampling on demand).
1965
+ this.setupAudioParams(frame);
1799
1966
  }
1800
1967
  // Setup hardware acceleration with validation
1801
1968
  this.setupHardwareAcceleration(frame);
@@ -1888,6 +2055,199 @@ export class Encoder {
1888
2055
  }
1889
2056
  }
1890
2057
  }
2058
+ /**
2059
+ * Configure the codec context's audio parameters from the first frame.
2060
+ *
2061
+ * Audio encoders only accept specific sample rates / sample formats / channel
2062
+ * layouts. This picks codec-supported targets; if they differ from the input it
2063
+ * either sets up a resampler (when `autoResample`) or throws a descriptive error.
2064
+ *
2065
+ * @param frame - First audio frame
2066
+ *
2067
+ * @throws {Error} If the input is unsupported and `autoResample` is disabled
2068
+ *
2069
+ * @throws {FFmpegError} If the resampler fails to configure
2070
+ *
2071
+ * @internal
2072
+ */
2073
+ setupAudioParams(frame) {
2074
+ // Always use frame timebase (typically 1/sample_rate) for correct audio PTS.
2075
+ this.codecContext.timeBase = frame.timeBase;
2076
+ const inRate = frame.sampleRate;
2077
+ const inFmt = frame.format;
2078
+ // Codec open and swr both need a concrete layout. PCM/raw frames often carry
2079
+ // an unspecified layout (order UNSPEC, mask 0); normalize it to the canonical
2080
+ // native layout and re-apply it to each incoming frame (see encode()) so it
2081
+ // matches the opened codec context / resampler input.
2082
+ let inLayout = frame.channelLayout;
2083
+ if (inLayout.order === AV_CHANNEL_ORDER_UNSPEC) {
2084
+ inLayout = avChannelLayoutDefault(inLayout.nbChannels);
2085
+ this.audioInputLayout = inLayout;
2086
+ }
2087
+ const targetRate = pickSupportedRate(inRate, this.codec.supportedSamplerates);
2088
+ const targetFmt = pickSupportedSampleFormat(inFmt, this.codec.sampleFormats);
2089
+ const targetLayout = pickSupportedLayout(inLayout, this.codec.channelLayouts);
2090
+ const needsResample = targetRate !== inRate || targetFmt !== inFmt || targetLayout.nbChannels !== inLayout.nbChannels;
2091
+ if (needsResample && !this.autoResample) {
2092
+ const rates = this.codec.supportedSamplerates;
2093
+ throw new Error(`Encoder '${this.codec.name}' does not support the input audio format ` +
2094
+ `(${inRate} Hz, ${avGetSampleFmtName(inFmt) ?? inFmt}, ${inLayout.nbChannels}ch)` +
2095
+ (rates && rates.length > 0 ? `. Supported sample rates: ${rates.join(', ')}` : '') +
2096
+ '. Set { autoResample: true } on the encoder, or convert the input with an aresample/aformat filter first.');
2097
+ }
2098
+ this.codecContext.sampleRate = targetRate;
2099
+ this.codecContext.sampleFormat = targetFmt;
2100
+ this.codecContext.channelLayout = targetLayout;
2101
+ if (needsResample) {
2102
+ const swr = new SoftwareResampleContext();
2103
+ FFmpegError.throwIfError(swr.allocSetOpts2(targetLayout, targetFmt, targetRate, inLayout, inFmt, inRate), 'Failed to configure audio resampler');
2104
+ FFmpegError.throwIfError(swr.init(), 'Failed to initialize audio resampler');
2105
+ this.audioResampler = swr;
2106
+ }
2107
+ }
2108
+ /**
2109
+ * Lazily allocate the reused resampler output frame.
2110
+ *
2111
+ * @returns The allocated output frame
2112
+ *
2113
+ * @internal
2114
+ */
2115
+ getResampleFrame() {
2116
+ if (!this.resampledFrame) {
2117
+ this.resampledFrame = new Frame();
2118
+ this.resampledFrame.alloc();
2119
+ }
2120
+ return this.resampledFrame;
2121
+ }
2122
+ /**
2123
+ * Resample an incoming audio frame to the codec's target format.
2124
+ *
2125
+ * Reuses a single output frame; `swr_convert_frame` allocates/sizes its buffer.
2126
+ * The (fixed-frame-size) audio FIFO copies the samples and re-stamps PTS, so the
2127
+ * reused frame and its carried timing are only relevant on the non-FIFO path.
2128
+ *
2129
+ * @param frame - Source audio frame
2130
+ *
2131
+ * @returns The resampled frame (owned by the encoder, reused across calls)
2132
+ *
2133
+ * @internal
2134
+ */
2135
+ resampleAudio(frame) {
2136
+ const out = this.getResampleFrame();
2137
+ out.unref();
2138
+ out.format = this.codecContext.sampleFormat;
2139
+ out.sampleRate = this.codecContext.sampleRate;
2140
+ out.channelLayout = this.codecContext.channelLayout;
2141
+ FFmpegError.throwIfError(this.audioResampler.convertFrame(out, frame), 'Failed to resample audio frame');
2142
+ out.timeBase = frame.timeBase;
2143
+ out.pts = frame.pts;
2144
+ return out;
2145
+ }
2146
+ /**
2147
+ * Drain samples buffered inside the resampler (rate-conversion delay) into the
2148
+ * encoder path. Returns the drained frame if any, else null.
2149
+ *
2150
+ * @returns The drained frame (reused), or null when the resampler is empty
2151
+ *
2152
+ * @internal
2153
+ */
2154
+ drainResampler() {
2155
+ if (!this.audioResampler) {
2156
+ return null;
2157
+ }
2158
+ const out = this.getResampleFrame();
2159
+ out.unref();
2160
+ out.format = this.codecContext.sampleFormat;
2161
+ out.sampleRate = this.codecContext.sampleRate;
2162
+ out.channelLayout = this.codecContext.channelLayout;
2163
+ const ret = this.audioResampler.convertFrame(out, null);
2164
+ if (ret < 0 || out.nbSamples <= 0) {
2165
+ return null;
2166
+ }
2167
+ return out;
2168
+ }
2169
+ /**
2170
+ * Configure the codec context's pixel format from the first video frame.
2171
+ *
2172
+ * Video encoders only accept specific pixel formats. This keeps the input format
2173
+ * when the codec accepts it; otherwise it either sets up a swscale converter to
2174
+ * the least-loss supported format (when `autoFormat`) or throws a descriptive
2175
+ * error. Hardware frames are left untouched - their format is negotiated through
2176
+ * the hardware frames context, not swscale.
2177
+ *
2178
+ * @param frame - First video frame
2179
+ *
2180
+ * @throws {Error} If the input is unsupported and `autoFormat` is disabled
2181
+ *
2182
+ * @throws {FFmpegError} If the converter fails to configure
2183
+ *
2184
+ * @internal
2185
+ */
2186
+ setupVideoFormat(frame) {
2187
+ const inFmt = frame.format;
2188
+ // Hardware frames carry a hw pixfmt negotiated via hw_frames_ctx; swscale can't
2189
+ // touch them - leave the format untouched.
2190
+ if (frame.isHwFrame()) {
2191
+ this.codecContext.pixelFormat = inFmt;
2192
+ return;
2193
+ }
2194
+ const targetFmt = pickSupportedPixelFormat(inFmt, this.codec.pixelFormats);
2195
+ const needsConversion = targetFmt !== inFmt;
2196
+ if (needsConversion && !this.autoFormat) {
2197
+ const supported = this.codec.pixelFormats;
2198
+ throw new Error(`Encoder '${this.codec.name}' does not support the input pixel format ` +
2199
+ `(${avGetPixFmtName(inFmt) ?? inFmt}). Supported: ${supported.map((f) => avGetPixFmtName(f) ?? f).join(', ')}` +
2200
+ '. Set { autoFormat: true } on the encoder, or convert the input with a scale/format filter first.');
2201
+ }
2202
+ this.codecContext.pixelFormat = targetFmt;
2203
+ // Set up a same-size swscale converter when the codec needs a different format.
2204
+ if (needsConversion) {
2205
+ this.videoTargetFormat = targetFmt;
2206
+ const sws = new SoftwareScaleContext();
2207
+ sws.getContext(frame.width, frame.height, inFmt, frame.width, frame.height, targetFmt, SWS_BILINEAR);
2208
+ FFmpegError.throwIfError(sws.initContext(), 'Failed to configure pixel-format converter');
2209
+ this.videoScaler = sws;
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Lazily allocate the reused scaler output frame.
2214
+ *
2215
+ * @returns The allocated output frame
2216
+ *
2217
+ * @internal
2218
+ */
2219
+ getScaledFrame() {
2220
+ if (!this.scaledFrame) {
2221
+ this.scaledFrame = new Frame();
2222
+ this.scaledFrame.alloc();
2223
+ }
2224
+ return this.scaledFrame;
2225
+ }
2226
+ /**
2227
+ * Convert an incoming video frame to the codec's target pixel format.
2228
+ *
2229
+ * Reuses a single output frame; `sws_scale_frame` allocates/sizes its buffer.
2230
+ * Resolution is unchanged - only the pixel format differs. Timing is carried over
2231
+ * explicitly so the encoder's PTS rescale stays correct.
2232
+ *
2233
+ * @param frame - Source video frame
2234
+ *
2235
+ * @returns The converted frame (owned by the encoder, reused across calls)
2236
+ *
2237
+ * @internal
2238
+ */
2239
+ scaleVideo(frame) {
2240
+ const out = this.getScaledFrame();
2241
+ out.unref();
2242
+ out.format = this.videoTargetFormat;
2243
+ out.width = frame.width;
2244
+ out.height = frame.height;
2245
+ FFmpegError.throwIfError(this.videoScaler.scaleFrameSync(out, frame), 'Failed to convert video frame format');
2246
+ out.timeBase = frame.timeBase;
2247
+ out.pts = frame.pts;
2248
+ out.duration = frame.duration;
2249
+ return out;
2250
+ }
1891
2251
  /**
1892
2252
  * Prepare frame for encoding.
1893
2253
  *