node-av 6.2.0-beta.25 → 6.2.0-beta.26
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 +1 -1
- package/dist/api/audio-frame-buffer.d.ts +368 -22
- package/dist/api/audio-frame-buffer.js +687 -100
- package/dist/api/audio-frame-buffer.js.map +1 -1
- package/dist/api/demuxer.d.ts +7 -3
- package/dist/api/demuxer.js +19 -7
- package/dist/api/demuxer.js.map +1 -1
- package/dist/api/encoder.d.ts +40 -3
- package/dist/api/encoder.js +61 -23
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/fmp4-stream.d.ts +103 -3
- package/dist/api/fmp4-stream.js +151 -28
- package/dist/api/fmp4-stream.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.js +21 -6
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/muxer.d.ts +275 -7
- package/dist/api/muxer.js +569 -62
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +19 -2
- package/dist/api/rtp-stream.js +62 -15
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/api/scaler.d.ts +19 -4
- package/dist/api/scaler.js +21 -6
- package/dist/api/scaler.js.map +1 -1
- 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/lib/binding.d.ts +9 -1
- package/dist/lib/binding.js +13 -0
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/format-context.d.ts +7 -0
- package/dist/lib/format-context.js +7 -0
- 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/native-types.d.ts +3 -1
- package/package.json +12 -12
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
|
*
|
|
@@ -81,12 +110,30 @@ export class Muxer {
|
|
|
81
110
|
writeWorkerPromise; // Background worker promise
|
|
82
111
|
writeWorkerError; // First error from the write worker - poisons subsequent writes and close()
|
|
83
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)
|
|
84
121
|
/**
|
|
85
122
|
* @param options - Media output options
|
|
86
123
|
*
|
|
124
|
+
* @throws {RangeError} If dtsBackwardThreshold or dtsForwardThreshold is not a finite number >= 0
|
|
125
|
+
*
|
|
87
126
|
* @internal
|
|
88
127
|
*/
|
|
89
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
|
+
}
|
|
90
137
|
this.options = {
|
|
91
138
|
copyInitialNonkeyframes: false,
|
|
92
139
|
exitOnError: true,
|
|
@@ -94,6 +141,7 @@ export class Muxer {
|
|
|
94
141
|
useAsyncWrite: true,
|
|
95
142
|
...options,
|
|
96
143
|
};
|
|
144
|
+
this.forwardThresholdMs = (options?.dtsForwardThreshold ?? 0) * 1000;
|
|
97
145
|
this.formatContext = new FormatContext();
|
|
98
146
|
}
|
|
99
147
|
static async open(target, options) {
|
|
@@ -486,6 +534,8 @@ export class Muxer {
|
|
|
486
534
|
isStreamCopy: true,
|
|
487
535
|
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
488
536
|
preMuxQueue: [],
|
|
537
|
+
preMuxArrivals: [],
|
|
538
|
+
sqArrivals: [],
|
|
489
539
|
preMuxQueueDataSize: 0,
|
|
490
540
|
eofReceived: false,
|
|
491
541
|
lastMuxDts: AV_NOPTS_VALUE,
|
|
@@ -507,6 +557,8 @@ export class Muxer {
|
|
|
507
557
|
isStreamCopy: false,
|
|
508
558
|
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
509
559
|
preMuxQueue: [],
|
|
560
|
+
preMuxArrivals: [],
|
|
561
|
+
sqArrivals: [],
|
|
510
562
|
preMuxQueueDataSize: 0,
|
|
511
563
|
eofReceived: false,
|
|
512
564
|
lastMuxDts: AV_NOPTS_VALUE,
|
|
@@ -664,6 +716,9 @@ export class Muxer {
|
|
|
664
716
|
*
|
|
665
717
|
* @throws {Error} If stream invalid or encoder not initialized
|
|
666
718
|
*
|
|
719
|
+
* @throws {Error} On a timestamp discontinuity the output cannot take (see {@link MuxerOptions.dtsBackwardThreshold}).
|
|
720
|
+
* With the background write queue (more than one stream) it surfaces from a later writePacket() or close().
|
|
721
|
+
*
|
|
667
722
|
* @throws {FFmpegError} If write fails
|
|
668
723
|
*
|
|
669
724
|
* @example
|
|
@@ -787,6 +842,7 @@ export class Muxer {
|
|
|
787
842
|
if (uninitialized || this.headerWritePromise) {
|
|
788
843
|
// Buffer NULL as EOF marker (no size contribution)
|
|
789
844
|
streamInfo.preMuxQueue.push(null);
|
|
845
|
+
streamInfo.preMuxArrivals.push(0);
|
|
790
846
|
return;
|
|
791
847
|
}
|
|
792
848
|
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
@@ -819,7 +875,7 @@ export class Muxer {
|
|
|
819
875
|
throw new Error('Failed to clone packet from sync queue');
|
|
820
876
|
}
|
|
821
877
|
pkt.streamIndex = recvRet;
|
|
822
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
878
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
823
879
|
}
|
|
824
880
|
}
|
|
825
881
|
}
|
|
@@ -830,6 +886,10 @@ export class Muxer {
|
|
|
830
886
|
if (!clonedPacket) {
|
|
831
887
|
throw new Error('Failed to clone packet for writing');
|
|
832
888
|
}
|
|
889
|
+
// The forward check compares DTS steps with the time between packets as
|
|
890
|
+
// they reach the muxer. Taken here and carried through the pre-mux, sync
|
|
891
|
+
// and write queues, so time a packet spends queued does not count.
|
|
892
|
+
const arrival = this.forwardThresholdMs > 0 ? this.clock() : 0;
|
|
833
893
|
// Apply streamcopy filtering BEFORE buffering
|
|
834
894
|
// This ensures rejected packets never enter the queue/buffer
|
|
835
895
|
if (streamInfo.isStreamCopy) {
|
|
@@ -843,6 +903,9 @@ export class Muxer {
|
|
|
843
903
|
// For encoded (non-streamcopy) streams, strip the device's startTime base.
|
|
844
904
|
this.applyStartTimeOffset(clonedPacket, streamInfo);
|
|
845
905
|
}
|
|
906
|
+
// Announced on acceptance, so the observer also counts what waits in the
|
|
907
|
+
// pre-mux queue for the header.
|
|
908
|
+
const observed = this.announcePacket(clonedPacket, streamIndex);
|
|
846
909
|
// Check if any streams are still uninitialized or header is being written
|
|
847
910
|
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
848
911
|
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
@@ -860,12 +923,16 @@ export class Muxer {
|
|
|
860
923
|
// Check if we would exceed packet limit (only if threshold reached)
|
|
861
924
|
if (currentPackets >= effectiveMaxPackets) {
|
|
862
925
|
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
926
|
+
if (observed) {
|
|
927
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
928
|
+
}
|
|
863
929
|
throw new Error(
|
|
864
930
|
// eslint-disable-next-line @stylistic/max-len
|
|
865
931
|
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
866
932
|
}
|
|
867
933
|
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
868
934
|
streamInfo.preMuxQueue.push(clonedPacket);
|
|
935
|
+
streamInfo.preMuxArrivals.push(arrival);
|
|
869
936
|
streamInfo.preMuxQueueDataSize += packetSize;
|
|
870
937
|
return; // Don't proceed to header write yet
|
|
871
938
|
}
|
|
@@ -877,9 +944,16 @@ export class Muxer {
|
|
|
877
944
|
const ret = await this.formatContext.writeHeader();
|
|
878
945
|
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
879
946
|
this.headerWritten = true;
|
|
947
|
+
this.readHeaderSetup();
|
|
880
948
|
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
881
949
|
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
882
|
-
|
|
950
|
+
// Concurrent writers keep queueing while headerWritePromise is set, also
|
|
951
|
+
// after a flush found the queues empty. Drain until they are empty and
|
|
952
|
+
// clear the promise in that same tick, or such packets are never written.
|
|
953
|
+
do {
|
|
954
|
+
await this.flushPreMuxQueues();
|
|
955
|
+
} while (this.hasPreMuxPackets());
|
|
956
|
+
this.headerWritePromise = undefined;
|
|
883
957
|
})();
|
|
884
958
|
await this.headerWritePromise;
|
|
885
959
|
if (this.headerWritten) {
|
|
@@ -895,6 +969,9 @@ export class Muxer {
|
|
|
895
969
|
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
896
970
|
// Handle errors from sq_send
|
|
897
971
|
if (ret < 0) {
|
|
972
|
+
if (observed) {
|
|
973
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
974
|
+
}
|
|
898
975
|
if (ret === AVERROR_EOF) {
|
|
899
976
|
// Stream finished - this is normal, just return
|
|
900
977
|
return;
|
|
@@ -904,6 +981,8 @@ export class Muxer {
|
|
|
904
981
|
}
|
|
905
982
|
return;
|
|
906
983
|
}
|
|
984
|
+
// The sync queue keeps each stream's packets in order and drops none.
|
|
985
|
+
streamInfo.sqArrivals.push(arrival);
|
|
907
986
|
// Receive synchronized packets from queue and write to muxer
|
|
908
987
|
while (!this.isClosed) {
|
|
909
988
|
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
@@ -924,14 +1003,14 @@ export class Muxer {
|
|
|
924
1003
|
}
|
|
925
1004
|
pkt.streamIndex = recvRet;
|
|
926
1005
|
// Write packet (muxer takes ownership)
|
|
927
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
1006
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
928
1007
|
}
|
|
929
1008
|
}
|
|
930
1009
|
}
|
|
931
1010
|
else {
|
|
932
1011
|
// No sync queue needed - write directly
|
|
933
1012
|
clonedPacket.streamIndex = streamIndex;
|
|
934
|
-
await this.write(clonedPacket, streamInfo, streamIndex);
|
|
1013
|
+
await this.write(clonedPacket, streamInfo, streamIndex, arrival);
|
|
935
1014
|
}
|
|
936
1015
|
}
|
|
937
1016
|
/**
|
|
@@ -964,6 +1043,8 @@ export class Muxer {
|
|
|
964
1043
|
*
|
|
965
1044
|
* @throws {Error} If stream invalid or encoder not initialized
|
|
966
1045
|
*
|
|
1046
|
+
* @throws {Error} On a timestamp discontinuity the output cannot take (see {@link MuxerOptions.dtsBackwardThreshold})
|
|
1047
|
+
*
|
|
967
1048
|
* @throws {FFmpegError} If write fails
|
|
968
1049
|
*
|
|
969
1050
|
* @example
|
|
@@ -1091,6 +1172,7 @@ export class Muxer {
|
|
|
1091
1172
|
if (uninitialized || this.headerWritePromise) {
|
|
1092
1173
|
// Buffer NULL as EOF marker (no size contribution)
|
|
1093
1174
|
streamInfo.preMuxQueue.push(null);
|
|
1175
|
+
streamInfo.preMuxArrivals.push(0);
|
|
1094
1176
|
return;
|
|
1095
1177
|
}
|
|
1096
1178
|
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
@@ -1123,7 +1205,7 @@ export class Muxer {
|
|
|
1123
1205
|
throw new Error('Failed to clone packet from sync queue');
|
|
1124
1206
|
}
|
|
1125
1207
|
pkt.streamIndex = recvRet;
|
|
1126
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1208
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1127
1209
|
}
|
|
1128
1210
|
}
|
|
1129
1211
|
}
|
|
@@ -1134,6 +1216,7 @@ export class Muxer {
|
|
|
1134
1216
|
if (!clonedPacket) {
|
|
1135
1217
|
throw new Error('Failed to clone packet for writing');
|
|
1136
1218
|
}
|
|
1219
|
+
const arrival = this.forwardThresholdMs > 0 ? this.clock() : 0;
|
|
1137
1220
|
// Apply streamcopy filtering BEFORE buffering
|
|
1138
1221
|
// This ensures rejected packets never enter the queue/buffer
|
|
1139
1222
|
if (streamInfo.isStreamCopy) {
|
|
@@ -1147,6 +1230,9 @@ export class Muxer {
|
|
|
1147
1230
|
// For encoded (non-streamcopy) streams, strip the device's startTime base.
|
|
1148
1231
|
this.applyStartTimeOffset(clonedPacket, streamInfo);
|
|
1149
1232
|
}
|
|
1233
|
+
// Announced on acceptance, so the observer also counts what waits in the
|
|
1234
|
+
// pre-mux queue for the header.
|
|
1235
|
+
const observed = this.announcePacket(clonedPacket, streamIndex);
|
|
1150
1236
|
// Check if any streams are still uninitialized
|
|
1151
1237
|
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
1152
1238
|
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
@@ -1164,12 +1250,16 @@ export class Muxer {
|
|
|
1164
1250
|
// Check if we would exceed packet limit (only if threshold reached)
|
|
1165
1251
|
if (currentPackets >= effectiveMaxPackets) {
|
|
1166
1252
|
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
1253
|
+
if (observed) {
|
|
1254
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
1255
|
+
}
|
|
1167
1256
|
throw new Error(
|
|
1168
1257
|
// eslint-disable-next-line @stylistic/max-len
|
|
1169
1258
|
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
1170
1259
|
}
|
|
1171
1260
|
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
1172
1261
|
streamInfo.preMuxQueue.push(clonedPacket);
|
|
1262
|
+
streamInfo.preMuxArrivals.push(arrival);
|
|
1173
1263
|
streamInfo.preMuxQueueDataSize += packetSize;
|
|
1174
1264
|
return; // Don't proceed to header write yet
|
|
1175
1265
|
}
|
|
@@ -1179,6 +1269,7 @@ export class Muxer {
|
|
|
1179
1269
|
const ret = this.formatContext.writeHeaderSync();
|
|
1180
1270
|
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
1181
1271
|
this.headerWritten = true;
|
|
1272
|
+
this.readHeaderSetup();
|
|
1182
1273
|
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
1183
1274
|
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
1184
1275
|
this.flushPreMuxQueuesSync();
|
|
@@ -1192,6 +1283,9 @@ export class Muxer {
|
|
|
1192
1283
|
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
1193
1284
|
// Handle errors from sq_send
|
|
1194
1285
|
if (ret < 0) {
|
|
1286
|
+
if (observed) {
|
|
1287
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
1288
|
+
}
|
|
1195
1289
|
if (ret === AVERROR_EOF) {
|
|
1196
1290
|
// Stream finished - this is normal, just return
|
|
1197
1291
|
return;
|
|
@@ -1201,6 +1295,7 @@ export class Muxer {
|
|
|
1201
1295
|
}
|
|
1202
1296
|
return;
|
|
1203
1297
|
}
|
|
1298
|
+
streamInfo.sqArrivals.push(arrival);
|
|
1204
1299
|
// Receive synchronized packets from queue and write to muxer
|
|
1205
1300
|
while (!this.isClosed) {
|
|
1206
1301
|
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
@@ -1221,14 +1316,14 @@ export class Muxer {
|
|
|
1221
1316
|
}
|
|
1222
1317
|
pkt.streamIndex = recvRet;
|
|
1223
1318
|
// Write packet (muxer takes ownership)
|
|
1224
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1319
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1225
1320
|
}
|
|
1226
1321
|
}
|
|
1227
1322
|
}
|
|
1228
1323
|
else {
|
|
1229
1324
|
// No sync queue needed - write directly
|
|
1230
1325
|
clonedPacket.streamIndex = streamIndex;
|
|
1231
|
-
this.writeSync(clonedPacket, streamInfo, streamIndex);
|
|
1326
|
+
this.writeSync(clonedPacket, streamInfo, streamIndex, arrival);
|
|
1232
1327
|
}
|
|
1233
1328
|
}
|
|
1234
1329
|
/**
|
|
@@ -1258,6 +1353,8 @@ export class Muxer {
|
|
|
1258
1353
|
return;
|
|
1259
1354
|
}
|
|
1260
1355
|
this.isClosed = true;
|
|
1356
|
+
// Detached first, so the final flush can never fail on the observer.
|
|
1357
|
+
this.packetObserver = undefined;
|
|
1261
1358
|
// A header write launched by a concurrent writePacket() must settle before
|
|
1262
1359
|
// teardown: nulling pb / freeing the format context under a running
|
|
1263
1360
|
// avformat_write_header() crashes on the worker thread. This happens when a
|
|
@@ -1292,6 +1389,7 @@ export class Muxer {
|
|
|
1292
1389
|
pkt?.free();
|
|
1293
1390
|
}
|
|
1294
1391
|
streamInfo.preMuxQueue = [];
|
|
1392
|
+
streamInfo.preMuxArrivals = [];
|
|
1295
1393
|
}
|
|
1296
1394
|
// Free sync queue resources
|
|
1297
1395
|
if (this.sqPacket) {
|
|
@@ -1304,7 +1402,7 @@ export class Muxer {
|
|
|
1304
1402
|
}
|
|
1305
1403
|
// Try to write trailer if header was written but trailer wasn't
|
|
1306
1404
|
try {
|
|
1307
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
1405
|
+
if (this.headerWritten && !this.trailerWritten && !this.trailerDiscarded) {
|
|
1308
1406
|
await this.formatContext.writeTrailer();
|
|
1309
1407
|
this.trailerWritten = true;
|
|
1310
1408
|
}
|
|
@@ -1378,6 +1476,7 @@ export class Muxer {
|
|
|
1378
1476
|
return;
|
|
1379
1477
|
}
|
|
1380
1478
|
this.isClosed = true;
|
|
1479
|
+
this.packetObserver = undefined;
|
|
1381
1480
|
// Free PreMuxQueue packets
|
|
1382
1481
|
for (const streamInfo of this._streams.values()) {
|
|
1383
1482
|
// Free any packets in PreMuxQueue
|
|
@@ -1385,6 +1484,7 @@ export class Muxer {
|
|
|
1385
1484
|
pkt?.free();
|
|
1386
1485
|
}
|
|
1387
1486
|
streamInfo.preMuxQueue = [];
|
|
1487
|
+
streamInfo.preMuxArrivals = [];
|
|
1388
1488
|
}
|
|
1389
1489
|
// Free sync queue resources
|
|
1390
1490
|
if (this.sqPacket) {
|
|
@@ -1397,7 +1497,7 @@ export class Muxer {
|
|
|
1397
1497
|
}
|
|
1398
1498
|
// Try to write trailer if header was written but trailer wasn't
|
|
1399
1499
|
try {
|
|
1400
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
1500
|
+
if (this.headerWritten && !this.trailerWritten && !this.trailerDiscarded) {
|
|
1401
1501
|
this.formatContext.writeTrailerSync();
|
|
1402
1502
|
this.trailerWritten = true;
|
|
1403
1503
|
}
|
|
@@ -1450,6 +1550,73 @@ export class Muxer {
|
|
|
1450
1550
|
getFormatContext() {
|
|
1451
1551
|
return this.formatContext;
|
|
1452
1552
|
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Follow the packets this muxer accepts for writing.
|
|
1555
|
+
*
|
|
1556
|
+
* The observer hears about every packet with payload once it is accepted
|
|
1557
|
+
* (before it waits in the pre-mux queue or reaches libavformat) and about
|
|
1558
|
+
* every such packet that is dropped later, so together with the output it
|
|
1559
|
+
* knows what the muxer holds. close() detaches it.
|
|
1560
|
+
*
|
|
1561
|
+
* @param observer - Observer, or undefined to detach
|
|
1562
|
+
*
|
|
1563
|
+
* @internal
|
|
1564
|
+
*/
|
|
1565
|
+
setPacketObserver(observer) {
|
|
1566
|
+
this.packetObserver = observer;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Replace the clock that stamps packet arrival times for the dtsForwardThreshold check.
|
|
1570
|
+
*
|
|
1571
|
+
* Lets tests drive real time deterministically. Set it before the first packet.
|
|
1572
|
+
*
|
|
1573
|
+
* @param clock - Monotonic time in ms
|
|
1574
|
+
*
|
|
1575
|
+
* @internal
|
|
1576
|
+
*/
|
|
1577
|
+
setClock(clock) {
|
|
1578
|
+
this.clock = clock;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Make close() skip the trailer.
|
|
1582
|
+
*
|
|
1583
|
+
* For owners that drop whatever the muxer still emits, such as FMP4Stream
|
|
1584
|
+
* after a failed session: the trailer would only push libavformat's backlog
|
|
1585
|
+
* into the void, and movenc asserts on some track states a failed session
|
|
1586
|
+
* can leave behind. The backlog is freed with the format context instead.
|
|
1587
|
+
*
|
|
1588
|
+
* @internal
|
|
1589
|
+
*/
|
|
1590
|
+
discardOnClose() {
|
|
1591
|
+
this.trailerDiscarded = true;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Announce a packet the muxer has accepted to the packet observer.
|
|
1595
|
+
*
|
|
1596
|
+
* @param pkt - The muxer's clone of the packet
|
|
1597
|
+
*
|
|
1598
|
+
* @param streamIndex - Output stream index
|
|
1599
|
+
*
|
|
1600
|
+
* @returns True when the observer now tracks the packet
|
|
1601
|
+
*
|
|
1602
|
+
* @throws {Error} If the observer fails the write (the clone is freed)
|
|
1603
|
+
*
|
|
1604
|
+
* @internal
|
|
1605
|
+
*/
|
|
1606
|
+
announcePacket(pkt, streamIndex) {
|
|
1607
|
+
// movenc drops packets without payload instead of storing a sample.
|
|
1608
|
+
if (this.packetObserver === undefined || pkt.size <= 0) {
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
try {
|
|
1612
|
+
this.packetObserver.onPacket(streamIndex);
|
|
1613
|
+
}
|
|
1614
|
+
catch (error) {
|
|
1615
|
+
pkt.free();
|
|
1616
|
+
throw error;
|
|
1617
|
+
}
|
|
1618
|
+
return true;
|
|
1619
|
+
}
|
|
1453
1620
|
/**
|
|
1454
1621
|
* Apply shared pre-header setup for writePacket and writePacketSync.
|
|
1455
1622
|
*
|
|
@@ -1465,6 +1632,26 @@ export class Muxer {
|
|
|
1465
1632
|
this.copyContainerMetadata();
|
|
1466
1633
|
applyContextOptions(this.formatContext, this.options.context);
|
|
1467
1634
|
this.options.configure?.(this.formatContext);
|
|
1635
|
+
const oformat = this.formatContext.oformat;
|
|
1636
|
+
this.movSampleLimit = MOV_FAMILY_FORMATS.has(oformat?.name ?? '');
|
|
1637
|
+
this.tsNonStrict = oformat?.hasFlags(AVFMT_TS_NONSTRICT) ?? false;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Read the muxing setup libavformat settles while writing the header.
|
|
1641
|
+
*
|
|
1642
|
+
* movenc decides on edit lists only then: without one (fragmented output
|
|
1643
|
+
* without delay_moov) it shifts the output to start at 0 and puts each
|
|
1644
|
+
* track's first sample there, which {@link checkMovLateStart} guards.
|
|
1645
|
+
*
|
|
1646
|
+
* @internal
|
|
1647
|
+
*/
|
|
1648
|
+
readHeaderSetup() {
|
|
1649
|
+
if (!this.movSampleLimit) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const ctx = this.formatContext;
|
|
1653
|
+
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;
|
|
1654
|
+
this.interleaveDeltaUs = ctx.maxInterleaveDelta;
|
|
1468
1655
|
}
|
|
1469
1656
|
/**
|
|
1470
1657
|
* Setup sync queues based on stream configuration.
|
|
@@ -1510,6 +1697,21 @@ export class Muxer {
|
|
|
1510
1697
|
}
|
|
1511
1698
|
}
|
|
1512
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Whether any stream still holds packets (or an EOF marker) in its pre-mux queue.
|
|
1702
|
+
*
|
|
1703
|
+
* @returns True if a pre-mux queue is not empty
|
|
1704
|
+
*
|
|
1705
|
+
* @internal
|
|
1706
|
+
*/
|
|
1707
|
+
hasPreMuxPackets() {
|
|
1708
|
+
for (const streamInfo of this._streams.values()) {
|
|
1709
|
+
if (streamInfo.preMuxQueue.length > 0) {
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1513
1715
|
/**
|
|
1514
1716
|
* Flush all PreMuxQueues in DTS-sorted order.
|
|
1515
1717
|
*
|
|
@@ -1555,6 +1757,7 @@ export class Muxer {
|
|
|
1555
1757
|
}
|
|
1556
1758
|
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1557
1759
|
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1760
|
+
const arrival = minStreamInfo.preMuxArrivals.shift() ?? 0;
|
|
1558
1761
|
// 4. Handle NULL packet (EOF marker)
|
|
1559
1762
|
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1560
1763
|
if (!pkt) {
|
|
@@ -1579,8 +1782,15 @@ export class Muxer {
|
|
|
1579
1782
|
// NOTE: Do NOT set pkt.timeBase here!
|
|
1580
1783
|
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1581
1784
|
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1785
|
+
const size = pkt.size;
|
|
1582
1786
|
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1583
|
-
if (ret < 0 &&
|
|
1787
|
+
if (ret < 0 && size > 0) {
|
|
1788
|
+
this.packetObserver?.onPacketRejected(minStreamIndex);
|
|
1789
|
+
}
|
|
1790
|
+
if (ret >= 0) {
|
|
1791
|
+
minStreamInfo.sqArrivals.push(arrival);
|
|
1792
|
+
}
|
|
1793
|
+
else if (ret !== AVERROR_EOF) {
|
|
1584
1794
|
if (this.options.exitOnError) {
|
|
1585
1795
|
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1586
1796
|
}
|
|
@@ -1588,7 +1798,7 @@ export class Muxer {
|
|
|
1588
1798
|
}
|
|
1589
1799
|
else {
|
|
1590
1800
|
// Write directly to muxer
|
|
1591
|
-
await this.write(pkt, minStreamInfo, minStreamIndex);
|
|
1801
|
+
await this.write(pkt, minStreamInfo, minStreamIndex, arrival);
|
|
1592
1802
|
}
|
|
1593
1803
|
}
|
|
1594
1804
|
// If using SyncQueue, receive and write all interleaved packets
|
|
@@ -1611,7 +1821,7 @@ export class Muxer {
|
|
|
1611
1821
|
}
|
|
1612
1822
|
pkt.streamIndex = recvRet;
|
|
1613
1823
|
// Write packet (muxer takes ownership)
|
|
1614
|
-
await this.write(pkt, recvStreamInfo, recvRet);
|
|
1824
|
+
await this.write(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1615
1825
|
}
|
|
1616
1826
|
}
|
|
1617
1827
|
}
|
|
@@ -1659,6 +1869,7 @@ export class Muxer {
|
|
|
1659
1869
|
break;
|
|
1660
1870
|
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1661
1871
|
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1872
|
+
const arrival = minStreamInfo.preMuxArrivals.shift() ?? 0;
|
|
1662
1873
|
// 4. Handle NULL packet (EOF marker)
|
|
1663
1874
|
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1664
1875
|
if (!pkt) {
|
|
@@ -1683,8 +1894,15 @@ export class Muxer {
|
|
|
1683
1894
|
// NOTE: Do NOT set pkt.timeBase here!
|
|
1684
1895
|
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1685
1896
|
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1897
|
+
const size = pkt.size;
|
|
1686
1898
|
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1687
|
-
if (ret < 0 &&
|
|
1899
|
+
if (ret < 0 && size > 0) {
|
|
1900
|
+
this.packetObserver?.onPacketRejected(minStreamIndex);
|
|
1901
|
+
}
|
|
1902
|
+
if (ret >= 0) {
|
|
1903
|
+
minStreamInfo.sqArrivals.push(arrival);
|
|
1904
|
+
}
|
|
1905
|
+
else if (ret !== AVERROR_EOF) {
|
|
1688
1906
|
if (this.options.exitOnError) {
|
|
1689
1907
|
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1690
1908
|
}
|
|
@@ -1692,7 +1910,7 @@ export class Muxer {
|
|
|
1692
1910
|
}
|
|
1693
1911
|
else {
|
|
1694
1912
|
// Write directly to muxer
|
|
1695
|
-
this.writeSync(pkt, minStreamInfo, minStreamIndex);
|
|
1913
|
+
this.writeSync(pkt, minStreamInfo, minStreamIndex, arrival);
|
|
1696
1914
|
}
|
|
1697
1915
|
}
|
|
1698
1916
|
// If using SyncQueue, receive and write all interleaved packets
|
|
@@ -1715,7 +1933,7 @@ export class Muxer {
|
|
|
1715
1933
|
}
|
|
1716
1934
|
pkt.streamIndex = recvRet;
|
|
1717
1935
|
// Write packet (muxer takes ownership)
|
|
1718
|
-
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1936
|
+
this.writeSync(pkt, recvStreamInfo, recvRet, recvStreamInfo.sqArrivals.shift() ?? 0);
|
|
1719
1937
|
}
|
|
1720
1938
|
}
|
|
1721
1939
|
}
|
|
@@ -1729,15 +1947,17 @@ export class Muxer {
|
|
|
1729
1947
|
*
|
|
1730
1948
|
* @param streamIndex - Stream index
|
|
1731
1949
|
*
|
|
1950
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
1951
|
+
*
|
|
1732
1952
|
* @internal
|
|
1733
1953
|
*/
|
|
1734
|
-
async write(pkt, streamInfo, streamIndex) {
|
|
1954
|
+
async write(pkt, streamInfo, streamIndex, arrival) {
|
|
1735
1955
|
if (this.writeQueue) {
|
|
1736
1956
|
// Use async queue for serialized writes.
|
|
1737
1957
|
// If the worker died with a write error, the queue is poisoned and send()
|
|
1738
1958
|
// rethrows that error - free the clone since it never reaches the worker.
|
|
1739
1959
|
try {
|
|
1740
|
-
await this.writeQueue.send({ pkt, streamInfo, streamIndex });
|
|
1960
|
+
await this.writeQueue.send({ pkt, streamInfo, streamIndex, arrival });
|
|
1741
1961
|
}
|
|
1742
1962
|
catch (error) {
|
|
1743
1963
|
pkt.free();
|
|
@@ -1746,7 +1966,7 @@ export class Muxer {
|
|
|
1746
1966
|
}
|
|
1747
1967
|
else {
|
|
1748
1968
|
// Direct write without serialization
|
|
1749
|
-
await this.writeInternal(pkt, streamInfo, streamIndex);
|
|
1969
|
+
await this.writeInternal(pkt, streamInfo, streamIndex, arrival);
|
|
1750
1970
|
}
|
|
1751
1971
|
}
|
|
1752
1972
|
/**
|
|
@@ -1759,15 +1979,21 @@ export class Muxer {
|
|
|
1759
1979
|
*
|
|
1760
1980
|
* @param streamIndex - Stream index
|
|
1761
1981
|
*
|
|
1982
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
1983
|
+
*
|
|
1762
1984
|
* @internal
|
|
1763
1985
|
*/
|
|
1764
|
-
async writeInternal(pkt, streamInfo, streamIndex) {
|
|
1986
|
+
async writeInternal(pkt, streamInfo, streamIndex, arrival) {
|
|
1987
|
+
// Read before the write moves the payload out (see announcePacket()).
|
|
1988
|
+
const observed = this.packetObserver !== undefined && pkt.size > 0;
|
|
1989
|
+
let accepted = false;
|
|
1765
1990
|
try {
|
|
1766
1991
|
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
1767
|
-
this.muxFixupTs(pkt, streamInfo, streamIndex);
|
|
1992
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex, arrival);
|
|
1768
1993
|
// Write the packet (muxer takes ownership and will unref it)
|
|
1769
1994
|
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
1770
1995
|
const ret = await this.formatContext.interleavedWriteFrame(pkt);
|
|
1996
|
+
accepted = ret >= 0;
|
|
1771
1997
|
// Handle write errors
|
|
1772
1998
|
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1773
1999
|
if (this.options.exitOnError) {
|
|
@@ -1776,6 +2002,9 @@ export class Muxer {
|
|
|
1776
2002
|
}
|
|
1777
2003
|
}
|
|
1778
2004
|
finally {
|
|
2005
|
+
if (observed && !accepted) {
|
|
2006
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
2007
|
+
}
|
|
1779
2008
|
// Every packet reaching here is a clone this class made, and the write
|
|
1780
2009
|
// has consumed its contents. Release the struct now instead of leaving a
|
|
1781
2010
|
// GC-sized backlog of them behind - this is the last owner.
|
|
@@ -1799,7 +2028,7 @@ export class Muxer {
|
|
|
1799
2028
|
const job = await this.writeQueue.receive();
|
|
1800
2029
|
if (!job)
|
|
1801
2030
|
break; // Queue closed
|
|
1802
|
-
await this.writeInternal(job.pkt, job.streamInfo, job.streamIndex);
|
|
2031
|
+
await this.writeInternal(job.pkt, job.streamInfo, job.streamIndex, job.arrival);
|
|
1803
2032
|
}
|
|
1804
2033
|
}
|
|
1805
2034
|
catch (error) {
|
|
@@ -1824,18 +2053,26 @@ export class Muxer {
|
|
|
1824
2053
|
*
|
|
1825
2054
|
* @param streamIndex - Stream index
|
|
1826
2055
|
*
|
|
2056
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2057
|
+
*
|
|
1827
2058
|
* @internal
|
|
1828
2059
|
*/
|
|
1829
|
-
writeSync(pkt, streamInfo, streamIndex) {
|
|
2060
|
+
writeSync(pkt, streamInfo, streamIndex, arrival) {
|
|
2061
|
+
const observed = this.packetObserver !== undefined && pkt.size > 0;
|
|
2062
|
+
let accepted = false;
|
|
1830
2063
|
try {
|
|
1831
2064
|
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
1832
|
-
this.muxFixupTs(pkt, streamInfo, streamIndex);
|
|
2065
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex, arrival);
|
|
1833
2066
|
// Write the packet (muxer takes ownership and will unref it)
|
|
1834
2067
|
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
1835
2068
|
const ret = this.formatContext.interleavedWriteFrameSync(pkt);
|
|
2069
|
+
accepted = ret >= 0;
|
|
1836
2070
|
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
1837
2071
|
}
|
|
1838
2072
|
finally {
|
|
2073
|
+
if (observed && !accepted) {
|
|
2074
|
+
this.packetObserver?.onPacketRejected(streamIndex);
|
|
2075
|
+
}
|
|
1839
2076
|
// See writeInternal(): this is the last owner of the clone.
|
|
1840
2077
|
pkt.free();
|
|
1841
2078
|
}
|
|
@@ -1866,11 +2103,12 @@ export class Muxer {
|
|
|
1866
2103
|
* Apply the configured `startTime` offset to an encoded packet, per stream.
|
|
1867
2104
|
*
|
|
1868
2105
|
* `startTime` exists to strip a device's boot-relative timestamp base (e.g. the
|
|
1869
|
-
* mach uptime avfoundation reports). But
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
* players (QuickTime) reject. The
|
|
1873
|
-
*
|
|
2106
|
+
* mach uptime avfoundation reports). But not every stream of an output carries
|
|
2107
|
+
* that base (e.g. zero-based audio next to device video), so subtracting a large
|
|
2108
|
+
* device startTime from such a stream would push its timestamps hugely negative and
|
|
2109
|
+
* produce a broken/overflowed edit list that strict players (QuickTime) reject. The
|
|
2110
|
+
* effective offset is therefore decided once on the stream's first packet and
|
|
2111
|
+
* clamped to what the stream actually carries —
|
|
1874
2112
|
* `min(startTime, max(0, firstPts))`: boot-relative streams normalize to zero,
|
|
1875
2113
|
* already-zero-based streams are left untouched.
|
|
1876
2114
|
*
|
|
@@ -1964,8 +2202,14 @@ export class Muxer {
|
|
|
1964
2202
|
* Performs timestamp corrections:
|
|
1965
2203
|
* 1. Rescales timestamps to output timebase (av_rescale_delta for audio streamcopy)
|
|
1966
2204
|
* 2. Sets pkt.timeBase to output stream timebase
|
|
1967
|
-
* 3.
|
|
1968
|
-
* 4.
|
|
2205
|
+
* 3. Rejects backward jumps beyond dtsBackwardThreshold
|
|
2206
|
+
* 4. Fixes invalid DTS > PTS relationships
|
|
2207
|
+
* 5. Enforces monotonic DTS (never decreasing), shrinking a clamped packet's duration to the clamped step
|
|
2208
|
+
* 6. Rejects forward jumps that run ahead of real time beyond dtsForwardThreshold
|
|
2209
|
+
* 7. Rejects DTS steps/durations a movenc output cannot store, and a late stream's first packet before its start
|
|
2210
|
+
*
|
|
2211
|
+
* The stream's last muxed DTS only advances when the packet is accepted and
|
|
2212
|
+
* carries a DTS.
|
|
1969
2213
|
*
|
|
1970
2214
|
* @param pkt - Packet to fix
|
|
1971
2215
|
*
|
|
@@ -1973,69 +2217,332 @@ export class Muxer {
|
|
|
1973
2217
|
*
|
|
1974
2218
|
* @param streamIndex - Stream index
|
|
1975
2219
|
*
|
|
2220
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2221
|
+
*
|
|
2222
|
+
* @throws {Error} On a rejected timestamp discontinuity
|
|
2223
|
+
*
|
|
1976
2224
|
* @internal
|
|
1977
2225
|
*/
|
|
1978
|
-
muxFixupTs(pkt, streamInfo, streamIndex) {
|
|
1979
|
-
const outputStream =
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
const
|
|
1984
|
-
|
|
2226
|
+
muxFixupTs(pkt, streamInfo, streamIndex, arrival) {
|
|
2227
|
+
const outputStream = streamInfo.outputStream;
|
|
2228
|
+
// Both are final once the header is written, which happens before the
|
|
2229
|
+
// first packet gets here.
|
|
2230
|
+
const dstTb = (streamInfo.muxTimeBase ??= outputStream.timeBase);
|
|
2231
|
+
const codecType = (streamInfo.muxCodecType ??= outputStream.codecpar.codecType);
|
|
2232
|
+
const lastMuxDts = streamInfo.lastMuxDts;
|
|
2233
|
+
// Every timestamp getter is a native call returning a new BigInt, so each
|
|
2234
|
+
// value is read once, corrected in locals, and only changes are written back.
|
|
2235
|
+
let dts = pkt.dts;
|
|
2236
|
+
let pts = pkt.pts;
|
|
1985
2237
|
// Check if timestamps are valid before rescaling
|
|
1986
2238
|
// FFmpeg's av_rescale_q/av_rescale_delta don't accept AV_NOPTS_VALUE
|
|
1987
|
-
if (
|
|
2239
|
+
if (dts === AV_NOPTS_VALUE && pts === AV_NOPTS_VALUE) {
|
|
1988
2240
|
// Set packet timebase anyway for muxer
|
|
1989
2241
|
pkt.timeBase = dstTb;
|
|
1990
2242
|
return;
|
|
1991
2243
|
}
|
|
1992
2244
|
// 1. Rescale timestamps to the stream timebase
|
|
2245
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
1993
2246
|
if (codecType === AVMEDIA_TYPE_AUDIO && streamInfo.isStreamCopy) {
|
|
1994
|
-
|
|
2247
|
+
const codecpar = outputStream.codecpar;
|
|
2248
|
+
let duration = avGetAudioFrameDuration2(codecpar, pkt.size);
|
|
1995
2249
|
if (!duration) {
|
|
1996
|
-
duration =
|
|
2250
|
+
duration = codecpar.frameSize;
|
|
1997
2251
|
}
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
pkt.dts =
|
|
2002
|
-
pkt.pts =
|
|
2252
|
+
const fsTb = { num: 1, den: codecpar.sampleRate };
|
|
2253
|
+
dts = avRescaleDelta(srcTb, dts, fsTb, duration, streamInfo.tsRescaleDeltaLast, dstTb);
|
|
2254
|
+
pts = dts;
|
|
2255
|
+
pkt.dts = dts;
|
|
2256
|
+
pkt.pts = pts;
|
|
2003
2257
|
pkt.duration = avRescaleQ(pkt.duration, srcTb, dstTb);
|
|
2004
2258
|
}
|
|
2005
2259
|
else {
|
|
2006
2260
|
// For video or encoded audio, use regular rescaling
|
|
2007
|
-
const srcTb = streamInfo.sourceTimeBase;
|
|
2008
2261
|
pkt.rescaleTs(srcTb, dstTb);
|
|
2262
|
+
dts = pkt.dts;
|
|
2263
|
+
pts = pkt.pts;
|
|
2009
2264
|
}
|
|
2010
2265
|
// 2. Set packet timeBase
|
|
2011
2266
|
// av_interleaved_write_frame uses this for sorting!
|
|
2012
2267
|
pkt.timeBase = dstTb;
|
|
2013
|
-
|
|
2268
|
+
const isAudioVideo = codecType === AVMEDIA_TYPE_AUDIO || codecType === AVMEDIA_TYPE_VIDEO;
|
|
2269
|
+
// 3. Reject a large backward jump while it is still visible - the corrections
|
|
2270
|
+
// below would turn it into a run of one-tick steps
|
|
2271
|
+
if (isAudioVideo) {
|
|
2272
|
+
this.checkBackwardDts(dts, lastMuxDts, dstTb, streamIndex);
|
|
2273
|
+
}
|
|
2274
|
+
// 4. Fix DTS > PTS (invalid relationship)
|
|
2014
2275
|
// FFmpeg formula: median of (pts, dts, last_mux_dts+1)
|
|
2015
|
-
if (
|
|
2016
|
-
const last =
|
|
2017
|
-
const min =
|
|
2018
|
-
const max =
|
|
2019
|
-
const median =
|
|
2276
|
+
if (dts !== AV_NOPTS_VALUE && pts !== AV_NOPTS_VALUE && dts > pts) {
|
|
2277
|
+
const last = lastMuxDts !== AV_NOPTS_VALUE ? lastMuxDts + 1n : 0n;
|
|
2278
|
+
const min = pts < dts ? (pts < last ? pts : last) : dts < last ? dts : last;
|
|
2279
|
+
const max = pts > dts ? (pts > last ? pts : last) : dts > last ? dts : last;
|
|
2280
|
+
const median = pts + dts + last - min - max;
|
|
2281
|
+
pts = median;
|
|
2282
|
+
dts = median;
|
|
2020
2283
|
pkt.pts = median;
|
|
2021
2284
|
pkt.dts = median;
|
|
2022
2285
|
}
|
|
2023
|
-
//
|
|
2024
|
-
|
|
2286
|
+
// 5. Enforce monotonic DTS
|
|
2287
|
+
let clampedDuration;
|
|
2288
|
+
if (isAudioVideo && dts !== AV_NOPTS_VALUE && lastMuxDts !== AV_NOPTS_VALUE) {
|
|
2025
2289
|
// FFmpeg: max = last_mux_dts + !(oformat->flags & AVFMT_TS_NONSTRICT)
|
|
2026
2290
|
// AVFMT_TS_NONSTRICT allows non-strict monotonic timestamps (equal DTS is OK)
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
if (pkt.dts < max) {
|
|
2291
|
+
const max = lastMuxDts + (this.tsNonStrict ? 0n : 1n);
|
|
2292
|
+
if (dts < max) {
|
|
2030
2293
|
// Adjust PTS if it would create invalid relationship
|
|
2031
|
-
if (
|
|
2032
|
-
|
|
2294
|
+
if (pts !== AV_NOPTS_VALUE && pts >= dts && pts < max) {
|
|
2295
|
+
pts = max;
|
|
2296
|
+
pkt.pts = max;
|
|
2033
2297
|
}
|
|
2298
|
+
dts = max;
|
|
2034
2299
|
pkt.dts = max;
|
|
2300
|
+
// The clamped packet must not keep its source duration: after a fragment
|
|
2301
|
+
// flush movenc continues the track at the previous DTS plus its duration,
|
|
2302
|
+
// which then lies ahead of the clamped timeline. movenc corrects that by
|
|
2303
|
+
// moving later DTS itself and can end with a negative sample duration
|
|
2304
|
+
// (av_assert0 in get_cluster_duration). A duration of 0 does not help -
|
|
2305
|
+
// mux.c guesses a new one - so use the clamped step.
|
|
2306
|
+
if (max > lastMuxDts) {
|
|
2307
|
+
clampedDuration = max - lastMuxDts;
|
|
2308
|
+
pkt.duration = clampedDuration;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
// mux.c derives a missing DTS from the PTS and never above it (its PTS-0
|
|
2313
|
+
// fallback aside, which only steps one frame), so the PTS stands in for the
|
|
2314
|
+
// DTS of such a packet in the checks below.
|
|
2315
|
+
const ts = dts !== AV_NOPTS_VALUE ? dts : pts;
|
|
2316
|
+
// 6. Reject a forward jump on the DTS libavformat would see, before the
|
|
2317
|
+
// packet enters it: once in, movenc takes the jump as the duration of the
|
|
2318
|
+
// stream's previous sample.
|
|
2319
|
+
let forwardOffset = NaN;
|
|
2320
|
+
if (isAudioVideo && this.forwardThresholdMs > 0) {
|
|
2321
|
+
forwardOffset = this.checkForwardDts(ts, arrival, streamInfo, streamIndex);
|
|
2322
|
+
}
|
|
2323
|
+
// 7. Reject what movenc cannot store before it reaches its abort
|
|
2324
|
+
if (this.movSampleLimit) {
|
|
2325
|
+
const duration = clampedDuration ?? pkt.duration;
|
|
2326
|
+
this.checkMovSampleDuration(ts, duration, lastMuxDts, dstTb, streamIndex);
|
|
2327
|
+
if (this.movZeroStart && streamInfo.firstMuxTs === undefined) {
|
|
2328
|
+
this.checkMovLateStart(ts, duration, dstTb, streamIndex);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
// 8. Update last mux DTS for next packet. mux.c fills a missing DTS itself
|
|
2332
|
+
// and rejects it unless it lies past the previous one, so the last known
|
|
2333
|
+
// DTS stays a valid reference: the clamp and the guards remain armed for
|
|
2334
|
+
// the next packet instead of letting it through unchecked.
|
|
2335
|
+
if (dts !== AV_NOPTS_VALUE) {
|
|
2336
|
+
streamInfo.lastMuxDts = dts;
|
|
2337
|
+
}
|
|
2338
|
+
streamInfo.firstMuxTs ??= ts;
|
|
2339
|
+
if (streamInfo.forward && !Number.isNaN(forwardOffset)) {
|
|
2340
|
+
this.recordForwardDts(streamInfo.forward, ts, forwardOffset, arrival);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Reject a packet whose DTS lies further behind the stream's last muxed DTS
|
|
2345
|
+
* than the configured dtsBackwardThreshold.
|
|
2346
|
+
*
|
|
2347
|
+
* Expects the DTS already rescaled to the output stream time base. No-op when
|
|
2348
|
+
* the threshold is unset or 0.
|
|
2349
|
+
*
|
|
2350
|
+
* @param dts - Packet DTS in the output time base
|
|
2351
|
+
*
|
|
2352
|
+
* @param lastMuxDts - The stream's last muxed DTS
|
|
2353
|
+
*
|
|
2354
|
+
* @param timeBase - Output stream time base
|
|
2355
|
+
*
|
|
2356
|
+
* @param streamIndex - Stream index
|
|
2357
|
+
*
|
|
2358
|
+
* @throws {Error} If the backward jump exceeds the threshold
|
|
2359
|
+
*
|
|
2360
|
+
* @internal
|
|
2361
|
+
*/
|
|
2362
|
+
checkBackwardDts(dts, lastMuxDts, timeBase, streamIndex) {
|
|
2363
|
+
const threshold = this.options.dtsBackwardThreshold;
|
|
2364
|
+
if (!threshold || dts === AV_NOPTS_VALUE || lastMuxDts === AV_NOPTS_VALUE || dts >= lastMuxDts) {
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const regression = lastMuxDts - dts;
|
|
2368
|
+
const thresholdUs = BigInt(Math.round(threshold * 1_000_000));
|
|
2369
|
+
if (avCompareTs(regression, timeBase, thresholdUs, AV_TIME_BASE_Q) <= 0) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const seconds = ((Number(regression) * timeBase.num) / timeBase.den).toFixed(3);
|
|
2373
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: DTS jumped back ${seconds}s, more than dtsBackwardThreshold (${threshold}s)`);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Reject a packet whose DTS runs further ahead of real time than the
|
|
2377
|
+
* configured dtsForwardThreshold.
|
|
2378
|
+
*
|
|
2379
|
+
* Tracks per stream how real time and media time move apart (the offset) and
|
|
2380
|
+
* compares the packet's offset with the lowest one of the stream's recent
|
|
2381
|
+
* packets, see FORWARD_WINDOW_BUCKET. A gap in a live source leaves the
|
|
2382
|
+
* offset level, since the wait comes with it; a jump lowers it by the size
|
|
2383
|
+
* of the jump. The stream's first packet only sets the starting point.
|
|
2384
|
+
*
|
|
2385
|
+
* @param ts - Final packet DTS (PTS without DTS) in the output time base
|
|
2386
|
+
*
|
|
2387
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2388
|
+
*
|
|
2389
|
+
* @param streamInfo - Stream description
|
|
2390
|
+
*
|
|
2391
|
+
* @param streamIndex - Stream index
|
|
2392
|
+
*
|
|
2393
|
+
* @returns The packet's offset, for {@link recordForwardDts} once the packet is accepted
|
|
2394
|
+
*
|
|
2395
|
+
* @throws {Error} If the DTS runs ahead of real time by more than the threshold
|
|
2396
|
+
*
|
|
2397
|
+
* @internal
|
|
2398
|
+
*/
|
|
2399
|
+
checkForwardDts(ts, arrival, streamInfo, streamIndex) {
|
|
2400
|
+
const tb = streamInfo.muxTimeBase;
|
|
2401
|
+
const state = (streamInfo.forward ??= {
|
|
2402
|
+
msPerTick: (1000 * tb.num) / tb.den,
|
|
2403
|
+
lastTs: AV_NOPTS_VALUE,
|
|
2404
|
+
lastArrival: NaN,
|
|
2405
|
+
offset: 0,
|
|
2406
|
+
windowMin: Infinity,
|
|
2407
|
+
windowPrevMin: Infinity,
|
|
2408
|
+
windowCount: 0,
|
|
2409
|
+
});
|
|
2410
|
+
if (Number.isNaN(state.lastArrival)) {
|
|
2411
|
+
return 0;
|
|
2412
|
+
}
|
|
2413
|
+
const stepMs = Number(ts - state.lastTs) * state.msPerTick;
|
|
2414
|
+
const elapsedMs = arrival - state.lastArrival;
|
|
2415
|
+
const offset = state.offset + elapsedMs - stepMs;
|
|
2416
|
+
const reference = state.windowMin < state.windowPrevMin ? state.windowMin : state.windowPrevMin;
|
|
2417
|
+
if (reference - offset <= this.forwardThresholdMs) {
|
|
2418
|
+
return offset;
|
|
2419
|
+
}
|
|
2420
|
+
const jump = `DTS jumped forward ${(stepMs / 1000).toFixed(3)}s while ${(elapsedMs / 1000).toFixed(3)}s passed`;
|
|
2421
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${jump}, more than dtsForwardThreshold (${this.options.dtsForwardThreshold}s)`);
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Make an accepted packet the reference of the stream's next forward check.
|
|
2425
|
+
*
|
|
2426
|
+
* @param state - The stream's forward check state
|
|
2427
|
+
*
|
|
2428
|
+
* @param ts - The packet's DTS (PTS without DTS) in the output time base
|
|
2429
|
+
*
|
|
2430
|
+
* @param offset - The packet's offset from {@link checkForwardDts}
|
|
2431
|
+
*
|
|
2432
|
+
* @param arrival - When the packet reached the muxer (ms on the arrival clock)
|
|
2433
|
+
*
|
|
2434
|
+
* @internal
|
|
2435
|
+
*/
|
|
2436
|
+
recordForwardDts(state, ts, offset, arrival) {
|
|
2437
|
+
state.lastTs = ts;
|
|
2438
|
+
state.offset = offset;
|
|
2439
|
+
state.lastArrival = arrival;
|
|
2440
|
+
if (offset < state.windowMin) {
|
|
2441
|
+
state.windowMin = offset;
|
|
2442
|
+
}
|
|
2443
|
+
// Two buckets keep the window between one and two buckets long in O(1).
|
|
2444
|
+
if (++state.windowCount >= FORWARD_WINDOW_BUCKET) {
|
|
2445
|
+
state.windowPrevMin = state.windowMin;
|
|
2446
|
+
state.windowMin = Infinity;
|
|
2447
|
+
state.windowCount = 0;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Reject a packet whose DTS step or duration a movenc output cannot store.
|
|
2452
|
+
*
|
|
2453
|
+
* Runs after the monotonic clamp, on the DTS movenc will see. Steps are
|
|
2454
|
+
* measured against the stream's last muxed DTS.
|
|
2455
|
+
*
|
|
2456
|
+
* @param dts - Final packet DTS, or its PTS for a packet without DTS
|
|
2457
|
+
*
|
|
2458
|
+
* @param duration - Final packet duration
|
|
2459
|
+
*
|
|
2460
|
+
* @param lastMuxDts - The stream's last muxed DTS
|
|
2461
|
+
*
|
|
2462
|
+
* @param timeBase - Output stream time base
|
|
2463
|
+
*
|
|
2464
|
+
* @param streamIndex - Stream index
|
|
2465
|
+
*
|
|
2466
|
+
* @throws {Error} If the step or duration reaches INT_MAX ticks
|
|
2467
|
+
*
|
|
2468
|
+
* @internal
|
|
2469
|
+
*/
|
|
2470
|
+
checkMovSampleDuration(dts, duration, lastMuxDts, timeBase, streamIndex) {
|
|
2471
|
+
const step = dts !== AV_NOPTS_VALUE && lastMuxDts !== AV_NOPTS_VALUE ? dts - lastMuxDts : 0n;
|
|
2472
|
+
const ticks = step >= MOV_SAMPLE_DURATION_LIMIT ? step : duration >= MOV_SAMPLE_DURATION_LIMIT ? duration : undefined;
|
|
2473
|
+
if (ticks === undefined) {
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const what = ticks === step ? 'DTS jumped forward' : 'packet duration is';
|
|
2477
|
+
const seconds = ((Number(ticks) * timeBase.num) / timeBase.den).toFixed(3);
|
|
2478
|
+
const format = this.formatContext.oformat?.name ?? 'mov';
|
|
2479
|
+
const detail = `${what} ${ticks} ticks (${seconds}s at time base ${timeBase.num}/${timeBase.den})`;
|
|
2480
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${detail}, but ${format} sample durations must stay below ${MOV_SAMPLE_DURATION_LIMIT} ticks`);
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Reject a stream's first packet that ends before the output's start.
|
|
2484
|
+
*
|
|
2485
|
+
* Without an edit list movenc puts each track's first sample at the output's
|
|
2486
|
+
* start, so a first packet that ends before it becomes a sample of negative
|
|
2487
|
+
* duration, and movenc aborts the process (av_assert0 in
|
|
2488
|
+
* get_cluster_duration) at the next fragment or the trailer unless a second
|
|
2489
|
+
* sample reaches it first. libavformat sets that start from the lowest DTS
|
|
2490
|
+
* it holds when it writes its first packet. It holds every packet until each
|
|
2491
|
+
* stream has sent one or its queue spans max_interleave_delta, so only a
|
|
2492
|
+
* stream that starts that much later than the others can land before it.
|
|
2493
|
+
*
|
|
2494
|
+
* @param ts - Packet DTS (PTS without DTS) in the output time base
|
|
2495
|
+
*
|
|
2496
|
+
* @param duration - Packet duration
|
|
2497
|
+
*
|
|
2498
|
+
* @param timeBase - Output stream time base
|
|
2499
|
+
*
|
|
2500
|
+
* @param streamIndex - Stream index
|
|
2501
|
+
*
|
|
2502
|
+
* @throws {Error} If the packet ends before a start libavformat may already have set
|
|
2503
|
+
*
|
|
2504
|
+
* @internal
|
|
2505
|
+
*/
|
|
2506
|
+
checkMovLateStart(ts, duration, timeBase, streamIndex) {
|
|
2507
|
+
if (this.interleaveDeltaUs <= 0n) {
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
// The other streams' earliest and latest timestamp so far, in µs
|
|
2511
|
+
let start;
|
|
2512
|
+
let startUs = 0n;
|
|
2513
|
+
let latestUs = 0n;
|
|
2514
|
+
for (const [index, other] of this._streams) {
|
|
2515
|
+
const first = other.firstMuxTs;
|
|
2516
|
+
const otherTb = other.muxTimeBase;
|
|
2517
|
+
if (first === undefined || !otherTb) {
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
const firstUs = avRescaleQ(first, otherTb, AV_TIME_BASE_Q);
|
|
2521
|
+
const lastUs = other.lastMuxDts !== AV_NOPTS_VALUE ? avRescaleQ(other.lastMuxDts, otherTb, AV_TIME_BASE_Q) : firstUs;
|
|
2522
|
+
if (!start) {
|
|
2523
|
+
latestUs = lastUs;
|
|
2524
|
+
}
|
|
2525
|
+
else if (lastUs > latestUs) {
|
|
2526
|
+
latestUs = lastUs;
|
|
2035
2527
|
}
|
|
2528
|
+
if (!start || firstUs < startUs) {
|
|
2529
|
+
start = { ts: first, timeBase: otherTb, index };
|
|
2530
|
+
startUs = firstUs;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
// Until its queue spans max_interleave_delta, libavformat has written nothing
|
|
2534
|
+
// and will start the output at this packet if it is the lowest.
|
|
2535
|
+
if (!start || latestUs - startUs <= this.interleaveDeltaUs) {
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
const end = ts + (duration > 0n ? duration : 0n);
|
|
2539
|
+
if (avCompareTs(end, timeBase, start.ts, start.timeBase) >= 0) {
|
|
2540
|
+
return;
|
|
2036
2541
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2542
|
+
const seconds = (Number(startUs - avRescaleQ(end, timeBase, AV_TIME_BASE_Q)) / 1_000_000).toFixed(3);
|
|
2543
|
+
const format = this.formatContext.oformat?.name ?? 'mov';
|
|
2544
|
+
const detail = `its first packet ends ${seconds}s before the start of output stream ${start.index}`;
|
|
2545
|
+
throw new Error(`Timestamp discontinuity on output stream ${streamIndex}: ${detail}, which ${format} without an edit list cannot store`);
|
|
2039
2546
|
}
|
|
2040
2547
|
/**
|
|
2041
2548
|
* Copy container metadata from input to output.
|