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