node-av 5.2.4 → 6.0.0-beta.10

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 (106) 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 +162 -102
  4. package/dist/api/bitstream-filter.js.map +1 -1
  5. package/dist/api/decoder.d.ts +87 -8
  6. package/dist/api/decoder.js +179 -4
  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 +133 -7
  16. package/dist/api/encoder.js +392 -68
  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 +13 -8
  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.js +1 -1
  41. package/dist/api/rtp-stream.js.map +1 -1
  42. package/dist/api/scaler.d.ts +431 -0
  43. package/dist/api/scaler.js +620 -0
  44. package/dist/api/scaler.js.map +1 -0
  45. package/dist/api/utilities/async-queue.d.ts +27 -1
  46. package/dist/api/utilities/async-queue.js +38 -1
  47. package/dist/api/utilities/async-queue.js.map +1 -1
  48. package/dist/api/utilities/electron-shared-texture.d.ts +41 -1
  49. package/dist/api/utilities/electron-shared-texture.js +41 -4
  50. package/dist/api/utilities/electron-shared-texture.js.map +1 -1
  51. package/dist/api/utilities/index.d.ts +1 -1
  52. package/dist/api/utilities/index.js.map +1 -1
  53. package/dist/api/webrtc-stream.d.ts +0 -1
  54. package/dist/api/webrtc-stream.js +0 -1
  55. package/dist/api/webrtc-stream.js.map +1 -1
  56. package/dist/constants/bsf-options.d.ts +333 -0
  57. package/dist/constants/bsf-options.js +7 -0
  58. package/dist/constants/bsf-options.js.map +1 -0
  59. package/dist/constants/constants.d.ts +109 -0
  60. package/dist/constants/constants.js +110 -0
  61. package/dist/constants/constants.js.map +1 -1
  62. package/dist/constants/decoders.d.ts +636 -618
  63. package/dist/constants/decoders.js +1 -3
  64. package/dist/constants/decoders.js.map +1 -1
  65. package/dist/constants/encoders.d.ts +300 -282
  66. package/dist/constants/encoders.js +0 -2
  67. package/dist/constants/encoders.js.map +1 -1
  68. package/dist/constants/filter-options.d.ts +10915 -0
  69. package/dist/constants/filter-options.js +7 -0
  70. package/dist/constants/filter-options.js.map +1 -0
  71. package/dist/constants/format-options.d.ts +3056 -0
  72. package/dist/constants/format-options.js +7 -0
  73. package/dist/constants/format-options.js.map +1 -0
  74. package/dist/constants/formats.d.ts +18 -0
  75. package/dist/constants/formats.js +7 -0
  76. package/dist/constants/formats.js.map +1 -0
  77. package/dist/constants/index.d.ts +5 -0
  78. package/dist/constants/options.d.ts +4073 -0
  79. package/dist/constants/options.js +7 -0
  80. package/dist/constants/options.js.map +1 -0
  81. package/dist/lib/binding.d.ts +4 -1
  82. package/dist/lib/binding.js.map +1 -1
  83. package/dist/lib/codec.d.ts +36 -5
  84. package/dist/lib/codec.js +37 -4
  85. package/dist/lib/codec.js.map +1 -1
  86. package/dist/lib/dictionary.d.ts +1 -1
  87. package/dist/lib/dictionary.js.map +1 -1
  88. package/dist/lib/error.d.ts +69 -0
  89. package/dist/lib/error.js +92 -0
  90. package/dist/lib/error.js.map +1 -1
  91. package/dist/lib/frame.d.ts +46 -3
  92. package/dist/lib/frame.js +50 -3
  93. package/dist/lib/frame.js.map +1 -1
  94. package/dist/lib/index.d.ts +1 -1
  95. package/dist/lib/index.js.map +1 -1
  96. package/dist/lib/native-types.d.ts +68 -0
  97. package/dist/lib/packet.d.ts +17 -3
  98. package/dist/lib/packet.js +19 -3
  99. package/dist/lib/packet.js.map +1 -1
  100. package/dist/lib/utilities.d.ts +21 -0
  101. package/dist/lib/utilities.js +23 -0
  102. package/dist/lib/utilities.js.map +1 -1
  103. package/dist/webrtc/index.d.ts +3 -0
  104. package/dist/webrtc/index.js +7 -0
  105. package/dist/webrtc/index.js.map +1 -0
  106. package/package.json +32 -21
