node-av 5.2.4-beta.1 → 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.
- package/README.md +15 -1
- package/dist/api/bitstream-filter.d.ts +110 -87
- package/dist/api/bitstream-filter.js +162 -102
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +87 -8
- package/dist/api/decoder.js +179 -4
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/demuxer.d.ts +30 -24
- package/dist/api/demuxer.js +4 -5
- package/dist/api/demuxer.js.map +1 -1
- package/dist/api/device.js.map +1 -1
- package/dist/api/encoder-pool.d.ts +220 -0
- package/dist/api/encoder-pool.js +285 -0
- package/dist/api/encoder-pool.js.map +1 -0
- package/dist/api/encoder.d.ts +133 -7
- package/dist/api/encoder.js +392 -68
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-complex.d.ts +2 -1
- package/dist/api/filter-complex.js +11 -7
- package/dist/api/filter-complex.js.map +1 -1
- package/dist/api/filter-presets.d.ts +130 -654
- package/dist/api/filter-presets.js +180 -858
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.js +13 -8
- package/dist/api/filter.js.map +1 -1
- package/dist/api/fmp4-stream.js +1 -1
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/index.d.ts +6 -6
- package/dist/api/index.js +8 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/muxer.d.ts +43 -15
- package/dist/api/muxer.js +79 -27
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/pipeline.d.ts +50 -0
- package/dist/api/pipeline.js +138 -22
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/probe.d.ts +128 -0
- package/dist/api/probe.js +227 -0
- package/dist/api/probe.js.map +1 -0
- package/dist/api/rtp-stream.js +1 -1
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +431 -0
- package/dist/api/scaler.js +620 -0
- package/dist/api/scaler.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +27 -1
- package/dist/api/utilities/async-queue.js +38 -1
- package/dist/api/utilities/async-queue.js.map +1 -1
- package/dist/api/utilities/electron-shared-texture.d.ts +41 -1
- package/dist/api/utilities/electron-shared-texture.js +41 -4
- package/dist/api/utilities/electron-shared-texture.js.map +1 -1
- package/dist/api/utilities/index.d.ts +1 -1
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/webrtc-stream.d.ts +0 -1
- package/dist/api/webrtc-stream.js +0 -1
- package/dist/api/webrtc-stream.js.map +1 -1
- package/dist/constants/bsf-options.d.ts +333 -0
- package/dist/constants/bsf-options.js +7 -0
- package/dist/constants/bsf-options.js.map +1 -0
- package/dist/constants/constants.d.ts +109 -0
- package/dist/constants/constants.js +110 -0
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/decoders.d.ts +636 -618
- package/dist/constants/decoders.js +1 -3
- package/dist/constants/decoders.js.map +1 -1
- package/dist/constants/encoders.d.ts +300 -282
- package/dist/constants/encoders.js +0 -2
- package/dist/constants/encoders.js.map +1 -1
- package/dist/constants/filter-options.d.ts +10915 -0
- package/dist/constants/filter-options.js +7 -0
- package/dist/constants/filter-options.js.map +1 -0
- package/dist/constants/format-options.d.ts +3056 -0
- package/dist/constants/format-options.js +7 -0
- package/dist/constants/format-options.js.map +1 -0
- package/dist/constants/formats.d.ts +18 -0
- package/dist/constants/formats.js +7 -0
- package/dist/constants/formats.js.map +1 -0
- package/dist/constants/index.d.ts +5 -0
- package/dist/constants/options.d.ts +4073 -0
- package/dist/constants/options.js +7 -0
- package/dist/constants/options.js.map +1 -0
- package/dist/lib/binding.d.ts +4 -1
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/codec.d.ts +36 -5
- package/dist/lib/codec.js +37 -4
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.d.ts +1 -1
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.d.ts +69 -0
- package/dist/lib/error.js +92 -0
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/frame.d.ts +46 -3
- package/dist/lib/frame.js +50 -3
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/native-types.d.ts +68 -0
- package/dist/lib/packet.d.ts +17 -3
- package/dist/lib/packet.js +19 -3
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/utilities.d.ts +21 -0
- package/dist/lib/utilities.js +23 -0
- package/dist/lib/utilities.js.map +1 -1
- package/dist/webrtc/index.d.ts +3 -0
- package/dist/webrtc/index.js +7 -0
- package/dist/webrtc/index.js.map +1 -0
- package/package.json +33 -22
package/dist/api/encoder.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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(
|
|
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(
|
|
818
|
+
await this.audioFrameBuffer.push(input);
|
|
637
819
|
}
|
|
638
820
|
else {
|
|
639
|
-
await encode(
|
|
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(
|
|
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(
|
|
893
|
+
this.audioFrameBuffer.pushSync(input);
|
|
705
894
|
}
|
|
706
895
|
else {
|
|
707
|
-
encode(
|
|
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
|
|
1098
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
910
1099
|
try {
|
|
911
|
-
const frame = __addDisposableResource(
|
|
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 (
|
|
920
|
-
|
|
921
|
-
|
|
1108
|
+
catch (e_3) {
|
|
1109
|
+
env_3.error = e_3;
|
|
1110
|
+
env_3.hasError = true;
|
|
922
1111
|
}
|
|
923
1112
|
finally {
|
|
924
|
-
__disposeResources(
|
|
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
|
|
1209
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
1021
1210
|
try {
|
|
1022
|
-
const frame = __addDisposableResource(
|
|
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 (
|
|
1031
|
-
|
|
1032
|
-
|
|
1219
|
+
catch (e_4) {
|
|
1220
|
+
env_4.error = e_4;
|
|
1221
|
+
env_4.hasError = true;
|
|
1033
1222
|
}
|
|
1034
1223
|
finally {
|
|
1035
|
-
__disposeResources(
|
|
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
|
|
1277
|
+
const env_5 = { stack: [], error: void 0, hasError: false };
|
|
1079
1278
|
try {
|
|
1080
|
-
const bufferedFrame = __addDisposableResource(
|
|
1279
|
+
const bufferedFrame = __addDisposableResource(env_5, _bufferedFrame, false);
|
|
1081
1280
|
await this.codecContext.sendFrame(bufferedFrame);
|
|
1082
1281
|
}
|
|
1083
|
-
catch (
|
|
1084
|
-
|
|
1085
|
-
|
|
1282
|
+
catch (e_5) {
|
|
1283
|
+
env_5.error = e_5;
|
|
1284
|
+
env_5.hasError = true;
|
|
1086
1285
|
}
|
|
1087
1286
|
finally {
|
|
1088
|
-
__disposeResources(
|
|
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
|
|
1347
|
+
const env_6 = { stack: [], error: void 0, hasError: false };
|
|
1139
1348
|
try {
|
|
1140
|
-
const bufferedFrame = __addDisposableResource(
|
|
1349
|
+
const bufferedFrame = __addDisposableResource(env_6, _bufferedFrame, false);
|
|
1141
1350
|
this.codecContext.sendFrameSync(bufferedFrame);
|
|
1142
1351
|
}
|
|
1143
|
-
catch (
|
|
1144
|
-
|
|
1145
|
-
|
|
1352
|
+
catch (e_6) {
|
|
1353
|
+
env_6.error = e_6;
|
|
1354
|
+
env_6.hasError = true;
|
|
1146
1355
|
}
|
|
1147
1356
|
finally {
|
|
1148
|
-
__disposeResources(
|
|
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
|
|
1500
|
+
const env_7 = { stack: [], error: void 0, hasError: false };
|
|
1292
1501
|
try {
|
|
1293
|
-
const bufferedFrame = __addDisposableResource(
|
|
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 (
|
|
1299
|
-
|
|
1300
|
-
|
|
1507
|
+
catch (e_7) {
|
|
1508
|
+
env_7.error = e_7;
|
|
1509
|
+
env_7.hasError = true;
|
|
1301
1510
|
}
|
|
1302
1511
|
finally {
|
|
1303
|
-
__disposeResources(
|
|
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
|
|
1605
|
+
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
1397
1606
|
try {
|
|
1398
|
-
const bufferedFrame = __addDisposableResource(
|
|
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 (
|
|
1404
|
-
|
|
1405
|
-
|
|
1612
|
+
catch (e_8) {
|
|
1613
|
+
env_8.error = e_8;
|
|
1614
|
+
env_8.hasError = true;
|
|
1406
1615
|
}
|
|
1407
1616
|
finally {
|
|
1408
|
-
__disposeResources(
|
|
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
|
|
1759
|
+
const env_9 = { stack: [], error: void 0, hasError: false };
|
|
1539
1760
|
try {
|
|
1540
|
-
const frame = __addDisposableResource(
|
|
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 (
|
|
1560
|
-
|
|
1561
|
-
|
|
1780
|
+
catch (e_9) {
|
|
1781
|
+
env_9.error = e_9;
|
|
1782
|
+
env_9.hasError = true;
|
|
1562
1783
|
}
|
|
1563
1784
|
finally {
|
|
1564
|
-
__disposeResources(
|
|
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:
|
|
1701
|
-
|
|
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:
|
|
1794
|
-
|
|
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
|
*
|