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