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/muxer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync } from 'fs';
|
|
|
2
2
|
import { mkdir } from 'fs/promises';
|
|
3
3
|
import { Writable } from 'node:stream';
|
|
4
4
|
import { dirname, resolve } from 'path';
|
|
5
|
-
import { AV_CODEC_FLAG_GLOBAL_HEADER, AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_DEFAULT, AV_NOPTS_VALUE, AV_TIME_BASE_Q, AVERROR_EAGAIN, AVERROR_EOF, AVFMT_FLAG_CUSTOM_IO, AVFMT_GLOBALHEADER, AVFMT_NOFILE, AVFMT_TS_NONSTRICT, AVIO_FLAG_WRITE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, } from '../constants/constants.js';
|
|
5
|
+
import { AV_CODEC_FLAG_GLOBAL_HEADER, AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_DEFAULT, AV_NOPTS_VALUE, AV_OPT_TYPE_BOOL, AV_OPT_TYPE_INT, AV_TIME_BASE_Q, AVERROR_EAGAIN, AVERROR_EOF, AVFMT_AVOID_NEG_TS_MAKE_ZERO, AVFMT_FLAG_CUSTOM_IO, AVFMT_GLOBALHEADER, AVFMT_NOFILE, AVFMT_TS_NONSTRICT, AVIO_FLAG_WRITE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, } from '../constants/constants.js';
|
|
6
6
|
import { Dictionary } from '../lib/dictionary.js';
|
|
7
7
|
import { FFmpegError } from '../lib/error.js';
|
|
8
8
|
import { FormatContext } from '../lib/format-context.js';
|
|
@@ -16,6 +16,35 @@ import { Encoder } from './encoder.js';
|
|
|
16
16
|
import { IOStream } from './io-stream.js';
|
|
17
17
|
import { AsyncQueue } from './utilities/async-queue.js';
|
|
18
18
|
import { applyContextOptions } from './utilities/context-options.js';
|
|
19
|
+
/**
|
|
20
|
+
* Output formats implemented by libavformat/movenc.c, whose sample durations are 32-bit.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const MOV_FAMILY_FORMATS = new Set(['mov', 'mp4', '3gp', '3g2', 'psp', 'ipod', 'ismv', 'f4v', 'avif']);
|
|
25
|
+
/**
|
|
26
|
+
* Smallest DTS step or packet duration (stream time base ticks) rejected for movenc outputs.
|
|
27
|
+
*
|
|
28
|
+
* movenc aborts the process via av_assert0(next_dts <= INT_MAX) in get_cluster_duration()
|
|
29
|
+
* when it writes a sample table. A step of exactly INT_MAX still trips it once movenc nudges
|
|
30
|
+
* a later DTS by one tick, so the limit itself is already rejected.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
const MOV_SAMPLE_DURATION_LIMIT = 0x7fffffffn;
|
|
35
|
+
/**
|
|
36
|
+
* Packets per bucket of the window of recent packets the dtsForwardThreshold check compares a packet with.
|
|
37
|
+
*
|
|
38
|
+
* Two buckets cover a stream's last 128 to 256 packets. After a gap in a live
|
|
39
|
+
* source, a parser or encoder first releases the frames it held back before
|
|
40
|
+
* the gap: those frames absorb the wait, and the first frame after the gap,
|
|
41
|
+
* though on time, steps forward by the whole gap against them. Against the
|
|
42
|
+
* frames before the gap it does not, and parsers and encoders hold back far
|
|
43
|
+
* fewer frames than the window spans.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
const FORWARD_WINDOW_BUCKET = 128;
|
|
19
48
|
/**
|
|
20
49
|
* High-level muxer for writing and muxing media files.
|
|
21
50
|
*
|
|
@@ -69,6 +98,7 @@ export class Muxer {
|
|
|
69
98
|
options;
|
|
70
99
|
_streams = new Map();
|
|
71
100
|
ioContext;
|
|
101
|
+
customIO = false; // ioContext is callback-backed (Writable/IOOutputCallbacks) - freed, never closed
|
|
72
102
|
headerWritten = false;
|
|
73
103
|
headerWritePromise;
|
|
74
104
|
trailerWritten = false;
|
|
@@ -80,12 +110,30 @@ export class Muxer {
|
|
|
80
110
|
writeWorkerPromise; // Background worker promise
|
|
81
111
|
writeWorkerError; // First error from the write worker - poisons subsequent writes and close()
|
|
82
112
|
signal;
|
|
113
|
+
movSampleLimit = false; // Output is a movenc format with 32-bit sample durations (set before the header write)
|
|
114
|
+
tsNonStrict = false; // Output allows equal consecutive DTS (AVFMT_TS_NONSTRICT, set before the header write)
|
|
115
|
+
movZeroStart = false; // movenc puts each track's first sample at the output's start (no edit list, set after the header write)
|
|
116
|
+
interleaveDeltaUs = 0n; // libavformat's max_interleave_delta in µs (set after the header write)
|
|
117
|
+
trailerDiscarded = false; // close() skips the trailer, see discardOnClose()
|
|
118
|
+
forwardThresholdMs = 0; // dtsForwardThreshold in ms, 0 = off
|
|
119
|
+
clock = () => performance.now(); // Monotonic ms clock for packet arrival times
|
|
120
|
+
packetObserver; // Follows the accepted packets (FMP4Stream's maxPacketAge)
|
|
83
121
|
/**
|
|
84
122
|
* @param options - Media output options
|
|
85
123
|
*
|
|
124
|
+
* @throws {RangeError} If dtsBackwardThreshold or dtsForwardThreshold is not a finite number >= 0
|
|
125
|
+
*
|
|
86
126
|
* @internal
|
|
87
127
|
*/
|
|
88
128
|
constructor(options) {
|
|
129
|
+
for (const [name, threshold] of [
|
|
130
|
+
['dtsBackwardThreshold', options?.dtsBackwardThreshold],
|
|
131
|
+
['dtsForwardThreshold', options?.dtsForwardThreshold],
|
|
132
|
+
]) {
|
|
133
|
+
if (threshold !== undefined && (!Number.isFinite(threshold) || threshold < 0)) {
|
|
134
|
+
throw new RangeError(`${name} must be a finite number of seconds >= 0, got ${threshold}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
89
137
|
this.options = {
|
|
90
138
|
copyInitialNonkeyframes: false,
|
|
91
139
|
exitOnError: true,
|
|
@@ -93,6 +141,7 @@ export class Muxer {
|
|
|
93
141
|
useAsyncWrite: true,
|
|
94
142
|
...options,
|
|
95
143
|
};
|
|
144
|
+
this.forwardThresholdMs = (options?.dtsForwardThreshold ?? 0) * 1000;
|
|
96
145
|
this.formatContext = new FormatContext();
|
|
97
146
|
}
|
|
98
147
|
static async open(target, options) {
|
|
@@ -144,6 +193,7 @@ export class Muxer {
|
|
|
144
193
|
}
|
|
145
194
|
}
|
|
146
195
|
output.ioContext = IOStream.createOutput(target, options);
|
|
196
|
+
output.customIO = true;
|
|
147
197
|
output.formatContext.pb = output.ioContext;
|
|
148
198
|
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
149
199
|
}
|
|
@@ -162,6 +212,7 @@ export class Muxer {
|
|
|
162
212
|
}
|
|
163
213
|
}
|
|
164
214
|
output.ioContext = IOStream.createOutput(target, options);
|
|
215
|
+
output.customIO = true;
|
|
165
216
|
output.formatContext.pb = output.ioContext;
|
|
166
217
|
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
167
218
|
}
|
|
@@ -175,10 +226,10 @@ export class Muxer {
|
|
|
175
226
|
// Cleanup on error
|
|
176
227
|
if (output.ioContext) {
|
|
177
228
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
229
|
+
// Clear the pb reference first - freeContext() below closes a pb that
|
|
230
|
+
// is still set, which would double-free the context released here
|
|
231
|
+
output.formatContext.pb = null;
|
|
232
|
+
if (output.customIO) {
|
|
182
233
|
// For custom IO with callbacks, free the context
|
|
183
234
|
output.ioContext.freeContext();
|
|
184
235
|
}
|
|
@@ -251,6 +302,7 @@ export class Muxer {
|
|
|
251
302
|
}
|
|
252
303
|
}
|
|
253
304
|
output.ioContext = IOStream.createOutput(target, options);
|
|
305
|
+
output.customIO = true;
|
|
254
306
|
output.formatContext.pb = output.ioContext;
|
|
255
307
|
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
256
308
|
}
|
|
@@ -269,6 +321,7 @@ export class Muxer {
|
|
|
269
321
|
}
|
|
270
322
|
}
|
|
271
323
|
output.ioContext = IOStream.createOutput(target, options);
|
|
324
|
+
output.customIO = true;
|
|
272
325
|
output.formatContext.pb = output.ioContext;
|
|
273
326
|
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
274
327
|
}
|
|
@@ -282,10 +335,10 @@ export class Muxer {
|
|
|
282
335
|
// Cleanup on error
|
|
283
336
|
if (output.ioContext) {
|
|
284
337
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
// Clear the pb reference first - freeContext() below closes a pb that
|
|
339
|
+
// is still set, which would double-free the context released here
|
|
340
|
+
output.formatContext.pb = null;
|
|
341
|
+
if (output.customIO) {
|
|
289
342
|
// For custom IO with callbacks, free the context
|
|
290
343
|
output.ioContext.freeContext();
|
|
291
344
|
}
|
|
@@ -446,6 +499,14 @@ export class Muxer {
|
|
|
446
499
|
}
|
|
447
500
|
const ret = stream.codecpar.copy(outStream.codecpar);
|
|
448
501
|
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
|
|
502
|
+
// Keep the source codec tag only where the output format accepts it, as
|
|
503
|
+
// FFmpeg CLI's streamcopy_init() does; otherwise let the muxer pick one.
|
|
504
|
+
// E.g. mp4's 'mp4a' for AAC fails the Matroska header as invalid data.
|
|
505
|
+
const oformat = this.formatContext.oformat;
|
|
506
|
+
const { codecTag, codecId } = outStream.codecpar;
|
|
507
|
+
if (codecTag && oformat && oformat.getCodecId(codecTag) !== codecId && oformat.getCodecTag(codecId) !== null) {
|
|
508
|
+
outStream.codecpar.codecTag = 0;
|
|
509
|
+
}
|
|
449
510
|
// Set the timebases
|
|
450
511
|
const sourceTimeBase = stream.timeBase;
|
|
451
512
|
outStream.timeBase = new Rational(stream.timeBase.num, stream.timeBase.den);
|
|
@@ -475,12 +536,13 @@ export class Muxer {
|
|
|
475
536
|
this._streams.set(outStream.index, {
|
|
476
537
|
initialized: true,
|
|
477
538
|
outputStream: outStream,
|
|
478
|
-
inputStream: stream,
|
|
479
539
|
encoder: undefined,
|
|
480
540
|
sourceTimeBase,
|
|
481
541
|
isStreamCopy: true,
|
|
482
542
|
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
483
543
|
preMuxQueue: [],
|
|
544
|
+
preMuxArrivals: [],
|
|
545
|
+
sqArrivals: [],
|
|
484
546
|
preMuxQueueDataSize: 0,
|
|
485
547
|
eofReceived: false,
|
|
486
548
|
lastMuxDts: AV_NOPTS_VALUE,
|
|
@@ -495,13 +557,23 @@ export class Muxer {
|
|
|
495
557
|
this._streams.set(outStream.index, {
|
|
496
558
|
initialized: false,
|
|
497
559
|
outputStream: outStream,
|
|
498
|
-
|
|
560
|
+
input: stream
|
|
561
|
+
? {
|
|
562
|
+
metadata: stream.metadata,
|
|
563
|
+
disposition: stream.disposition,
|
|
564
|
+
codedSideData: stream.codecpar.getAllCodedSideData(),
|
|
565
|
+
duration: stream.duration,
|
|
566
|
+
timeBase: stream.timeBase,
|
|
567
|
+
}
|
|
568
|
+
: undefined,
|
|
499
569
|
encoder,
|
|
500
570
|
bsf: options?.bsf,
|
|
501
571
|
sourceTimeBase: undefined, // Will be set on initialization
|
|
502
572
|
isStreamCopy: false,
|
|
503
573
|
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
504
574
|
preMuxQueue: [],
|
|
575
|
+
preMuxArrivals: [],
|
|
576
|
+
sqArrivals: [],
|
|
505
577
|
preMuxQueueDataSize: 0,
|
|
506
578
|
eofReceived: false,
|
|
507
579
|
lastMuxDts: AV_NOPTS_VALUE,
|
|
@@ -659,6 +731,9 @@ export class Muxer {
|
|
|
659
731
|
*
|
|
660
732
|
* @throws {Error} If stream invalid or encoder not initialized
|
|
661
733
|
*
|
|
734
|
+
* @throws {Error} On a timestamp discontinuity the output cannot take (see {@link MuxerOptions.dtsBackwardThreshold}).
|
|
735
|
+
* With the background write queue (more than one stream) it surfaces from a later writePacket() or close().
|
|
736
|
+
*
|
|
662
737
|
* @throws {FFmpegError} If write fails
|
|
663
738
|
*
|
|
664
739
|
* @example
|
|
@@ -737,28 +812,25 @@ export class Muxer {
|
|
|
737
812
|
FFmpegError.throwIfError(bsfRet, 'Failed to copy codec parameters from bitstream filter');
|
|
738
813
|
}
|
|
739
814
|
// 4. Copy metadata from input stream
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (metadata) {
|
|
743
|
-
streamInfo.outputStream.metadata = metadata;
|
|
815
|
+
const input = streamInfo.input;
|
|
816
|
+
if (input) {
|
|
817
|
+
if (input.metadata) {
|
|
818
|
+
streamInfo.outputStream.metadata = input.metadata;
|
|
744
819
|
}
|
|
745
820
|
// 5. Copy disposition from input stream
|
|
746
|
-
streamInfo.outputStream.disposition =
|
|
821
|
+
streamInfo.outputStream.disposition = input.disposition;
|
|
747
822
|
// 5b. Copy coded_side_data from input stream (display matrix / rotation,
|
|
748
823
|
// HDR mastering display & content light level). The encoder path only
|
|
749
824
|
// copied codec parameters, so without this these are lost on transcode
|
|
750
825
|
// (e.g. a portrait phone video would play sideways). Mirrors the stream
|
|
751
826
|
// copy path.
|
|
752
|
-
const
|
|
753
|
-
for (const sd of inputSideData) {
|
|
827
|
+
for (const sd of input.codedSideData) {
|
|
754
828
|
streamInfo.outputStream.codecpar.addCodedSideData(sd.type, sd.data);
|
|
755
829
|
}
|
|
756
830
|
// 6. Copy duration hint from input stream
|
|
757
|
-
if (
|
|
758
|
-
const inputTb = streamInfo.inputStream.timeBase;
|
|
831
|
+
if (input.duration > 0n) {
|
|
759
832
|
const outputTb = streamInfo.outputStream.timeBase;
|
|
760
|
-
|
|
761
|
-
streamInfo.outputStream.duration = rescaledDuration;
|
|
833
|
+
streamInfo.outputStream.duration = avRescaleQ(input.duration, input.timeBase, outputTb);
|
|
762
834
|
}
|
|
763
835
|
}
|
|
764
836
|
// Update the source timebase for timestamp rescaling
|
|
@@ -782,6 +854,7 @@ export class Muxer {
|
|
|
782
854
|
if (uninitialized || this.headerWritePromise) {
|
|
783
855
|
// Buffer NULL as EOF marker (no size contribution)
|
|
784
856
|
streamInfo.preMuxQueue.push(null);
|
|
857
|
+
streamInfo.preMuxArrivals.push(0);
|
|
785
858
|
return;
|
|
786
859
|
}
|
|
787
860
|
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
@@ -814,7 +887,7 @@ export class Muxer {
|
|
|
814
887
|
throw new Error('Failed to clone packet from sync queue');
|
|
815
888
|
}
|
|
816
889
|
pkt.streamIndex = recvRet;
|
|
817
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
890
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
818
891
|
}
|
|
819
892
|
}
|
|
820
893
|
}
|
|
@@ -825,6 +898,10 @@ export class Muxer {
|
|
|
825
898
|
if (!clonedPacket) {
|
|
826
899
|
throw new Error('Failed to clone packet for writing');
|
|
827
900
|
}
|
|
901
|
+
// The forward check compares DTS steps with the time between packets as
|
|
902
|
+
// they reach the muxer. Taken here and carried through the pre-mux, sync
|
|
903
|
+
// and write queues, so time a packet spends queued does not count.
|
|
904
|
+
const arrival = this.forwardThresholdMs > 0 ? this.clock() : 0;
|
|
828
905
|
// Apply streamcopy filtering BEFORE buffering
|
|
829
906
|
// This ensures rejected packets never enter the queue/buffer
|
|
830
907
|
if (streamInfo.isStreamCopy) {
|
|
@@ -838,6 +915,9 @@ export class Muxer {
|
|
|
838
915
|
// For encoded (non-streamcopy) streams, strip the device's startTime base.
|
|
839
916
|
this.applyStartTimeOffset(clonedPacket, streamInfo);
|
|
840
917
|
}
|
|
918
|
+
// Announced on acceptance, so the observer also counts what waits in the
|
|
919
|
+
// pre-mux queue for the header.
|
|
920
|
+
const observed = this.announcePacket(clonedPacket, streamIndex);
|
|
841
921
|
// Check if any streams are still uninitialized or header is being written
|
|
842
922
|
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
843
923
|
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
@@ -855,12 +935,16 @@ export class Muxer {
|
|
|
855
935
|
// Check if we would exceed packet limit (only if threshold reached)
|
|
856
936
|
if (currentPackets >= effectiveMaxPackets) {
|
|
857
937
|
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
938
|
+
if (observed) {
|
|
939
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
940
|
+
}
|
|
858
941
|
throw new Error(
|
|
859
942
|
// eslint-disable-next-line @stylistic/max-len
|
|
860
943
|
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
861
944
|
}
|
|
862
945
|
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
863
946
|
streamInfo.preMuxQueue.push(clonedPacket);
|
|
947
|
+
streamInfo.preMuxArrivals.push(arrival);
|
|
864
948
|
streamInfo.preMuxQueueDataSize += packetSize;
|
|
865
949
|
return; // Don't proceed to header write yet
|
|
866
950
|
}
|
|
@@ -872,9 +956,16 @@ export class Muxer {
|
|
|
872
956
|
const ret = await this.formatContext.writeHeader();
|
|
873
957
|
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
874
958
|
this.headerWritten = true;
|
|
959
|
+
this.readHeaderSetup();
|
|
875
960
|
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
876
961
|
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
877
|
-
|
|
962
|
+
// Concurrent writers keep queueing while headerWritePromise is set, also
|
|
963
|
+
// after a flush found the queues empty. Drain until they are empty and
|
|
964
|
+
// clear the promise in that same tick, or such packets are never written.
|
|
965
|
+
do {
|
|
966
|
+
await this.flushPreMuxQueues();
|
|
967
|
+
} while (this.hasPreMuxPackets());
|
|
968
|
+
this.headerWritePromise = undefined;
|
|
878
969
|
})();
|
|
879
970
|
await this.headerWritePromise;
|
|
880
971
|
if (this.headerWritten) {
|
|
@@ -890,6 +981,9 @@ export class Muxer {
|
|
|
890
981
|
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
891
982
|
// Handle errors from sq_send
|
|
892
983
|
if (ret < 0) {
|
|
984
|
+
if (observed) {
|
|
985
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
986
|
+
}
|
|
893
987
|
if (ret === AVERROR_EOF) {
|
|
894
988
|
// Stream finished - this is normal, just return
|
|
895
989
|
return;
|
|
@@ -899,6 +993,8 @@ export class Muxer {
|
|
|
899
993
|
}
|
|
900
994
|
return;
|
|
901
995
|
}
|
|
996
|
+
// The sync queue keeps each stream's packets in order and drops none.
|
|
997
|
+
streamInfo.sqArrivals.push(arrival);
|
|
902
998
|
// Receive synchronized packets from queue and write to muxer
|
|
903
999
|
while (!this.isClosed) {
|
|
904
1000
|
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
@@ -919,14 +1015,14 @@ export class Muxer {
|
|
|
919
1015
|
}
|
|
920
1016
|
pkt.streamIndex = recvRet;
|
|
921
1017
|
// Write packet (muxer takes ownership)
|
|
922
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
1018
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
923
1019
|
}
|
|
924
1020
|
}
|
|
925
1021
|
}
|
|
926
1022
|
else {
|
|
927
1023
|
// No sync queue needed - write directly
|
|
928
1024
|
clonedPacket.streamIndex = streamIndex;
|
|
929
|
-
await this.write(clonedPacket, streamInfo, streamIndex);
|
|
1025
|
+
await this.write(clonedPacket, streamInfo, streamIndex, arrival);
|
|
930
1026
|
}
|
|
931
1027
|
}
|
|
932
1028
|
/**
|
|
@@ -959,6 +1055,8 @@ export class Muxer {
|
|
|
959
1055
|
*
|
|
960
1056
|
* @throws {Error} If stream invalid or encoder not initialized
|
|
961
1057
|
*
|
|
1058
|
+
* @throws {Error} On a timestamp discontinuity the output cannot take (see {@link MuxerOptions.dtsBackwardThreshold})
|
|
1059
|
+
*
|
|
962
1060
|
* @throws {FFmpegError} If write fails
|
|
963
1061
|
*
|
|
964
1062
|
* @example
|
|
@@ -1041,28 +1139,25 @@ export class Muxer {
|
|
|
1041
1139
|
FFmpegError.throwIfError(bsfRet, 'Failed to copy codec parameters from bitstream filter');
|
|
1042
1140
|
}
|
|
1043
1141
|
// 4. Copy metadata from input stream
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (metadata) {
|
|
1047
|
-
streamInfo.outputStream.metadata = metadata;
|
|
1142
|
+
const input = streamInfo.input;
|
|
1143
|
+
if (input) {
|
|
1144
|
+
if (input.metadata) {
|
|
1145
|
+
streamInfo.outputStream.metadata = input.metadata;
|
|
1048
1146
|
}
|
|
1049
1147
|
// 5. Copy disposition from input stream
|
|
1050
|
-
streamInfo.outputStream.disposition =
|
|
1148
|
+
streamInfo.outputStream.disposition = input.disposition;
|
|
1051
1149
|
// 5b. Copy coded_side_data from input stream (display matrix / rotation,
|
|
1052
1150
|
// HDR mastering display & content light level). The encoder path only
|
|
1053
1151
|
// copied codec parameters, so without this these are lost on transcode
|
|
1054
1152
|
// (e.g. a portrait phone video would play sideways). Mirrors the stream
|
|
1055
1153
|
// copy path.
|
|
1056
|
-
const
|
|
1057
|
-
for (const sd of inputSideData) {
|
|
1154
|
+
for (const sd of input.codedSideData) {
|
|
1058
1155
|
streamInfo.outputStream.codecpar.addCodedSideData(sd.type, sd.data);
|
|
1059
1156
|
}
|
|
1060
1157
|
// 6. Copy duration hint from input stream
|
|
1061
|
-
if (
|
|
1062
|
-
const inputTb = streamInfo.inputStream.timeBase;
|
|
1158
|
+
if (input.duration > 0n) {
|
|
1063
1159
|
const outputTb = streamInfo.outputStream.timeBase;
|
|
1064
|
-
|
|
1065
|
-
streamInfo.outputStream.duration = rescaledDuration;
|
|
1160
|
+
streamInfo.outputStream.duration = avRescaleQ(input.duration, input.timeBase, outputTb);
|
|
1066
1161
|
}
|
|
1067
1162
|
}
|
|
1068
1163
|
// Update the source timebase for timestamp rescaling
|
|
@@ -1086,6 +1181,7 @@ export class Muxer {
|
|
|
1086
1181
|
if (uninitialized || this.headerWritePromise) {
|
|
1087
1182
|
// Buffer NULL as EOF marker (no size contribution)
|
|
1088
1183
|
streamInfo.preMuxQueue.push(null);
|
|
1184
|
+
streamInfo.preMuxArrivals.push(0);
|
|
1089
1185
|
return;
|
|
1090
1186
|
}
|
|
1091
1187
|
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
@@ -1118,7 +1214,7 @@ export class Muxer {
|
|
|
1118
1214
|
throw new Error('Failed to clone packet from sync queue');
|
|
1119
1215
|
}
|
|
1120
1216
|
pkt.streamIndex = recvRet;
|
|
1121
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1217
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1122
1218
|
}
|
|
1123
1219
|
}
|
|
1124
1220
|
}
|
|
@@ -1129,6 +1225,7 @@ export class Muxer {
|
|
|
1129
1225
|
if (!clonedPacket) {
|
|
1130
1226
|
throw new Error('Failed to clone packet for writing');
|
|
1131
1227
|
}
|
|
1228
|
+
const arrival = this.forwardThresholdMs > 0 ? this.clock() : 0;
|
|
1132
1229
|
// Apply streamcopy filtering BEFORE buffering
|
|
1133
1230
|
// This ensures rejected packets never enter the queue/buffer
|
|
1134
1231
|
if (streamInfo.isStreamCopy) {
|
|
@@ -1142,6 +1239,9 @@ export class Muxer {
|
|
|
1142
1239
|
// For encoded (non-streamcopy) streams, strip the device's startTime base.
|
|
1143
1240
|
this.applyStartTimeOffset(clonedPacket, streamInfo);
|
|
1144
1241
|
}
|
|
1242
|
+
// Announced on acceptance, so the observer also counts what waits in the
|
|
1243
|
+
// pre-mux queue for the header.
|
|
1244
|
+
const observed = this.announcePacket(clonedPacket, streamIndex);
|
|
1145
1245
|
// Check if any streams are still uninitialized
|
|
1146
1246
|
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
1147
1247
|
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
@@ -1159,12 +1259,16 @@ export class Muxer {
|
|
|
1159
1259
|
// Check if we would exceed packet limit (only if threshold reached)
|
|
1160
1260
|
if (currentPackets >= effectiveMaxPackets) {
|
|
1161
1261
|
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
1262
|
+
if (observed) {
|
|
1263
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
1264
|
+
}
|
|
1162
1265
|
throw new Error(
|
|
1163
1266
|
// eslint-disable-next-line @stylistic/max-len
|
|
1164
1267
|
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
1165
1268
|
}
|
|
1166
1269
|
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
1167
1270
|
streamInfo.preMuxQueue.push(clonedPacket);
|
|
1271
|
+
streamInfo.preMuxArrivals.push(arrival);
|
|
1168
1272
|
streamInfo.preMuxQueueDataSize += packetSize;
|
|
1169
1273
|
return; // Don't proceed to header write yet
|
|
1170
1274
|
}
|
|
@@ -1174,6 +1278,7 @@ export class Muxer {
|
|
|
1174
1278
|
const ret = this.formatContext.writeHeaderSync();
|
|
1175
1279
|
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
1176
1280
|
this.headerWritten = true;
|
|
1281
|
+
this.readHeaderSetup();
|
|
1177
1282
|
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
1178
1283
|
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
1179
1284
|
this.flushPreMuxQueuesSync();
|
|
@@ -1187,6 +1292,9 @@ export class Muxer {
|
|
|
1187
1292
|
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
1188
1293
|
// Handle errors from sq_send
|
|
1189
1294
|
if (ret < 0) {
|
|
1295
|
+
if (observed) {
|
|
1296
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
1297
|
+
}
|
|
1190
1298
|
if (ret === AVERROR_EOF) {
|
|
1191
1299
|
// Stream finished - this is normal, just return
|
|
1192
1300
|
return;
|
|
@@ -1196,6 +1304,7 @@ export class Muxer {
|
|
|
1196
1304
|
}
|
|
1197
1305
|
return;
|
|
1198
1306
|
}
|
|
1307
|
+
streamInfo.sqArrivals.push(arrival);
|
|
1199
1308
|
// Receive synchronized packets from queue and write to muxer
|
|
1200
1309
|
while (!this.isClosed) {
|
|
1201
1310
|
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
@@ -1216,14 +1325,14 @@ export class Muxer {
|
|
|
1216
1325
|
}
|
|
1217
1326
|
pkt.streamIndex = recvRet;
|
|
1218
1327
|
// Write packet (muxer takes ownership)
|
|
1219
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1328
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1220
1329
|
}
|
|
1221
1330
|
}
|
|
1222
1331
|
}
|
|
1223
1332
|
else {
|
|
1224
1333
|
// No sync queue needed - write directly
|
|
1225
1334
|
clonedPacket.streamIndex = streamIndex;
|
|
1226
|
-
this.writeSync(clonedPacket, streamInfo, streamIndex);
|
|
1335
|
+
this.writeSync(clonedPacket, streamInfo, streamIndex, arrival);
|
|
1227
1336
|
}
|
|
1228
1337
|
}
|
|
1229
1338
|
/**
|
|
@@ -1253,6 +1362,8 @@ export class Muxer {
|
|
|
1253
1362
|
return;
|
|
1254
1363
|
}
|
|
1255
1364
|
this.isClosed = true;
|
|
1365
|
+
// Detached first, so the final flush can never fail on the observer.
|
|
1366
|
+
this.packetObserver = undefined;
|
|
1256
1367
|
// A header write launched by a concurrent writePacket() must settle before
|
|
1257
1368
|
// teardown: nulling pb / freeing the format context under a running
|
|
1258
1369
|
// avformat_write_header() crashes on the worker thread. This happens when a
|
|
@@ -1287,6 +1398,7 @@ export class Muxer {
|
|
|
1287
1398
|
pkt?.free();
|
|
1288
1399
|
}
|
|
1289
1400
|
streamInfo.preMuxQueue = [];
|
|
1401
|
+
streamInfo.preMuxArrivals = [];
|
|
1290
1402
|
}
|
|
1291
1403
|
// Free sync queue resources
|
|
1292
1404
|
if (this.sqPacket) {
|
|
@@ -1299,7 +1411,7 @@ export class Muxer {
|
|
|
1299
1411
|
}
|
|
1300
1412
|
// Try to write trailer if header was written but trailer wasn't
|
|
1301
1413
|
try {
|
|
1302
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
1414
|
+
if (this.headerWritten && !this.trailerWritten && !this.trailerDiscarded) {
|
|
1303
1415
|
await this.formatContext.writeTrailer();
|
|
1304
1416
|
this.trailerWritten = true;
|
|
1305
1417
|
}
|
|
@@ -1311,11 +1423,11 @@ export class Muxer {
|
|
|
1311
1423
|
if (this.ioContext) {
|
|
1312
1424
|
this.formatContext.pb = null;
|
|
1313
1425
|
}
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1316
|
-
//
|
|
1426
|
+
// For file-based IO, close the file handle via closep - freeContext() alone
|
|
1427
|
+
// would release the AVIOContext but leak the underlying protocol handle,
|
|
1428
|
+
// leaving the file locked (EBUSY on Windows) until the process exits.
|
|
1317
1429
|
// For custom IO, the context will be freed below
|
|
1318
|
-
if (this.ioContext && !
|
|
1430
|
+
if (this.ioContext && !this.customIO) {
|
|
1319
1431
|
try {
|
|
1320
1432
|
await this.ioContext.closep();
|
|
1321
1433
|
}
|
|
@@ -1333,7 +1445,7 @@ export class Muxer {
|
|
|
1333
1445
|
}
|
|
1334
1446
|
}
|
|
1335
1447
|
// Now free custom IO context if present
|
|
1336
|
-
if (this.ioContext &&
|
|
1448
|
+
if (this.ioContext && this.customIO) {
|
|
1337
1449
|
try {
|
|
1338
1450
|
this.ioContext.freeContext();
|
|
1339
1451
|
}
|
|
@@ -1373,6 +1485,7 @@ export class Muxer {
|
|
|
1373
1485
|
return;
|
|
1374
1486
|
}
|
|
1375
1487
|
this.isClosed = true;
|
|
1488
|
+
this.packetObserver = undefined;
|
|
1376
1489
|
// Free PreMuxQueue packets
|
|
1377
1490
|
for (const streamInfo of this._streams.values()) {
|
|
1378
1491
|
// Free any packets in PreMuxQueue
|
|
@@ -1380,6 +1493,7 @@ export class Muxer {
|
|
|
1380
1493
|
pkt?.free();
|
|
1381
1494
|
}
|
|
1382
1495
|
streamInfo.preMuxQueue = [];
|
|
1496
|
+
streamInfo.preMuxArrivals = [];
|
|
1383
1497
|
}
|
|
1384
1498
|
// Free sync queue resources
|
|
1385
1499
|
if (this.sqPacket) {
|
|
@@ -1392,7 +1506,7 @@ export class Muxer {
|
|
|
1392
1506
|
}
|
|
1393
1507
|
// Try to write trailer if header was written but trailer wasn't
|
|
1394
1508
|
try {
|
|
1395
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
1509
|
+
if (this.headerWritten && !this.trailerWritten && !this.trailerDiscarded) {
|
|
1396
1510
|
this.formatContext.writeTrailerSync();
|
|
1397
1511
|
this.trailerWritten = true;
|
|
1398
1512
|
}
|
|
@@ -1404,11 +1518,9 @@ export class Muxer {
|
|
|
1404
1518
|
if (this.ioContext) {
|
|
1405
1519
|
this.formatContext.pb = null;
|
|
1406
1520
|
}
|
|
1407
|
-
// Determine if this is custom IO before freeing format context
|
|
1408
|
-
const isCustomIO = this.formatContext.hasFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
1409
1521
|
// For file-based IO, close the file handle via closep
|
|
1410
1522
|
// For custom IO, the context will be freed below
|
|
1411
|
-
if (this.ioContext && !
|
|
1523
|
+
if (this.ioContext && !this.customIO) {
|
|
1412
1524
|
try {
|
|
1413
1525
|
this.ioContext.closepSync();
|
|
1414
1526
|
}
|
|
@@ -1426,7 +1538,7 @@ export class Muxer {
|
|
|
1426
1538
|
}
|
|
1427
1539
|
}
|
|
1428
1540
|
// Now free custom IO context if present
|
|
1429
|
-
if (this.ioContext &&
|
|
1541
|
+
if (this.ioContext && this.customIO) {
|
|
1430
1542
|
try {
|
|
1431
1543
|
this.ioContext.freeContext();
|
|
1432
1544
|
}
|
|
@@ -1447,6 +1559,73 @@ export class Muxer {
|
|
|
1447
1559
|
getFormatContext() {
|
|
1448
1560
|
return this.formatContext;
|
|
1449
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Follow the packets this muxer accepts for writing.
|
|
1564
|
+
*
|
|
1565
|
+
* The observer hears about every packet with payload once it is accepted
|
|
1566
|
+
* (before it waits in the pre-mux queue or reaches libavformat) and about
|
|
1567
|
+
* every such packet that is dropped later, so together with the output it
|
|
1568
|
+
* knows what the muxer holds. close() detaches it.
|
|
1569
|
+
*
|
|
1570
|
+
* @param observer - Observer, or undefined to detach
|
|
1571
|
+
*
|
|
1572
|
+
* @internal
|
|
1573
|
+
*/
|
|
1574
|
+
setPacketObserver(observer) {
|
|
1575
|
+
this.packetObserver = observer;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Replace the clock that stamps packet arrival times for the dtsForwardThreshold check.
|
|
1579
|
+
*
|
|
1580
|
+
* Lets tests drive real time deterministically. Set it before the first packet.
|
|
1581
|
+
*
|
|
1582
|
+
* @param clock - Monotonic time in ms
|
|
1583
|
+
*
|
|
1584
|
+
* @internal
|
|
1585
|
+
*/
|
|
1586
|
+
setClock(clock) {
|
|
1587
|
+
this.clock = clock;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Make close() skip the trailer.
|
|
1591
|
+
*
|
|
1592
|
+
* For owners that drop whatever the muxer still emits, such as FMP4Stream
|
|
1593
|
+
* after a failed session: the trailer would only push libavformat's backlog
|
|
1594
|
+
* into the void, and movenc asserts on some track states a failed session
|
|
1595
|
+
* can leave behind. The backlog is freed with the format context instead.
|
|
1596
|
+
*
|
|
1597
|
+
* @internal
|
|
1598
|
+
*/
|
|
1599
|
+
discardOnClose() {
|
|
1600
|
+
this.trailerDiscarded = true;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Announce a packet the muxer has accepted to the packet observer.
|
|
1604
|
+
*
|
|
1605
|
+
* @param pkt - The muxer's clone of the packet
|
|
1606
|
+
*
|
|
1607
|
+
* @param streamIndex - Output stream index
|
|
1608
|
+
*
|
|
1609
|
+
* @returns True when the observer now tracks the packet
|
|
1610
|
+
*
|
|
1611
|
+
* @throws {Error} If the observer fails the write (the clone is freed)
|
|
1612
|
+
*
|
|
1613
|
+
* @internal
|
|
1614
|
+
*/
|
|
1615
|
+
announcePacket(pkt, streamIndex) {
|
|
1616
|
+
// movenc drops packets without payload instead of storing a sample.
|
|
1617
|
+
if (this.packetObserver === undefined || pkt.size <= 0) {
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
this.packetObserver.onPacket(streamIndex);
|
|
1622
|
+
}
|
|
1623
|
+
catch (error) {
|
|
1624
|
+
pkt.free();
|
|
1625
|
+
throw error;
|
|
1626
|
+
}
|
|
1627
|
+
return true;
|
|
1628
|
+
}
|
|
1450
1629
|
/**
|
|
1451
1630
|
* Apply shared pre-header setup for writePacket and writePacketSync.
|
|
1452
1631
|
*
|
|
@@ -1462,6 +1641,26 @@ export class Muxer {
|
|
|
1462
1641
|
this.copyContainerMetadata();
|
|
1463
1642
|
applyContextOptions(this.formatContext, this.options.context);
|
|
1464
1643
|
this.options.configure?.(this.formatContext);
|
|
1644
|
+
const oformat = this.formatContext.oformat;
|
|
1645
|
+
this.movSampleLimit = MOV_FAMILY_FORMATS.has(oformat?.name ?? '');
|
|
1646
|
+
this.tsNonStrict = oformat?.hasFlags(AVFMT_TS_NONSTRICT) ?? false;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Read the muxing setup libavformat settles while writing the header.
|
|
1650
|
+
*
|
|
1651
|
+
* movenc decides on edit lists only then: without one (fragmented output
|
|
1652
|
+
* without delay_moov) it shifts the output to start at 0 and puts each
|
|
1653
|
+
* track's first sample there, which {@link checkMovLateStart} guards.
|
|
1654
|
+
*
|
|
1655
|
+
* @internal
|
|
1656
|
+
*/
|
|
1657
|
+
readHeaderSetup() {
|
|
1658
|
+
if (!this.movSampleLimit) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const ctx = this.formatContext;
|
|
1662
|
+
this.movZeroStart = ctx.getOption('use_editlist', AV_OPT_TYPE_BOOL) === false && ctx.getOption('avoid_negative_ts', AV_OPT_TYPE_INT) === AVFMT_AVOID_NEG_TS_MAKE_ZERO;
|
|
1663
|
+
this.interleaveDeltaUs = ctx.maxInterleaveDelta;
|
|
1465
1664
|
}
|
|
1466
1665
|
/**
|
|
1467
1666
|
* Setup sync queues based on stream configuration.
|
|
@@ -1507,6 +1706,21 @@ export class Muxer {
|
|
|
1507
1706
|
}
|
|
1508
1707
|
}
|
|
1509
1708
|
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Whether any stream still holds packets (or an EOF marker) in its pre-mux queue.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns True if a pre-mux queue is not empty
|
|
1713
|
+
*
|
|
1714
|
+
* @internal
|
|
1715
|
+
*/
|
|
1716
|
+
hasPreMuxPackets() {
|
|
1717
|
+
for (const streamInfo of this._streams.values()) {
|
|
1718
|
+
if (streamInfo.preMuxQueue.length > 0) {
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1510
1724
|
/**
|
|
1511
1725
|
* Flush all PreMuxQueues in DTS-sorted order.
|
|
1512
1726
|
*
|
|
@@ -1552,6 +1766,7 @@ export class Muxer {
|
|
|
1552
1766
|
}
|
|
1553
1767
|
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1554
1768
|
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1769
|
+
const arrival = minStreamInfo.preMuxArrivals.shift() ?? 0;
|
|
1555
1770
|
// 4. Handle NULL packet (EOF marker)
|
|
1556
1771
|
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1557
1772
|
if (!pkt) {
|
|
@@ -1576,8 +1791,15 @@ export class Muxer {
|
|
|
1576
1791
|
// NOTE: Do NOT set pkt.timeBase here!
|
|
1577
1792
|
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1578
1793
|
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1794
|
+
const size = pkt.size;
|
|
1579
1795
|
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1580
|
-
if (ret < 0 &&
|
|
1796
|
+
if (ret < 0 && size > 0) {
|
|
1797
|
+
this.packetObserver?.onPacketRejected(minStreamIndex);
|
|
1798
|
+
}
|
|
1799
|
+
if (ret >= 0) {
|
|
1800
|
+
minStreamInfo.sqArrivals.push(arrival);
|
|
1801
|
+
}
|
|
1802
|
+
else if (ret !== AVERROR_EOF) {
|
|
1581
1803
|
if (this.options.exitOnError) {
|
|
1582
1804
|
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1583
1805
|
}
|
|
@@ -1585,7 +1807,7 @@ export class Muxer {
|
|
|
1585
1807
|
}
|
|
1586
1808
|
else {
|
|
1587
1809
|
// Write directly to muxer
|
|
1588
|
-
await this.write(pkt, minStreamInfo, minStreamIndex);
|
|
1810
|
+
await this.write(pkt, minStreamInfo, minStreamIndex, arrival);
|
|
1589
1811
|
}
|
|
1590
1812
|
}
|
|
1591
1813
|
// If using SyncQueue, receive and write all interleaved packets
|
|
@@ -1608,7 +1830,7 @@ export class Muxer {
|
|
|
1608
1830
|
}
|
|
1609
1831
|
pkt.streamIndex = recvRet;
|
|
1610
1832
|
// Write packet (muxer takes ownership)
|
|
1611
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
1833
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1612
1834
|
}
|
|
1613
1835
|
}
|
|
1614
1836
|
}
|
|
@@ -1656,6 +1878,7 @@ export class Muxer {
|
|
|
1656
1878
|
break;
|
|
1657
1879
|
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1658
1880
|
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1881
|
+
const arrival = minStreamInfo.preMuxArrivals.shift() ?? 0;
|
|
1659
1882
|
// 4. Handle NULL packet (EOF marker)
|
|
1660
1883
|
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1661
1884
|
if (!pkt) {
|
|
@@ -1680,8 +1903,15 @@ export class Muxer {
|
|
|
1680
1903
|
// NOTE: Do NOT set pkt.timeBase here!
|
|
1681
1904
|
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1682
1905
|
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1906
|
+
const size = pkt.size;
|
|
1683
1907
|
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1684
|
-
if (ret < 0 &&
|
|
1908
|
+
if (ret < 0 && size > 0) {
|
|
1909
|
+
this.packetObserver?.onPacketRejected(minStreamIndex);
|
|
1910
|
+
}
|
|
1911
|
+
if (ret >= 0) {
|
|
1912
|
+
minStreamInfo.sqArrivals.push(arrival);
|
|
1913
|
+
}
|
|
1914
|
+
else if (ret !== AVERROR_EOF) {
|
|
1685
1915
|
if (this.options.exitOnError) {
|
|
1686
1916
|
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1687
1917
|
}
|
|
@@ -1689,7 +1919,7 @@ export class Muxer {
|
|
|
1689
1919
|
}
|
|
1690
1920
|
else {
|
|
1691
1921
|
// Write directly to muxer
|
|
1692
|
-
this.writeSync(pkt, minStreamInfo, minStreamIndex);
|
|
1922
|
+
this.writeSync(pkt, minStreamInfo, minStreamIndex, arrival);
|
|
1693
1923
|
}
|
|
1694
1924
|
}
|
|
1695
1925
|
// If using SyncQueue, receive and write all interleaved packets
|
|
@@ -1712,7 +1942,7 @@ export class Muxer {
|
|
|
1712
1942
|
}
|
|
1713
1943
|
pkt.streamIndex = recvRet;
|
|
1714
1944
|
// Write packet (muxer takes ownership)
|
|
1715
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1945
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1716
1946
|
}
|
|
1717
1947
|
}
|
|
1718
1948
|
}
|
|
@@ -1726,15 +1956,17 @@ export class Muxer {
|
|
|
1726
1956
|
*
|
|
1727
1957
|
* @param streamIndex - Stream index
|
|
1728
1958
|
*
|
|
1959
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
1960
|
+
*
|
|
1729
1961
|
* @internal
|
|
1730
1962
|
*/
|
|
1731
|
-
async write(pkt, streamInfo, streamIndex) {
|
|
1963
|
+
async write(pkt, streamInfo, streamIndex, arrival) {
|
|
1732
1964
|
if (this.writeQueue) {
|
|
1733
1965
|
// Use async queue for serialized writes.
|
|
1734
1966
|
// If the worker died with a write error, the queue is poisoned and send()
|
|
1735
1967
|
// rethrows that error - free the clone since it never reaches the worker.
|
|
1736
1968
|
try {
|
|
1737
|
-
await this.writeQueue.send({ pkt, streamInfo, streamIndex });
|
|
1969
|
+
await this.writeQueue.send({ pkt, streamInfo, streamIndex, arrival });
|
|
1738
1970
|
}
|
|
1739
1971
|
catch (error) {
|
|
1740
1972
|
pkt.free();
|
|
@@ -1743,7 +1975,7 @@ export class Muxer {
|
|
|
1743
1975
|
}
|
|
1744
1976
|
else {
|
|
1745
1977
|
// Direct write without serialization
|
|
1746
|
-
await this.writeInternal(pkt, streamInfo, streamIndex);
|
|
1978
|
+
await this.writeInternal(pkt, streamInfo, streamIndex, arrival);
|
|
1747
1979
|
}
|
|
1748
1980
|
}
|
|
1749
1981
|
/**
|
|
@@ -1756,19 +1988,36 @@ export class Muxer {
|
|
|
1756
1988
|
*
|
|
1757
1989
|
* @param streamIndex - Stream index
|
|
1758
1990
|
*
|
|
1991
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
1992
|
+
*
|
|
1759
1993
|
* @internal
|
|
1760
1994
|
*/
|
|
1761
|
-
async writeInternal(pkt, streamInfo, streamIndex) {
|
|
1762
|
-
//
|
|
1763
|
-
this.
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
if (
|
|
1770
|
-
|
|
1995
|
+
async writeInternal(pkt, streamInfo, streamIndex, arrival) {
|
|
1996
|
+
// Read before the write moves the payload out (see announcePacket()).
|
|
1997
|
+
const observed = this.packetObserver !== undefined && pkt.size > 0;
|
|
1998
|
+
let accepted = false;
|
|
1999
|
+
try {
|
|
2000
|
+
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
2001
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex, arrival);
|
|
2002
|
+
// Write the packet (muxer takes ownership and will unref it)
|
|
2003
|
+
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
2004
|
+
const ret = await this.formatContext.interleavedWriteFrame(pkt);
|
|
2005
|
+
accepted = ret >= 0;
|
|
2006
|
+
// Handle write errors
|
|
2007
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
2008
|
+
if (this.options.exitOnError) {
|
|
2009
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
finally {
|
|
2014
|
+
if (observed && !accepted) {
|
|
2015
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
1771
2016
|
}
|
|
2017
|
+
// Every packet reaching here is a clone this class made, and the write
|
|
2018
|
+
// has consumed its contents. Release the struct now instead of leaving a
|
|
2019
|
+
// GC-sized backlog of them behind - this is the last owner.
|
|
2020
|
+
pkt.free();
|
|
1772
2021
|
}
|
|
1773
2022
|
}
|
|
1774
2023
|
/**
|
|
@@ -1788,7 +2037,7 @@ export class Muxer {
|
|
|
1788
2037
|
const job = await this.writeQueue.receive();
|
|
1789
2038
|
if (!job)
|
|
1790
2039
|
break; // Queue closed
|
|
1791
|
-
await this.writeInternal(job.pkt, job.streamInfo, job.streamIndex);
|
|
2040
|
+
await this.writeInternal(job.pkt, job.streamInfo, job.streamIndex, job.arrival);
|
|
1792
2041
|
}
|
|
1793
2042
|
}
|
|
1794
2043
|
catch (error) {
|
|
@@ -1813,15 +2062,29 @@ export class Muxer {
|
|
|
1813
2062
|
*
|
|
1814
2063
|
* @param streamIndex - Stream index
|
|
1815
2064
|
*
|
|
2065
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2066
|
+
*
|
|
1816
2067
|
* @internal
|
|
1817
2068
|
*/
|
|
1818
|
-
writeSync(pkt, streamInfo, streamIndex) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2069
|
+
writeSync(pkt, streamInfo, streamIndex, arrival) {
|
|
2070
|
+
const observed = this.packetObserver !== undefined && pkt.size > 0;
|
|
2071
|
+
let accepted = false;
|
|
2072
|
+
try {
|
|
2073
|
+
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
2074
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex, arrival);
|
|
2075
|
+
// Write the packet (muxer takes ownership and will unref it)
|
|
2076
|
+
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
2077
|
+
const ret = this.formatContext.interleavedWriteFrameSync(pkt);
|
|
2078
|
+
accepted = ret >= 0;
|
|
2079
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
2080
|
+
}
|
|
2081
|
+
finally {
|
|
2082
|
+
if (observed && !accepted) {
|
|
2083
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
2084
|
+
}
|
|
2085
|
+
// See writeInternal(): this is the last owner of the clone.
|
|
2086
|
+
pkt.free();
|
|
2087
|
+
}
|
|
1825
2088
|
}
|
|
1826
2089
|
/**
|
|
1827
2090
|
* Streamcopy packet filtering and timestamp offset.
|
|
@@ -1849,11 +2112,12 @@ export class Muxer {
|
|
|
1849
2112
|
* Apply the configured `startTime` offset to an encoded packet, per stream.
|
|
1850
2113
|
*
|
|
1851
2114
|
* `startTime` exists to strip a device's boot-relative timestamp base (e.g. the
|
|
1852
|
-
* mach uptime avfoundation reports). But
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
* players (QuickTime) reject. The
|
|
1856
|
-
*
|
|
2115
|
+
* mach uptime avfoundation reports). But not every stream of an output carries
|
|
2116
|
+
* that base (e.g. zero-based audio next to device video), so subtracting a large
|
|
2117
|
+
* device startTime from such a stream would push its timestamps hugely negative and
|
|
2118
|
+
* produce a broken/overflowed edit list that strict players (QuickTime) reject. The
|
|
2119
|
+
* effective offset is therefore decided once on the stream's first packet and
|
|
2120
|
+
* clamped to what the stream actually carries —
|
|
1857
2121
|
* `min(startTime, max(0, firstPts))`: boot-relative streams normalize to zero,
|
|
1858
2122
|
* already-zero-based streams are left untouched.
|
|
1859
2123
|
*
|
|
@@ -1947,8 +2211,14 @@ export class Muxer {
|
|
|
1947
2211
|
* Performs timestamp corrections:
|
|
1948
2212
|
* 1. Rescales timestamps to output timebase (av_rescale_delta for audio streamcopy)
|
|
1949
2213
|
* 2. Sets pkt.timeBase to output stream timebase
|
|
1950
|
-
* 3.
|
|
1951
|
-
* 4.
|
|
2214
|
+
* 3. Rejects backward jumps beyond dtsBackwardThreshold
|
|
2215
|
+
* 4. Fixes invalid DTS > PTS relationships
|
|
2216
|
+
* 5. Enforces monotonic DTS (never decreasing), shrinking a clamped packet's duration to the clamped step
|
|
2217
|
+
* 6. Rejects forward jumps that run ahead of real time beyond dtsForwardThreshold
|
|
2218
|
+
* 7. Rejects DTS steps/durations a movenc output cannot store, and a late stream's first packet before its start
|
|
2219
|
+
*
|
|
2220
|
+
* The stream's last muxed DTS only advances when the packet is accepted and
|
|
2221
|
+
* carries a DTS.
|
|
1952
2222
|
*
|
|
1953
2223
|
* @param pkt - Packet to fix
|
|
1954
2224
|
*
|
|
@@ -1956,69 +2226,332 @@ export class Muxer {
|
|
|
1956
2226
|
*
|
|
1957
2227
|
* @param streamIndex - Stream index
|
|
1958
2228
|
*
|
|
2229
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2230
|
+
*
|
|
2231
|
+
* @throws {Error} On a rejected timestamp discontinuity
|
|
2232
|
+
*
|
|
1959
2233
|
* @internal
|
|
1960
2234
|
*/
|
|
1961
|
-
muxFixupTs(pkt, streamInfo, streamIndex) {
|
|
1962
|
-
const outputStream =
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const
|
|
1966
|
-
const
|
|
1967
|
-
|
|
2235
|
+
muxFixupTs(pkt, streamInfo, streamIndex, arrival) {
|
|
2236
|
+
const outputStream = streamInfo.outputStream;
|
|
2237
|
+
// Both are final once the header is written, which happens before the
|
|
2238
|
+
// first packet gets here.
|
|
2239
|
+
const dstTb = (streamInfo.muxTimeBase ??= outputStream.timeBase);
|
|
2240
|
+
const codecType = (streamInfo.muxCodecType ??= outputStream.codecpar.codecType);
|
|
2241
|
+
const lastMuxDts = streamInfo.lastMuxDts;
|
|
2242
|
+
// Every timestamp getter is a native call returning a new BigInt, so each
|
|
2243
|
+
// value is read once, corrected in locals, and only changes are written back.
|
|
2244
|
+
let dts = pkt.dts;
|
|
2245
|
+
let pts = pkt.pts;
|
|
1968
2246
|
// Check if timestamps are valid before rescaling
|
|
1969
2247
|
// FFmpeg's av_rescale_q/av_rescale_delta don't accept AV_NOPTS_VALUE
|
|
1970
|
-
if (
|
|
2248
|
+
if (dts === AV_NOPTS_VALUE && pts === AV_NOPTS_VALUE) {
|
|
1971
2249
|
// Set packet timebase anyway for muxer
|
|
1972
2250
|
pkt.timeBase = dstTb;
|
|
1973
2251
|
return;
|
|
1974
2252
|
}
|
|
1975
2253
|
// 1. Rescale timestamps to the stream timebase
|
|
2254
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
1976
2255
|
if (codecType === AVMEDIA_TYPE_AUDIO && streamInfo.isStreamCopy) {
|
|
1977
|
-
|
|
2256
|
+
const codecpar = outputStream.codecpar;
|
|
2257
|
+
let duration = avGetAudioFrameDuration2(codecpar, pkt.size);
|
|
1978
2258
|
if (!duration) {
|
|
1979
|
-
duration =
|
|
2259
|
+
duration = codecpar.frameSize;
|
|
1980
2260
|
}
|
|
1981
|
-
const
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
pkt.dts =
|
|
1985
|
-
pkt.pts =
|
|
2261
|
+
const fsTb = { num: 1, den: codecpar.sampleRate };
|
|
2262
|
+
dts = avRescaleDelta(srcTb, dts, fsTb, duration, streamInfo.tsRescaleDeltaLast, dstTb);
|
|
2263
|
+
pts = dts;
|
|
2264
|
+
pkt.dts = dts;
|
|
2265
|
+
pkt.pts = pts;
|
|
1986
2266
|
pkt.duration = avRescaleQ(pkt.duration, srcTb, dstTb);
|
|
1987
2267
|
}
|
|
1988
2268
|
else {
|
|
1989
2269
|
// For video or encoded audio, use regular rescaling
|
|
1990
|
-
const srcTb = streamInfo.sourceTimeBase;
|
|
1991
2270
|
pkt.rescaleTs(srcTb, dstTb);
|
|
2271
|
+
dts = pkt.dts;
|
|
2272
|
+
pts = pkt.pts;
|
|
1992
2273
|
}
|
|
1993
2274
|
// 2. Set packet timeBase
|
|
1994
2275
|
// av_interleaved_write_frame uses this for sorting!
|
|
1995
2276
|
pkt.timeBase = dstTb;
|
|
1996
|
-
|
|
2277
|
+
const isAudioVideo = codecType === AVMEDIA_TYPE_AUDIO || codecType === AVMEDIA_TYPE_VIDEO;
|
|
2278
|
+
// 3. Reject a large backward jump while it is still visible - the corrections
|
|
2279
|
+
// below would turn it into a run of one-tick steps
|
|
2280
|
+
if (isAudioVideo) {
|
|
2281
|
+
this.checkBackwardDts(dts, lastMuxDts, dstTb, streamIndex);
|
|
2282
|
+
}
|
|
2283
|
+
// 4. Fix DTS > PTS (invalid relationship)
|
|
1997
2284
|
// FFmpeg formula: median of (pts, dts, last_mux_dts+1)
|
|
1998
|
-
if (
|
|
1999
|
-
const last =
|
|
2000
|
-
const min =
|
|
2001
|
-
const max =
|
|
2002
|
-
const median =
|
|
2285
|
+
if (dts !== AV_NOPTS_VALUE && pts !== AV_NOPTS_VALUE && dts > pts) {
|
|
2286
|
+
const last = lastMuxDts !== AV_NOPTS_VALUE ? lastMuxDts + 1n : 0n;
|
|
2287
|
+
const min = pts < dts ? (pts < last ? pts : last) : dts < last ? dts : last;
|
|
2288
|
+
const max = pts > dts ? (pts > last ? pts : last) : dts > last ? dts : last;
|
|
2289
|
+
const median = pts + dts + last - min - max;
|
|
2290
|
+
pts = median;
|
|
2291
|
+
dts = median;
|
|
2003
2292
|
pkt.pts = median;
|
|
2004
2293
|
pkt.dts = median;
|
|
2005
2294
|
}
|
|
2006
|
-
//
|
|
2007
|
-
|
|
2295
|
+
// 5. Enforce monotonic DTS
|
|
2296
|
+
let clampedDuration;
|
|
2297
|
+
if (isAudioVideo && dts !== AV_NOPTS_VALUE && lastMuxDts !== AV_NOPTS_VALUE) {
|
|
2008
2298
|
// FFmpeg: max = last_mux_dts + !(oformat->flags & AVFMT_TS_NONSTRICT)
|
|
2009
2299
|
// AVFMT_TS_NONSTRICT allows non-strict monotonic timestamps (equal DTS is OK)
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
if (pkt.dts < max) {
|
|
2300
|
+
const max = lastMuxDts + (this.tsNonStrict ? 0n : 1n);
|
|
2301
|
+
if (dts < max) {
|
|
2013
2302
|
// Adjust PTS if it would create invalid relationship
|
|
2014
|
-
if (
|
|
2015
|
-
|
|
2303
|
+
if (pts !== AV_NOPTS_VALUE && pts >= dts && pts < max) {
|
|
2304
|
+
pts = max;
|
|
2305
|
+
pkt.pts = max;
|
|
2016
2306
|
}
|
|
2307
|
+
dts = max;
|
|
2017
2308
|
pkt.dts = max;
|
|
2309
|
+
// The clamped packet must not keep its source duration: after a fragment
|
|
2310
|
+
// flush movenc continues the track at the previous DTS plus its duration,
|
|
2311
|
+
// which then lies ahead of the clamped timeline. movenc corrects that by
|
|
2312
|
+
// moving later DTS itself and can end with a negative sample duration
|
|
2313
|
+
// (av_assert0 in get_cluster_duration). A duration of 0 does not help -
|
|
2314
|
+
// mux.c guesses a new one - so use the clamped step.
|
|
2315
|
+
if (max > lastMuxDts) {
|
|
2316
|
+
clampedDuration = max - lastMuxDts;
|
|
2317
|
+
pkt.duration = clampedDuration;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
// mux.c derives a missing DTS from the PTS and never above it (its PTS-0
|
|
2322
|
+
// fallback aside, which only steps one frame), so the PTS stands in for the
|
|
2323
|
+
// DTS of such a packet in the checks below.
|
|
2324
|
+
const ts = dts !== AV_NOPTS_VALUE ? dts : pts;
|
|
2325
|
+
// 6. Reject a forward jump on the DTS libavformat would see, before the
|
|
2326
|
+
// packet enters it: once in, movenc takes the jump as the duration of the
|
|
2327
|
+
// stream's previous sample.
|
|
2328
|
+
let forwardOffset = NaN;
|
|
2329
|
+
if (isAudioVideo && this.forwardThresholdMs > 0) {
|
|
2330
|
+
forwardOffset = this.checkForwardDts(ts, arrival, streamInfo, streamIndex);
|
|
2331
|
+
}
|
|
2332
|
+
// 7. Reject what movenc cannot store before it reaches its abort
|
|
2333
|
+
if (this.movSampleLimit) {
|
|
2334
|
+
const duration = clampedDuration ?? pkt.duration;
|
|
2335
|
+
this.checkMovSampleDuration(ts, duration, lastMuxDts, dstTb, streamIndex);
|
|
2336
|
+
if (this.movZeroStart && streamInfo.firstMuxTs === undefined) {
|
|
2337
|
+
this.checkMovLateStart(ts, duration, dstTb, streamIndex);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
// 8. Update last mux DTS for next packet. mux.c fills a missing DTS itself
|
|
2341
|
+
// and rejects it unless it lies past the previous one, so the last known
|
|
2342
|
+
// DTS stays a valid reference: the clamp and the guards remain armed for
|
|
2343
|
+
// the next packet instead of letting it through unchecked.
|
|
2344
|
+
if (dts !== AV_NOPTS_VALUE) {
|
|
2345
|
+
streamInfo.lastMuxDts = dts;
|
|
2346
|
+
}
|
|
2347
|
+
streamInfo.firstMuxTs ??= ts;
|
|
2348
|
+
if (streamInfo.forward && !Number.isNaN(forwardOffset)) {
|
|
2349
|
+
this.recordForwardDts(streamInfo.forward, ts, forwardOffset, arrival);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Reject a packet whose DTS lies further behind the stream's last muxed DTS
|
|
2354
|
+
* than the configured dtsBackwardThreshold.
|
|
2355
|
+
*
|
|
2356
|
+
* Expects the DTS already rescaled to the output stream time base. No-op when
|
|
2357
|
+
* the threshold is unset or 0.
|
|
2358
|
+
*
|
|
2359
|
+
* @param dts - Packet DTS in the output time base
|
|
2360
|
+
*
|
|
2361
|
+
* @param lastMuxDts - The stream's last muxed DTS
|
|
2362
|
+
*
|
|
2363
|
+
* @param timeBase - Output stream time base
|
|
2364
|
+
*
|
|
2365
|
+
* @param streamIndex - Stream index
|
|
2366
|
+
*
|
|
2367
|
+
* @throws {Error} If the backward jump exceeds the threshold
|
|
2368
|
+
*
|
|
2369
|
+
* @internal
|
|
2370
|
+
*/
|
|
2371
|
+
checkBackwardDts(dts, lastMuxDts, timeBase, streamIndex) {
|
|
2372
|
+
const threshold = this.options.dtsBackwardThreshold;
|
|
2373
|
+
if (!threshold || dts === AV_NOPTS_VALUE || lastMuxDts === AV_NOPTS_VALUE || dts >= lastMuxDts) {
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const regression = lastMuxDts - dts;
|
|
2377
|
+
const thresholdUs = BigInt(Math.round(threshold * 1_000_000));
|
|
2378
|
+
if (avCompareTs(regression, timeBase, thresholdUs, AV_TIME_BASE_Q) <= 0) {
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const seconds = ((Number(regression) * timeBase.num) / timeBase.den).toFixed(3);
|
|
2382
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: DTS jumped back ${seconds}s, more than dtsBackwardThreshold (${threshold}s)`);
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Reject a packet whose DTS runs further ahead of real time than the
|
|
2386
|
+
* configured dtsForwardThreshold.
|
|
2387
|
+
*
|
|
2388
|
+
* Tracks per stream how real time and media time move apart (the offset) and
|
|
2389
|
+
* compares the packet's offset with the lowest one of the stream's recent
|
|
2390
|
+
* packets, see FORWARD_WINDOW_BUCKET. A gap in a live source leaves the
|
|
2391
|
+
* offset level, since the wait comes with it; a jump lowers it by the size
|
|
2392
|
+
* of the jump. The stream's first packet only sets the starting point.
|
|
2393
|
+
*
|
|
2394
|
+
* @param ts - Final packet DTS (PTS without DTS) in the output time base
|
|
2395
|
+
*
|
|
2396
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2397
|
+
*
|
|
2398
|
+
* @param streamInfo - Stream description
|
|
2399
|
+
*
|
|
2400
|
+
* @param streamIndex - Stream index
|
|
2401
|
+
*
|
|
2402
|
+
* @returns The packet's offset, for {@link recordForwardDts} once the packet is accepted
|
|
2403
|
+
*
|
|
2404
|
+
* @throws {Error} If the DTS runs ahead of real time by more than the threshold
|
|
2405
|
+
*
|
|
2406
|
+
* @internal
|
|
2407
|
+
*/
|
|
2408
|
+
checkForwardDts(ts, arrival, streamInfo, streamIndex) {
|
|
2409
|
+
const tb = streamInfo.muxTimeBase;
|
|
2410
|
+
const state = (streamInfo.forward ??= {
|
|
2411
|
+
msPerTick: (1000 * tb.num) / tb.den,
|
|
2412
|
+
lastTs: AV_NOPTS_VALUE,
|
|
2413
|
+
lastArrival: NaN,
|
|
2414
|
+
offset: 0,
|
|
2415
|
+
windowMin: Infinity,
|
|
2416
|
+
windowPrevMin: Infinity,
|
|
2417
|
+
windowCount: 0,
|
|
2418
|
+
});
|
|
2419
|
+
if (Number.isNaN(state.lastArrival)) {
|
|
2420
|
+
return 0;
|
|
2421
|
+
}
|
|
2422
|
+
const stepMs = Number(ts - state.lastTs) * state.msPerTick;
|
|
2423
|
+
const elapsedMs = arrival - state.lastArrival;
|
|
2424
|
+
const offset = state.offset + elapsedMs - stepMs;
|
|
2425
|
+
const reference = state.windowMin < state.windowPrevMin ? state.windowMin : state.windowPrevMin;
|
|
2426
|
+
if (reference - offset <= this.forwardThresholdMs) {
|
|
2427
|
+
return offset;
|
|
2428
|
+
}
|
|
2429
|
+
const jump = `DTS jumped forward ${(stepMs / 1000).toFixed(3)}s while ${(elapsedMs / 1000).toFixed(3)}s passed`;
|
|
2430
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${jump}, more than dtsForwardThreshold (${this.options.dtsForwardThreshold}s)`);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Make an accepted packet the reference of the stream's next forward check.
|
|
2434
|
+
*
|
|
2435
|
+
* @param state - The stream's forward check state
|
|
2436
|
+
*
|
|
2437
|
+
* @param ts - The packet's DTS (PTS without DTS) in the output time base
|
|
2438
|
+
*
|
|
2439
|
+
* @param offset - The packet's offset from {@link checkForwardDts}
|
|
2440
|
+
*
|
|
2441
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2442
|
+
*
|
|
2443
|
+
* @internal
|
|
2444
|
+
*/
|
|
2445
|
+
recordForwardDts(state, ts, offset, arrival) {
|
|
2446
|
+
state.lastTs = ts;
|
|
2447
|
+
state.offset = offset;
|
|
2448
|
+
state.lastArrival = arrival;
|
|
2449
|
+
if (offset < state.windowMin) {
|
|
2450
|
+
state.windowMin = offset;
|
|
2451
|
+
}
|
|
2452
|
+
// Two buckets keep the window between one and two buckets long in O(1).
|
|
2453
|
+
if (++state.windowCount >= FORWARD_WINDOW_BUCKET) {
|
|
2454
|
+
state.windowPrevMin = state.windowMin;
|
|
2455
|
+
state.windowMin = Infinity;
|
|
2456
|
+
state.windowCount = 0;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Reject a packet whose DTS step or duration a movenc output cannot store.
|
|
2461
|
+
*
|
|
2462
|
+
* Runs after the monotonic clamp, on the DTS movenc will see. Steps are
|
|
2463
|
+
* measured against the stream's last muxed DTS.
|
|
2464
|
+
*
|
|
2465
|
+
* @param dts - Final packet DTS, or its PTS for a packet without DTS
|
|
2466
|
+
*
|
|
2467
|
+
* @param duration - Final packet duration
|
|
2468
|
+
*
|
|
2469
|
+
* @param lastMuxDts - The stream's last muxed DTS
|
|
2470
|
+
*
|
|
2471
|
+
* @param timeBase - Output stream time base
|
|
2472
|
+
*
|
|
2473
|
+
* @param streamIndex - Stream index
|
|
2474
|
+
*
|
|
2475
|
+
* @throws {Error} If the step or duration reaches INT_MAX ticks
|
|
2476
|
+
*
|
|
2477
|
+
* @internal
|
|
2478
|
+
*/
|
|
2479
|
+
checkMovSampleDuration(dts, duration, lastMuxDts, timeBase, streamIndex) {
|
|
2480
|
+
const step = dts !== AV_NOPTS_VALUE && lastMuxDts !== AV_NOPTS_VALUE ? dts - lastMuxDts : 0n;
|
|
2481
|
+
const ticks = step >= MOV_SAMPLE_DURATION_LIMIT ? step : duration >= MOV_SAMPLE_DURATION_LIMIT ? duration : undefined;
|
|
2482
|
+
if (ticks === undefined) {
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const what = ticks === step ? 'DTS jumped forward' : 'packet duration is';
|
|
2486
|
+
const seconds = ((Number(ticks) * timeBase.num) / timeBase.den).toFixed(3);
|
|
2487
|
+
const format = this.formatContext.oformat?.name ?? 'mov';
|
|
2488
|
+
const detail = `${what} ${ticks} ticks (${seconds}s at time base ${timeBase.num}/${timeBase.den})`;
|
|
2489
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${detail}, but ${format} sample durations must stay below ${MOV_SAMPLE_DURATION_LIMIT} ticks`);
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Reject a stream's first packet that ends before the output's start.
|
|
2493
|
+
*
|
|
2494
|
+
* Without an edit list movenc puts each track's first sample at the output's
|
|
2495
|
+
* start, so a first packet that ends before it becomes a sample of negative
|
|
2496
|
+
* duration, and movenc aborts the process (av_assert0 in
|
|
2497
|
+
* get_cluster_duration) at the next fragment or the trailer unless a second
|
|
2498
|
+
* sample reaches it first. libavformat sets that start from the lowest DTS
|
|
2499
|
+
* it holds when it writes its first packet. It holds every packet until each
|
|
2500
|
+
* stream has sent one or its queue spans max_interleave_delta, so only a
|
|
2501
|
+
* stream that starts that much later than the others can land before it.
|
|
2502
|
+
*
|
|
2503
|
+
* @param ts - Packet DTS (PTS without DTS) in the output time base
|
|
2504
|
+
*
|
|
2505
|
+
* @param duration - Packet duration
|
|
2506
|
+
*
|
|
2507
|
+
* @param timeBase - Output stream time base
|
|
2508
|
+
*
|
|
2509
|
+
* @param streamIndex - Stream index
|
|
2510
|
+
*
|
|
2511
|
+
* @throws {Error} If the packet ends before a start libavformat may already have set
|
|
2512
|
+
*
|
|
2513
|
+
* @internal
|
|
2514
|
+
*/
|
|
2515
|
+
checkMovLateStart(ts, duration, timeBase, streamIndex) {
|
|
2516
|
+
if (this.interleaveDeltaUs <= 0n) {
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
// The other streams' earliest and latest timestamp so far, in µs
|
|
2520
|
+
let start;
|
|
2521
|
+
let startUs = 0n;
|
|
2522
|
+
let latestUs = 0n;
|
|
2523
|
+
for (const [index, other] of this._streams) {
|
|
2524
|
+
const first = other.firstMuxTs;
|
|
2525
|
+
const otherTb = other.muxTimeBase;
|
|
2526
|
+
if (first === undefined || !otherTb) {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const firstUs = avRescaleQ(first, otherTb, AV_TIME_BASE_Q);
|
|
2530
|
+
const lastUs = other.lastMuxDts !== AV_NOPTS_VALUE ? avRescaleQ(other.lastMuxDts, otherTb, AV_TIME_BASE_Q) : firstUs;
|
|
2531
|
+
if (!start) {
|
|
2532
|
+
latestUs = lastUs;
|
|
2533
|
+
}
|
|
2534
|
+
else if (lastUs > latestUs) {
|
|
2535
|
+
latestUs = lastUs;
|
|
2018
2536
|
}
|
|
2537
|
+
if (!start || firstUs < startUs) {
|
|
2538
|
+
start = { ts: first, timeBase: otherTb, index };
|
|
2539
|
+
startUs = firstUs;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
// Until its queue spans max_interleave_delta, libavformat has written nothing
|
|
2543
|
+
// and will start the output at this packet if it is the lowest.
|
|
2544
|
+
if (!start || latestUs - startUs <= this.interleaveDeltaUs) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const end = ts + (duration > 0n ? duration : 0n);
|
|
2548
|
+
if (avCompareTs(end, timeBase, start.ts, start.timeBase) >= 0) {
|
|
2549
|
+
return;
|
|
2019
2550
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2551
|
+
const seconds = (Number(startUs - avRescaleQ(end, timeBase, AV_TIME_BASE_Q)) / 1_000_000).toFixed(3);
|
|
2552
|
+
const format = this.formatContext.oformat?.name ?? 'mov';
|
|
2553
|
+
const detail = `its first packet ends ${seconds}s before the start of output stream ${start.index}`;
|
|
2554
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${detail}, which ${format} without an edit list cannot store`);
|
|
2022
2555
|
}
|
|
2023
2556
|
/**
|
|
2024
2557
|
* Copy container metadata from input to output.
|