@@ -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, } 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,12 +59,67 @@ 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 { avChannelLayoutDefault, avGetSampleFmtName, avRescaleQ } from '../lib/utilities.js';
62
64
  import { AudioFrameBuffer } from './audio-frame-buffer.js';
63
65
  import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
64
66
  import { AsyncQueue } from './utilities/async-queue.js';
65
67
  import { SchedulerControl } from './utilities/scheduler.js';
66
68
  import { parseBitrate } from './utils.js';
69
+ /**
70
+ * Pick a codec-supported sample rate, keeping the input rate when accepted and
71
+ * otherwise choosing the numerically nearest supported one.
72
+ *
73
+ * @param rate - Input sample rate in Hz
74
+ *
75
+ * @param supported - Codec's supported sample rates, or null when unrestricted
76
+ *
77
+ * @returns A sample rate the codec accepts
78
+ *
79
+ * @internal
80
+ */
81
+ function pickSupportedRate(rate, supported) {
82
+ if (!supported || supported.length === 0 || supported.includes(rate)) {
83
+ return rate;
84
+ }
85
+ return supported.reduce((best, r) => (Math.abs(r - rate) < Math.abs(best - rate) ? r : best), supported[0]);
86
+ }
87
+ /**
88
+ * Pick a codec-supported sample format, keeping the input when accepted and
89
+ * otherwise the codec's preferred (first) format.
90
+ *
91
+ * @param fmt - Input sample format
92
+ *
93
+ * @param supported - Codec's supported sample formats, or null when unrestricted
94
+ *
95
+ * @returns A sample format the codec accepts
96
+ *
97
+ * @internal
98
+ */
99
+ function pickSupportedFormat(fmt, supported) {
100
+ if (!supported || supported.length === 0 || supported.includes(fmt)) {
101
+ return fmt;
102
+ }
103
+ return supported[0];
104
+ }
105
+ /**
106
+ * Pick a codec-supported channel layout. Keeps the input layout when the codec
107
+ * accepts its channel count, otherwise the codec's first supported layout.
108
+ *
109
+ * @param layout - Input channel layout
110
+ *
111
+ * @param supported - Codec's supported channel layouts, or null when unrestricted
112
+ *
113
+ * @returns A channel layout the codec accepts
114
+ *
115
+ * @internal
116
+ */
117
+ function pickSupportedLayout(layout, supported) {
118
+ if (!supported || supported.length === 0 || supported.some((l) => l.nbChannels === layout.nbChannels)) {
119
+ return layout;
120
+ }
121
+ return supported[0];
122
+ }
67
123
  /**
68
124
  * High-level encoder for audio and video streams.
69
125
  *
@@ -132,6 +188,10 @@ export class Encoder {
132
188
  opts;
133
189
  options;
134
190
  audioFrameBuffer;
191
+ autoResample;
192
+ audioResampler;
193
+ resampledFrame;
194
+ audioInputLayout;
135
195
  // Worker pattern for push-based processing
136
196
  inputQueue;
137
197
  outputQueue;
@@ -154,10 +214,11 @@ export class Encoder {
154
214
  this.codec = codec;
155
215
  this.options = options;
156
216
  this.opts = opts;
217
+ this.autoResample = options.autoResample ?? false;
157
218
  this.packet = new Packet();
158
219
  this.packet.alloc();
159
- this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
160
- this.outputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE);
220
+ this.inputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE, (f) => f.free());
221
+ this.outputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE, (p) => p.free());
161
222
  }
162
223
  /**
163
224
  * Create an encoder with specified codec and options.
@@ -262,8 +323,11 @@ export class Encoder {
262
323
  if (options.threadType !== undefined) {
263
324
  codecContext.threadType = options.threadType;
264
325
  }
265
- const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
266
- const encoder = new Encoder(codecContext, codec, options, opts);
326
+ // Loose view for internal use: the public signature narrows `options` to the
327
+ // codec, but internally codec options are handled as a generic dictionary.
328
+ const looseOptions = options;
329
+ const opts = looseOptions.options ? Dictionary.fromObject(looseOptions.options) : undefined;
330
+ const encoder = new Encoder(codecContext, codec, looseOptions, opts);
267
331
  if (options.signal) {
268
332
  options.signal.throwIfAborted();
269
333
  encoder.signal = options.signal;
@@ -375,14 +439,125 @@ export class Encoder {
375
439
  if (options.threadType !== undefined) {
376
440
  codecContext.threadType = options.threadType;
377
441
  }
378
- const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
379
- const encoder = new Encoder(codecContext, codec, options, opts);
442
+ // Loose view for internal use: the public signature narrows `options` to the
443
+ // codec, but internally codec options are handled as a generic dictionary.
444
+ const looseOptions = options;
445
+ const opts = looseOptions.options ? Dictionary.fromObject(looseOptions.options) : undefined;
446
+ const encoder = new Encoder(codecContext, codec, looseOptions, opts);
380
447
  if (options.signal) {
381
448
  options.signal.throwIfAborted();
382
449
  encoder.signal = options.signal;
383
450
  }
384
451
  return encoder;
385
452
  }
453
+ /**
454
+ * Encode a single frame into a self-contained image buffer.
455
+ *
456
+ * One-shot, stateless helper for intra-only image codecs (MJPEG, PNG, WebP, ...).
457
+ * Creates a fresh encoder, encodes the frame, flushes and frees everything in one call.
458
+ * The encoder adopts dimensions, pixel format and hardware context from the frame,
459
+ * so any frame size works without reconfiguration.
460
+ *
461
+ * @param encoderCodec - Encoder codec (name, ID, branded constant, or Codec)
462
+ *
463
+ * @param frame - Frame to encode
464
+ *
465
+ * @param options - Optional encoder configuration (e.g. `{ options: { q: 3 } }` for MJPEG quality)
466
+ *
467
+ * @returns Encoded image bytes
468
+ *
469
+ * @throws {FFmpegError} If the encoder is not found or encoding fails
470
+ *
471
+ * @throws {Error} If the encoder produced no output
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * const jpeg = await Encoder.encodeOne(FF_ENCODER_MJPEG, frame, { options: { q: 3 } });
476
+ * ```
477
+ *
478
+ * @see {@link EncoderPool} For reusing encoders across recurring resolutions
479
+ */
480
+ static async encodeOne(encoderCodec, frame, options = {}) {
481
+ const env_1 = { stack: [], error: void 0, hasError: false };
482
+ try {
483
+ const encoder = __addDisposableResource(env_1, await Encoder.create(encoderCodec, options), false);
484
+ const packets = [...(await encoder.encodeAll(frame)), ...(await encoder.encodeAll(null))];
485
+ try {
486
+ const data = packets[0]?.data;
487
+ if (!data) {
488
+ throw new Error(`Encoder '${encoder.getCodec().name}' produced no output for frame`);
489
+ }
490
+ return data;
491
+ }
492
+ finally {
493
+ for (const packet of packets) {
494
+ packet.free();
495
+ }
496
+ }
497
+ }
498
+ catch (e_1) {
499
+ env_1.error = e_1;
500
+ env_1.hasError = true;
501
+ }
502
+ finally {
503
+ __disposeResources(env_1);
504
+ }
505
+ }
506
+ /**
507
+ * Encode a single frame into a self-contained image buffer synchronously.
508
+ * Synchronous version of encodeOne.
509
+ *
510
+ * One-shot, stateless helper for intra-only image codecs (MJPEG, PNG, WebP, ...).
511
+ * Creates a fresh encoder, encodes the frame, flushes and frees everything in one call.
512
+ * The encoder adopts dimensions, pixel format and hardware context from the frame,
513
+ * so any frame size works without reconfiguration.
514
+ *
515
+ * @param encoderCodec - Encoder codec (name, ID, branded constant, or Codec)
516
+ *
517
+ * @param frame - Frame to encode
518
+ *
519
+ * @param options - Optional encoder configuration (e.g. `{ options: { q: 3 } }` for MJPEG quality)
520
+ *
521
+ * @returns Encoded image bytes
522
+ *
523
+ * @throws {FFmpegError} If the encoder is not found or encoding fails
524
+ *
525
+ * @throws {Error} If the encoder produced no output
526
+ *
527
+ * @example
528
+ * ```typescript
529
+ * const jpeg = Encoder.encodeOneSync(FF_ENCODER_MJPEG, frame, { options: { q: 3 } });
530
+ * ```
531
+ *
532
+ * @see {@link encodeOne} For async version
533
+ * @see {@link EncoderPool} For reusing encoders across recurring resolutions
534
+ */
535
+ static encodeOneSync(encoderCodec, frame, options = {}) {
536
+ const env_2 = { stack: [], error: void 0, hasError: false };
537
+ try {
538
+ const encoder = __addDisposableResource(env_2, Encoder.createSync(encoderCodec, options), false);
539
+ const packets = [...encoder.encodeAllSync(frame), ...encoder.encodeAllSync(null)];
540
+ try {
541
+ const data = packets[0]?.data;
542
+ if (!data) {
543
+ throw new Error(`Encoder '${encoder.getCodec().name}' produced no output for frame`);
544
+ }
545
+ return data;
546
+ }
547
+ finally {
548
+ for (const packet of packets) {
549
+ packet.free();
550
+ }
551
+ }
552
+ }
553
+ catch (e_2) {
554
+ env_2.error = e_2;
555
+ env_2.hasError = true;
556
+ }
557
+ finally {
558
+ __disposeResources(env_2);
559
+ }
560
+ }
386
561
  /**
387
562
  * Check if encoder is open.
388
563
  *
@@ -622,8 +797,15 @@ export class Encoder {
622
797
  // Open encoder if not already done
623
798
  this.initializePromise ??= this.initialize(frame);
624
799
  await this.initializePromise;
800
+ // Give an unspecified-layout frame the concrete native layout the codec was
801
+ // opened with (and the resampler configured for), so both accept it.
802
+ if (this.audioInputLayout) {
803
+ frame.channelLayout = this.audioInputLayout;
804
+ }
805
+ // Resample audio to the codec's format first
806
+ const input = this.audioResampler ? this.resampleAudio(frame) : frame;
625
807
  // Prepare frame for encoding (set quality, validate channel count)
626
- this.prepareFrameForEncoding(frame);
808
+ this.prepareFrameForEncoding(input);
627
809
  const encode = async (newFrame) => {
628
810
  const sendRet = await this.codecContext.sendFrame(newFrame);
629
811
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -633,10 +815,10 @@ export class Encoder {
633
815
  };
634
816
  if (this.audioFrameBuffer) {
635
817
  // Push frame into buffer - actual sending happens in receive()
636
- await this.audioFrameBuffer.push(frame);
818
+ await this.audioFrameBuffer.push(input);
637
819
  }
638
820
  else {
639
- await encode(frame);
821
+ await encode(input);
640
822
  }
641
823
  }
642
824
  /**
@@ -690,8 +872,15 @@ export class Encoder {
690
872
  if (!this.initialized) {
691
873
  this.initializeSync(frame);
692
874
  }
875
+ // Give an unspecified-layout frame the concrete native layout the codec was
876
+ // opened with (and the resampler configured for), so both accept it.
877
+ if (this.audioInputLayout) {
878
+ frame.channelLayout = this.audioInputLayout;
879
+ }
880
+ // Resample audio to the codec's format first
881
+ const input = this.audioResampler ? this.resampleAudio(frame) : frame;
693
882
  // Prepare frame for encoding (set quality, validate channel count)
694
- this.prepareFrameForEncoding(frame);
883
+ this.prepareFrameForEncoding(input);
695
884
  const encode = (newFrame) => {
696
885
  const sendRet = this.codecContext.sendFrameSync(newFrame);
697
886
  if (sendRet < 0 && sendRet !== AVERROR_EOF) {
@@ -701,10 +890,10 @@ export class Encoder {
701
890
  };
702
891
  if (this.audioFrameBuffer) {
703
892
  // Push frame into buffer - actual sending happens in receiveSync()
704
- this.audioFrameBuffer.pushSync(frame);
893
+ this.audioFrameBuffer.pushSync(input);
705
894
  }
706
895
  else {
707
- encode(frame);
896
+ encode(input);
708
897
  }
709
898
  }
710
899
  /**
@@ -906,9 +1095,9 @@ export class Encoder {
906
1095
  return;
907
1096
  }
908
1097
  for await (const frame_1 of frames) {
909
- const env_1 = { stack: [], error: void 0, hasError: false };
1098
+ const env_3 = { stack: [], error: void 0, hasError: false };
910
1099
  try {
911
- const frame = __addDisposableResource(env_1, frame_1, false);
1100
+ const frame = __addDisposableResource(env_3, frame_1, false);
912
1101
  this.signal?.throwIfAborted();
913
1102
  if (frame === null) {
914
1103
  yield* finalize();
@@ -916,12 +1105,12 @@ export class Encoder {
916
1105
  }
917
1106
  yield* processFrame(frame);
918
1107
  }
919
- catch (e_1) {
920
- env_1.error = e_1;
921
- env_1.hasError = true;
1108
+ catch (e_3) {
1109
+ env_3.error = e_3;
1110
+ env_3.hasError = true;
922
1111
  }
923
1112
  finally {
924
- __disposeResources(env_1);
1113
+ __disposeResources(env_3);
925
1114
  }
926
1115
  }
927
1116
  }
@@ -1017,9 +1206,9 @@ export class Encoder {
1017
1206
  }
1018
1207
  // Case 3: Iterable of frames
1019
1208
  for (const frame_2 of frames) {
1020
- const env_2 = { stack: [], error: void 0, hasError: false };
1209
+ const env_4 = { stack: [], error: void 0, hasError: false };
1021
1210
  try {
1022
- const frame = __addDisposableResource(env_2, frame_2, false);
1211
+ const frame = __addDisposableResource(env_4, frame_2, false);
1023
1212
  // Check for EOF signal from upstream
1024
1213
  if (frame === null) {
1025
1214
  yield* finalize();
@@ -1027,12 +1216,12 @@ export class Encoder {
1027
1216
  }
1028
1217
  yield* processFrame(frame);
1029
1218
  }
1030
- catch (e_2) {
1031
- env_2.error = e_2;
1032
- env_2.hasError = true;
1219
+ catch (e_4) {
1220
+ env_4.error = e_4;
1221
+ env_4.hasError = true;
1033
1222
  }
1034
1223
  finally {
1035
- __disposeResources(env_2);
1224
+ __disposeResources(env_4);
1036
1225
  }
1037
1226
  }
1038
1227
  // No fallback flush - only flush on explicit EOF
@@ -1069,23 +1258,33 @@ export class Encoder {
1069
1258
  if (this.isClosed || !this.initialized) {
1070
1259
  return;
1071
1260
  }
1261
+ // Drain samples buffered inside the resampler into the FIFO/encoder first.
1262
+ const drained = this.drainResampler();
1263
+ if (drained) {
1264
+ if (this.audioFrameBuffer) {
1265
+ await this.audioFrameBuffer.push(drained);
1266
+ }
1267
+ else {
1268
+ await this.codecContext.sendFrame(drained);
1269
+ }
1270
+ }
1072
1271
  // If using AudioFrameBuffer, flush remaining buffered samples first
1073
1272
  if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1074
1273
  // Pull any remaining partial frame (may be less than frameSize)
1075
1274
  // For the final frame, we pad or truncate as needed
1076
1275
  let _bufferedFrame;
1077
1276
  while (!this.isClosed && (_bufferedFrame = await this.audioFrameBuffer.pull()) !== null) {
1078
- const env_3 = { stack: [], error: void 0, hasError: false };
1277
+ const env_5 = { stack: [], error: void 0, hasError: false };
1079
1278
  try {
1080
- const bufferedFrame = __addDisposableResource(env_3, _bufferedFrame, false);
1279
+ const bufferedFrame = __addDisposableResource(env_5, _bufferedFrame, false);
1081
1280
  await this.codecContext.sendFrame(bufferedFrame);
1082
1281
  }
1083
- catch (e_3) {
1084
- env_3.error = e_3;
1085
- env_3.hasError = true;
1282
+ catch (e_5) {
1283
+ env_5.error = e_5;
1284
+ env_5.hasError = true;
1086
1285
  }
1087
1286
  finally {
1088
- __disposeResources(env_3);
1287
+ __disposeResources(env_5);
1089
1288
  }
1090
1289
  }
1091
1290
  }
@@ -1129,23 +1328,33 @@ export class Encoder {
1129
1328
  if (this.isClosed || !this.initialized) {
1130
1329
  return;
1131
1330
  }
1331
+ // Drain samples buffered inside the resampler into the FIFO/encoder first.
1332
+ const drained = this.drainResampler();
1333
+ if (drained) {
1334
+ if (this.audioFrameBuffer) {
1335
+ this.audioFrameBuffer.pushSync(drained);
1336
+ }
1337
+ else {
1338
+ this.codecContext.sendFrameSync(drained);
1339
+ }
1340
+ }
1132
1341
  // If using AudioFrameBuffer, flush remaining buffered samples first
1133
1342
  if (this.audioFrameBuffer && this.audioFrameBuffer.size > 0) {
1134
1343
  // Pull any remaining partial frame (may be less than frameSize)
1135
1344
  // For the final frame, we pad or truncate as needed
1136
1345
  let _bufferedFrame;
1137
1346
  while (!this.isClosed && (_bufferedFrame = this.audioFrameBuffer.pullSync()) !== null) {
1138
- const env_4 = { stack: [], error: void 0, hasError: false };
1347
+ const env_6 = { stack: [], error: void 0, hasError: false };
1139
1348
  try {
1140
- const bufferedFrame = __addDisposableResource(env_4, _bufferedFrame, false);
1349
+ const bufferedFrame = __addDisposableResource(env_6, _bufferedFrame, false);
1141
1350
  this.codecContext.sendFrameSync(bufferedFrame);
1142
1351
  }
1143
- catch (e_4) {
1144
- env_4.error = e_4;
1145
- env_4.hasError = true;
1352
+ catch (e_6) {
1353
+ env_6.error = e_6;
1354
+ env_6.hasError = true;
1146
1355
  }
1147
1356
  finally {
1148
- __disposeResources(env_4);
1357
+ __disposeResources(env_6);
1149
1358
  }
1150
1359
  }
1151
1360
  }
@@ -1288,19 +1497,19 @@ export class Encoder {
1288
1497
  // Clear previous packet data
1289
1498
  this.packet.unref();
1290
1499
  if (this.audioFrameBuffer?.hasFrame()) {
1291
- const env_5 = { stack: [], error: void 0, hasError: false };
1500
+ const env_7 = { stack: [], error: void 0, hasError: false };
1292
1501
  try {
1293
- const bufferedFrame = __addDisposableResource(env_5, await this.audioFrameBuffer.pull(), false);
1502
+ const bufferedFrame = __addDisposableResource(env_7, await this.audioFrameBuffer.pull(), false);
1294
1503
  if (bufferedFrame) {
1295
1504
  await this.codecContext.sendFrame(bufferedFrame);
1296
1505
  }
1297
1506
  }
1298
- catch (e_5) {
1299
- env_5.error = e_5;
1300
- env_5.hasError = true;
1507
+ catch (e_7) {
1508
+ env_7.error = e_7;
1509
+ env_7.hasError = true;
1301
1510
  }
1302
1511
  finally {
1303
- __disposeResources(env_5);
1512
+ __disposeResources(env_7);
1304
1513
  }
1305
1514
  }
1306
1515
  const ret = await this.codecContext.receivePacket(this.packet);
@@ -1393,19 +1602,19 @@ export class Encoder {
1393
1602
  // Clear previous packet data
1394
1603
  this.packet.unref();
1395
1604
  if (this.audioFrameBuffer?.hasFrame()) {
1396
- const env_6 = { stack: [], error: void 0, hasError: false };
1605
+ const env_8 = { stack: [], error: void 0, hasError: false };
1397
1606
  try {
1398
- const bufferedFrame = __addDisposableResource(env_6, this.audioFrameBuffer.pullSync(), false);
1607
+ const bufferedFrame = __addDisposableResource(env_8, this.audioFrameBuffer.pullSync(), false);
1399
1608
  if (bufferedFrame) {
1400
1609
  this.codecContext.sendFrameSync(bufferedFrame);
1401
1610
  }
1402
1611
  }
1403
- catch (e_6) {
1404
- env_6.error = e_6;
1405
- env_6.hasError = true;
1612
+ catch (e_8) {
1613
+ env_8.error = e_8;
1614
+ env_8.hasError = true;
1406
1615
  }
1407
1616
  finally {
1408
- __disposeResources(env_6);
1617
+ __disposeResources(env_8);
1409
1618
  }
1410
1619
  }
1411
1620
  const ret = this.codecContext.receivePacketSync(this.packet);
@@ -1491,8 +1700,20 @@ export class Encoder {
1491
1700
  // Close queues
1492
1701
  this.inputQueue.close();
1493
1702
  this.outputQueue.close();
1703
+ // Free any frames/packets left buffered on an aborted/early-closed pipeline.
1704
+ this.inputQueue.clear();
1705
+ this.outputQueue.clear();
1494
1706
  this.packet.free();
1495
1707
  this.codecContext.freeContext();
1708
+ // Release the audio frame buffer (owns a native Frame + AudioFifo) used by
1709
+ // fixed-frame-size audio encoders.
1710
+ this.audioFrameBuffer?.[Symbol.dispose]();
1711
+ this.audioFrameBuffer = undefined;
1712
+ // Release the audio resampler and its reused output frame.
1713
+ this.audioResampler?.[Symbol.dispose]();
1714
+ this.audioResampler = undefined;
1715
+ this.resampledFrame?.free();
1716
+ this.resampledFrame = undefined;
1496
1717
  this.initialized = false;
1497
1718
  }
1498
1719
  /**
@@ -1535,9 +1756,9 @@ export class Encoder {
1535
1756
  try {
1536
1757
  // Outer loop - receive frames
1537
1758
  while (!this.inputQueue.isClosed) {
1538
- const env_7 = { stack: [], error: void 0, hasError: false };
1759
+ const env_9 = { stack: [], error: void 0, hasError: false };
1539
1760
  try {
1540
- const frame = __addDisposableResource(env_7, await this.inputQueue.receive(), false);
1761
+ const frame = __addDisposableResource(env_9, await this.inputQueue.receive(), false);
1541
1762
  if (!frame)
1542
1763
  break;
1543
1764
  // Open encoder if not already done
@@ -1556,12 +1777,12 @@ export class Encoder {
1556
1777
  await this.outputQueue.send(packet); // Only send actual packets
1557
1778
  }
1558
1779
  }
1559
- catch (e_7) {
1560
- env_7.error = e_7;
1561
- env_7.hasError = true;
1780
+ catch (e_9) {
1781
+ env_9.error = e_9;
1782
+ env_9.hasError = true;
1562
1783
  }
1563
1784
  finally {
1564
- __disposeResources(env_7);
1785
+ __disposeResources(env_9);
1565
1786
  }
1566
1787
  }
1567
1788
  // Flush encoder at end
@@ -1697,12 +1918,8 @@ export class Encoder {
1697
1918
  }
1698
1919
  }
1699
1920
  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;
1921
+ // Audio: pick codec-supported sample rate/format/layout (resampling on demand).
1922
+ this.setupAudioParams(frame);
1706
1923
  }
1707
1924
  // Setup hardware acceleration with validation
1708
1925
  this.setupHardwareAcceleration(frame);
@@ -1790,12 +2007,8 @@ export class Encoder {
1790
2007
  }
1791
2008
  }
1792
2009
  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;
2010
+ // Audio: pick codec-supported sample rate/format/layout (resampling on demand).
2011
+ this.setupAudioParams(frame);
1799
2012
  }
1800
2013
  // Setup hardware acceleration with validation
1801
2014
  this.setupHardwareAcceleration(frame);
@@ -1888,6 +2101,117 @@ export class Encoder {
1888
2101
  }
1889
2102
  }
1890
2103
  }
2104
+ /**
2105
+ * Configure the codec context's audio parameters from the first frame.
2106
+ *
2107
+ * Audio encoders only accept specific sample rates / sample formats / channel
2108
+ * layouts. This picks codec-supported targets; if they differ from the input it
2109
+ * either sets up a resampler (when `autoResample`) or throws a descriptive error.
2110
+ *
2111
+ * @param frame - First audio frame
2112
+ *
2113
+ * @throws {Error} If the input is unsupported and `autoResample` is disabled
2114
+ *
2115
+ * @throws {FFmpegError} If the resampler fails to configure
2116
+ *
2117
+ * @internal
2118
+ */
2119
+ setupAudioParams(frame) {
2120
+ // Always use frame timebase (typically 1/sample_rate) for correct audio PTS.
2121
+ this.codecContext.timeBase = frame.timeBase;
2122
+ const inRate = frame.sampleRate;
2123
+ const inFmt = frame.format;
2124
+ // Codec open and swr both need a concrete layout. PCM/raw frames often carry
2125
+ // an unspecified layout (order UNSPEC, mask 0); normalize it to the canonical
2126
+ // native layout and re-apply it to each incoming frame (see encode()) so it
2127
+ // matches the opened codec context / resampler input.
2128
+ let inLayout = frame.channelLayout;
2129
+ if (inLayout.order === AV_CHANNEL_ORDER_UNSPEC) {
2130
+ inLayout = avChannelLayoutDefault(inLayout.nbChannels);
2131
+ this.audioInputLayout = inLayout;
2132
+ }
2133
+ const targetRate = pickSupportedRate(inRate, this.codec.supportedSamplerates);
2134
+ const targetFmt = pickSupportedFormat(inFmt, this.codec.sampleFormats);
2135
+ const targetLayout = pickSupportedLayout(inLayout, this.codec.channelLayouts);
2136
+ const needsResample = targetRate !== inRate || targetFmt !== inFmt || targetLayout.nbChannels !== inLayout.nbChannels;
2137
+ if (needsResample && !this.autoResample) {
2138
+ const rates = this.codec.supportedSamplerates;
2139
+ throw new Error(`Encoder '${this.codec.name}' does not support the input audio format ` +
2140
+ `(${inRate} Hz, ${avGetSampleFmtName(inFmt) ?? inFmt}, ${inLayout.nbChannels}ch)` +
2141
+ (rates && rates.length > 0 ? `. Supported sample rates: ${rates.join(', ')}` : '') +
2142
+ '. Set { autoResample: true } on the encoder, or convert the input with an aresample/aformat filter first.');
2143
+ }
2144
+ this.codecContext.sampleRate = targetRate;
2145
+ this.codecContext.sampleFormat = targetFmt;
2146
+ this.codecContext.channelLayout = targetLayout;
2147
+ if (needsResample) {
2148
+ const swr = new SoftwareResampleContext();
2149
+ FFmpegError.throwIfError(swr.allocSetOpts2(targetLayout, targetFmt, targetRate, inLayout, inFmt, inRate), 'Failed to configure audio resampler');
2150
+ FFmpegError.throwIfError(swr.init(), 'Failed to initialize audio resampler');
2151
+ this.audioResampler = swr;
2152
+ }
2153
+ }
2154
+ /**
2155
+ * Lazily allocate the reused resampler output frame.
2156
+ *
2157
+ * @returns The allocated output frame
2158
+ *
2159
+ * @internal
2160
+ */
2161
+ getResampleFrame() {
2162
+ if (!this.resampledFrame) {
2163
+ this.resampledFrame = new Frame();
2164
+ this.resampledFrame.alloc();
2165
+ }
2166
+ return this.resampledFrame;
2167
+ }
2168
+ /**
2169
+ * Resample an incoming audio frame to the codec's target format.
2170
+ *
2171
+ * Reuses a single output frame; `swr_convert_frame` allocates/sizes its buffer.
2172
+ * The (fixed-frame-size) audio FIFO copies the samples and re-stamps PTS, so the
2173
+ * reused frame and its carried timing are only relevant on the non-FIFO path.
2174
+ *
2175
+ * @param frame - Source audio frame
2176
+ *
2177
+ * @returns The resampled frame (owned by the encoder, reused across calls)
2178
+ *
2179
+ * @internal
2180
+ */
2181
+ resampleAudio(frame) {
2182
+ const out = this.getResampleFrame();
2183
+ out.unref();
2184
+ out.format = this.codecContext.sampleFormat;
2185
+ out.sampleRate = this.codecContext.sampleRate;
2186
+ out.channelLayout = this.codecContext.channelLayout;
2187
+ FFmpegError.throwIfError(this.audioResampler.convertFrame(out, frame), 'Failed to resample audio frame');
2188
+ out.timeBase = frame.timeBase;
2189
+ out.pts = frame.pts;
2190
+ return out;
2191
+ }
2192
+ /**
2193
+ * Drain samples buffered inside the resampler (rate-conversion delay) into the
2194
+ * encoder path. Returns the drained frame if any, else null.
2195
+ *
2196
+ * @returns The drained frame (reused), or null when the resampler is empty
2197
+ *
2198
+ * @internal
2199
+ */
2200
+ drainResampler() {
2201
+ if (!this.audioResampler) {
2202
+ return null;
2203
+ }
2204
+ const out = this.getResampleFrame();
2205
+ out.unref();
2206
+ out.format = this.codecContext.sampleFormat;
2207
+ out.sampleRate = this.codecContext.sampleRate;
2208
+ out.channelLayout = this.codecContext.channelLayout;
2209
+ const ret = this.audioResampler.convertFrame(out, null);
2210
+ if (ret < 0 || out.nbSamples <= 0) {
2211
+ return null;
2212
+ }
2213
+ return out;
2214
+ }
1891
2215
  /**
1892
2216
  * Prepare frame for encoding.
1893
2217
  *