node-av 3.1.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -52
- package/binding.gyp +4 -0
- package/dist/api/audio-frame-buffer.d.ts +201 -0
- package/dist/api/audio-frame-buffer.js +275 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +319 -78
- package/dist/api/bitstream-filter.js +680 -151
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +279 -17
- package/dist/api/decoder.js +998 -209
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
- package/dist/api/demuxer.js +1968 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/encoder.d.ts +308 -50
- package/dist/api/encoder.js +1133 -111
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +12 -5
- package/dist/api/filter-presets.js +21 -7
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +406 -40
- package/dist/api/filter.js +966 -139
- package/dist/api/filter.js.map +1 -1
- package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
- package/dist/api/fmp4-stream.js +539 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +58 -6
- package/dist/api/hardware.js +127 -11
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +6 -4
- package/dist/api/index.js +14 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +3 -3
- package/dist/api/io-stream.js +5 -4
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
- package/dist/api/muxer.js +1934 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +77 -29
- package/dist/api/pipeline.js +435 -425
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/rtp-stream.d.ts +312 -0
- package/dist/api/rtp-stream.js +630 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/types.d.ts +476 -55
- package/dist/api/utilities/async-queue.d.ts +91 -0
- package/dist/api/utilities/async-queue.js +162 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +1 -1
- package/dist/api/utilities/image.d.ts +1 -1
- package/dist/api/utilities/index.d.ts +2 -0
- package/dist/api/utilities/index.js +4 -0
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +1 -1
- package/dist/api/utilities/pixel-format.d.ts +1 -1
- package/dist/api/utilities/sample-format.d.ts +1 -1
- package/dist/api/utilities/scheduler.d.ts +169 -0
- package/dist/api/utilities/scheduler.js +136 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +74 -15
- package/dist/api/utilities/streaming.js +170 -12
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +1 -1
- package/dist/api/webrtc-stream.d.ts +288 -0
- package/dist/api/webrtc-stream.js +440 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/constants/constants.d.ts +51 -1
- package/dist/constants/constants.js +47 -1
- package/dist/constants/constants.js.map +1 -1
- package/dist/constants/encoders.d.ts +2 -1
- package/dist/constants/encoders.js +4 -3
- package/dist/constants/encoders.js.map +1 -1
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/lib/binding.d.ts +19 -8
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/codec-context.d.ts +87 -0
- package/dist/lib/codec-context.js +125 -4
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/codec-parameters.d.ts +183 -1
- package/dist/lib/codec-parameters.js +209 -0
- package/dist/lib/codec-parameters.js.map +1 -1
- package/dist/lib/codec-parser.d.ts +23 -0
- package/dist/lib/codec-parser.js +25 -0
- package/dist/lib/codec-parser.js.map +1 -1
- package/dist/lib/codec.d.ts +26 -4
- package/dist/lib/codec.js +35 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.js +1 -0
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.js +1 -1
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/filter-context.d.ts +52 -11
- package/dist/lib/filter-context.js +56 -12
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +9 -0
- package/dist/lib/filter-graph.js +13 -0
- package/dist/lib/filter-graph.js.map +1 -1
- package/dist/lib/filter.d.ts +21 -0
- package/dist/lib/filter.js +28 -0
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +48 -14
- package/dist/lib/format-context.js +76 -7
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +168 -0
- package/dist/lib/frame.js +212 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +3 -2
- package/dist/lib/hardware-device-context.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/input-format.d.ts +21 -0
- package/dist/lib/input-format.js +42 -2
- package/dist/lib/input-format.js.map +1 -1
- package/dist/lib/native-types.d.ts +48 -26
- package/dist/lib/option.d.ts +25 -13
- package/dist/lib/option.js +28 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +22 -1
- package/dist/lib/output-format.js +28 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/packet.d.ts +35 -0
- package/dist/lib/packet.js +52 -2
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/stream.d.ts +126 -0
- package/dist/lib/stream.js +188 -5
- package/dist/lib/stream.js.map +1 -1
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/utilities.d.ts +281 -53
- package/dist/lib/utilities.js +298 -55
- package/dist/lib/utilities.js.map +1 -1
- package/install/check.js +18 -7
- package/package.json +20 -19
- package/dist/api/fmp4.js +0 -710
- package/dist/api/fmp4.js.map +0 -1
- package/dist/api/media-input.js +0 -1075
- package/dist/api/media-input.js.map +0 -1
- package/dist/api/media-output.js +0 -1040
- package/dist/api/media-output.js.map +0 -1
- package/dist/api/webrtc.d.ts +0 -664
- package/dist/api/webrtc.js +0 -1132
- package/dist/api/webrtc.js.map +0 -1
|
@@ -0,0 +1,1934 @@
|
|
|
1
|
+
import { mkdirSync } from 'fs';
|
|
2
|
+
import { mkdir } from 'fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
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 { Dictionary } from '../lib/dictionary.js';
|
|
6
|
+
import { FFmpegError } from '../lib/error.js';
|
|
7
|
+
import { FormatContext } from '../lib/format-context.js';
|
|
8
|
+
import { IOContext } from '../lib/io-context.js';
|
|
9
|
+
import { Packet } from '../lib/packet.js';
|
|
10
|
+
import { Rational } from '../lib/rational.js';
|
|
11
|
+
import { SyncQueue, SyncQueueType } from '../lib/sync-queue.js';
|
|
12
|
+
import { avAddQ, avCompareTs, avGetAudioFrameDuration2, avRescaleDelta, avRescaleQ } from '../lib/utilities.js';
|
|
13
|
+
import { IO_BUFFER_SIZE, MAX_MUXING_QUEUE_SIZE, MAX_PACKET_SIZE, MUXING_QUEUE_DATA_THRESHOLD, SYNC_BUFFER_DURATION } from './constants.js';
|
|
14
|
+
import { Encoder } from './encoder.js';
|
|
15
|
+
import { AsyncQueue } from './utilities/async-queue.js';
|
|
16
|
+
/**
|
|
17
|
+
* High-level muxer for writing and muxing media files.
|
|
18
|
+
*
|
|
19
|
+
* Provides simplified access to media muxing and file writing operations.
|
|
20
|
+
* Automatically manages header and trailer writing - header is written on first packet,
|
|
21
|
+
* trailer is written on close. Supports lazy initialization for both encoders and streams.
|
|
22
|
+
* Handles stream configuration, packet writing, and format management.
|
|
23
|
+
* Supports files, URLs, and custom I/O with automatic cleanup.
|
|
24
|
+
* Essential component for media encoding pipelines and transcoding.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { Muxer } from 'node-av/api';
|
|
29
|
+
*
|
|
30
|
+
* // Create output file
|
|
31
|
+
* await using output = await Muxer.open('output.mp4');
|
|
32
|
+
*
|
|
33
|
+
* // Add streams from encoders
|
|
34
|
+
* const videoIdx = output.addStream(videoEncoder);
|
|
35
|
+
* const audioIdx = output.addStream(audioEncoder);
|
|
36
|
+
*
|
|
37
|
+
* // Write packets - header written automatically on first packet
|
|
38
|
+
* await output.writePacket(packet, videoIdx);
|
|
39
|
+
*
|
|
40
|
+
* // Close - trailer written automatically
|
|
41
|
+
* // (automatic with await using)
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // Stream copy
|
|
47
|
+
* await using input = await Demuxer.open('input.mp4');
|
|
48
|
+
* await using output = await Muxer.open('output.mp4');
|
|
49
|
+
*
|
|
50
|
+
* // Copy stream configuration
|
|
51
|
+
* const videoIdx = output.addStream(input.video());
|
|
52
|
+
*
|
|
53
|
+
* // Process packets - header/trailer handled automatically
|
|
54
|
+
* for await (const packet of input.packets()) {
|
|
55
|
+
* await output.writePacket(packet, videoIdx);
|
|
56
|
+
* packet.free();
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @see {@link Demuxer} For reading media files
|
|
61
|
+
* @see {@link Encoder} For encoding frames to packets
|
|
62
|
+
* @see {@link FormatContext} For low-level API
|
|
63
|
+
*/
|
|
64
|
+
export class Muxer {
|
|
65
|
+
formatContext;
|
|
66
|
+
options;
|
|
67
|
+
_streams = new Map();
|
|
68
|
+
ioContext;
|
|
69
|
+
headerWritten = false;
|
|
70
|
+
headerWritePromise;
|
|
71
|
+
trailerWritten = false;
|
|
72
|
+
isClosed = false;
|
|
73
|
+
syncQueue; // FFmpeg's native sync queue for packet interleaving
|
|
74
|
+
sqPacket; // Reusable packet for sync queue receive
|
|
75
|
+
containerMetadataCopied = false; // Track if container metadata has been copied
|
|
76
|
+
writeQueue; // Optional async queue for serialized writes
|
|
77
|
+
writeWorkerPromise; // Background worker promise
|
|
78
|
+
/**
|
|
79
|
+
* @param options - Media output options
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.options = {
|
|
85
|
+
copyInitialNonkeyframes: false,
|
|
86
|
+
exitOnError: true,
|
|
87
|
+
useSyncQueue: true,
|
|
88
|
+
useAsyncWrite: true,
|
|
89
|
+
...options,
|
|
90
|
+
};
|
|
91
|
+
this.formatContext = new FormatContext();
|
|
92
|
+
}
|
|
93
|
+
static async open(target, options) {
|
|
94
|
+
const output = new Muxer(options);
|
|
95
|
+
try {
|
|
96
|
+
if (typeof target === 'string') {
|
|
97
|
+
// File or stream URL - resolve relative paths and create directories
|
|
98
|
+
// Check if it's a URL (starts with protocol://) or a file path
|
|
99
|
+
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target);
|
|
100
|
+
const resolvedTarget = isUrl ? target : resolve(target);
|
|
101
|
+
// Create directory structure for local files (not URLs)
|
|
102
|
+
if (!isUrl && target !== '') {
|
|
103
|
+
const dir = dirname(resolvedTarget);
|
|
104
|
+
await mkdir(dir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
// Allocate output context
|
|
107
|
+
const ret = output.formatContext.allocOutputContext2(null, options?.format ?? null, resolvedTarget === '' ? null : resolvedTarget);
|
|
108
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
109
|
+
// Set format options if provided
|
|
110
|
+
if (options?.options) {
|
|
111
|
+
for (const [key, value] of Object.entries(options.options)) {
|
|
112
|
+
output.formatContext.setOption(key, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Check if we need to open IO
|
|
116
|
+
const oformat = output.formatContext.oformat;
|
|
117
|
+
if (resolvedTarget && oformat && !oformat.hasFlags(AVFMT_NOFILE)) {
|
|
118
|
+
// For file-based formats, we need to open the file using avio_open2
|
|
119
|
+
// FFmpeg will manage the AVIOContext internally
|
|
120
|
+
output.ioContext = new IOContext();
|
|
121
|
+
const openRet = await output.ioContext.open2(resolvedTarget, AVIO_FLAG_WRITE);
|
|
122
|
+
FFmpegError.throwIfError(openRet, `Failed to open output file: ${resolvedTarget}`);
|
|
123
|
+
output.formatContext.pb = output.ioContext;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Custom IO with callbacks - format is required
|
|
128
|
+
if (!options?.format) {
|
|
129
|
+
throw new Error('Format must be specified for custom IO');
|
|
130
|
+
}
|
|
131
|
+
const ret = output.formatContext.allocOutputContext2(null, options.format, null);
|
|
132
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
133
|
+
// Set format options if provided
|
|
134
|
+
if (options?.options) {
|
|
135
|
+
for (const [key, value] of Object.entries(options.options)) {
|
|
136
|
+
output.formatContext.setOption(key, value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Setup custom IO with callbacks
|
|
140
|
+
output.ioContext = new IOContext();
|
|
141
|
+
output.ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 1, target.read, target.write, target.seek);
|
|
142
|
+
output.ioContext.maxPacketSize = options.maxPacketSize ?? MAX_PACKET_SIZE;
|
|
143
|
+
output.formatContext.pb = output.ioContext;
|
|
144
|
+
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
145
|
+
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Cleanup on error
|
|
150
|
+
if (output.ioContext) {
|
|
151
|
+
try {
|
|
152
|
+
const isCustomIO = output.formatContext.hasFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
153
|
+
if (isCustomIO) {
|
|
154
|
+
// Clear the pb reference first
|
|
155
|
+
output.formatContext.pb = null;
|
|
156
|
+
// For custom IO with callbacks, free the context
|
|
157
|
+
output.ioContext.freeContext();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// For file-based IO, close the file handle
|
|
161
|
+
await output.ioContext.closep();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Ignore errors
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (output.formatContext) {
|
|
169
|
+
try {
|
|
170
|
+
output.formatContext.freeContext();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Ignore errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
static openSync(target, options) {
|
|
180
|
+
const output = new Muxer(options);
|
|
181
|
+
try {
|
|
182
|
+
if (typeof target === 'string') {
|
|
183
|
+
// File or stream URL - resolve relative paths and create directories
|
|
184
|
+
// Check if it's a URL (starts with protocol://) or a file path
|
|
185
|
+
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target);
|
|
186
|
+
const resolvedTarget = isUrl ? target : resolve(target);
|
|
187
|
+
// Create directory structure for local files (not URLs)
|
|
188
|
+
if (!isUrl && target !== '') {
|
|
189
|
+
const dir = dirname(resolvedTarget);
|
|
190
|
+
mkdirSync(dir, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
// Allocate output context
|
|
193
|
+
const ret = output.formatContext.allocOutputContext2(null, options?.format ?? null, resolvedTarget === '' ? null : resolvedTarget);
|
|
194
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
195
|
+
// Set format options if provided
|
|
196
|
+
if (options?.options) {
|
|
197
|
+
for (const [key, value] of Object.entries(options.options)) {
|
|
198
|
+
output.formatContext.setOption(key, value);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Check if we need to open IO
|
|
202
|
+
const oformat = output.formatContext.oformat;
|
|
203
|
+
if (resolvedTarget && oformat && !oformat.hasFlags(AVFMT_NOFILE)) {
|
|
204
|
+
// For file-based formats, we need to open the file using avio_open2
|
|
205
|
+
// FFmpeg will manage the AVIOContext internally
|
|
206
|
+
output.ioContext = new IOContext();
|
|
207
|
+
const openRet = output.ioContext.open2Sync(resolvedTarget, AVIO_FLAG_WRITE);
|
|
208
|
+
FFmpegError.throwIfError(openRet, `Failed to open output file: ${resolvedTarget}`);
|
|
209
|
+
output.formatContext.pb = output.ioContext;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Custom IO with callbacks - format is required
|
|
214
|
+
if (!options?.format) {
|
|
215
|
+
throw new Error('Format must be specified for custom IO');
|
|
216
|
+
}
|
|
217
|
+
const ret = output.formatContext.allocOutputContext2(null, options.format, null);
|
|
218
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
219
|
+
// Set format options if provided
|
|
220
|
+
if (options?.options) {
|
|
221
|
+
for (const [key, value] of Object.entries(options.options)) {
|
|
222
|
+
output.formatContext.setOption(key, value);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Setup custom IO with callbacks
|
|
226
|
+
output.ioContext = new IOContext();
|
|
227
|
+
output.ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 1, target.read, target.write, target.seek);
|
|
228
|
+
output.ioContext.maxPacketSize = options.maxPacketSize ?? MAX_PACKET_SIZE;
|
|
229
|
+
output.formatContext.pb = output.ioContext;
|
|
230
|
+
output.formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
231
|
+
}
|
|
232
|
+
return output;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
// Cleanup on error
|
|
236
|
+
if (output.ioContext) {
|
|
237
|
+
try {
|
|
238
|
+
const isCustomIO = output.formatContext.hasFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
239
|
+
if (isCustomIO) {
|
|
240
|
+
// Clear the pb reference first
|
|
241
|
+
output.formatContext.pb = null;
|
|
242
|
+
// For custom IO with callbacks, free the context
|
|
243
|
+
output.ioContext.freeContext();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// For file-based IO, close the file handle
|
|
247
|
+
output.ioContext.closepSync();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Ignore errors
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (output.formatContext) {
|
|
255
|
+
try {
|
|
256
|
+
output.formatContext.freeContext();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Ignore errors
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Check if output is open.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* if (!output.isOutputOpen) {
|
|
271
|
+
* console.log('Output is not open');
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
get isOpen() {
|
|
276
|
+
return !this.isClosed;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if output is initialized.
|
|
280
|
+
*
|
|
281
|
+
* All streams have been initialized.
|
|
282
|
+
* This occurs after the first packet has been written to each stream.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* if (!output.isOutputInitialized) {
|
|
287
|
+
* console.log('Output is not initialized');
|
|
288
|
+
* }
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
get streamsInitialized() {
|
|
292
|
+
if (this._streams.size === 0) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
if (this.isClosed) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return Array.from(this._streams).every(([_, stream]) => stream.initialized);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get all streams in the media.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* for (const stream of output.streams) {
|
|
306
|
+
* console.log(`Stream ${stream.index}: ${stream.codecpar.codecType}`);
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
get streams() {
|
|
311
|
+
return this.formatContext.streams;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get format name.
|
|
315
|
+
*
|
|
316
|
+
* Returns 'unknown' if output is closed or format is not available.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* console.log(`Format: ${output.formatName}`); // "mov,mp4,m4a,3gp,3g2,mj2"
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
get formatName() {
|
|
324
|
+
if (this.isClosed) {
|
|
325
|
+
return 'unknown';
|
|
326
|
+
}
|
|
327
|
+
return this.formatContext.oformat?.name ?? 'unknown';
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get format long name.
|
|
331
|
+
*
|
|
332
|
+
* Returns 'Unknown Format' if output is closed or format is not available.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* console.log(`Format: ${output.formatLongName}`); // "QuickTime / MOV"
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
get formatLongName() {
|
|
340
|
+
if (this.isClosed) {
|
|
341
|
+
return 'Unknown Format';
|
|
342
|
+
}
|
|
343
|
+
return this.formatContext.oformat?.longName ?? 'Unknown Format';
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get MIME type of the output format.
|
|
347
|
+
*
|
|
348
|
+
* Returns format's native MIME type.
|
|
349
|
+
* For DASH/HLS formats, use {@link getStreamMimeType} for stream-specific MIME types.
|
|
350
|
+
*
|
|
351
|
+
* Returns null if output is closed or format is not available.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```typescript
|
|
355
|
+
* console.log(mp4Output.mimeType); // "video/mp4"
|
|
356
|
+
* console.log(dashOutput.mimeType); // null (DASH has no global MIME type)
|
|
357
|
+
*
|
|
358
|
+
* // For DASH/HLS, get MIME type per stream:
|
|
359
|
+
* console.log(dashOutput.getStreamMimeType(0)); // "video/mp4"
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
get mimeType() {
|
|
363
|
+
if (this.isClosed) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return this.formatContext.oformat?.mimeType ?? null;
|
|
367
|
+
}
|
|
368
|
+
addStream(streamOrEncoder, options) {
|
|
369
|
+
if (this.isClosed) {
|
|
370
|
+
throw new Error('Muxer is closed');
|
|
371
|
+
}
|
|
372
|
+
if (this.headerWritten) {
|
|
373
|
+
throw new Error('Cannot add streams after packets have been written');
|
|
374
|
+
}
|
|
375
|
+
const outStream = this.formatContext.newStream(null);
|
|
376
|
+
if (!outStream) {
|
|
377
|
+
throw new Error('Failed to create new stream');
|
|
378
|
+
}
|
|
379
|
+
// Determine if first parameter is Encoder or Stream
|
|
380
|
+
const isEncoderFirst = streamOrEncoder instanceof Encoder;
|
|
381
|
+
let stream;
|
|
382
|
+
let encoder;
|
|
383
|
+
if (isEncoderFirst) {
|
|
384
|
+
// First parameter is Encoder
|
|
385
|
+
encoder = streamOrEncoder;
|
|
386
|
+
stream = options?.inputStream;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// First parameter is Stream
|
|
390
|
+
stream = streamOrEncoder;
|
|
391
|
+
encoder = options?.encoder;
|
|
392
|
+
}
|
|
393
|
+
const isStreamCopy = !encoder;
|
|
394
|
+
// Auto-set GLOBAL_HEADER flag if format requires it
|
|
395
|
+
if (encoder) {
|
|
396
|
+
const oformat = this.formatContext.oformat;
|
|
397
|
+
if (oformat?.hasFlags(AVFMT_GLOBALHEADER)) {
|
|
398
|
+
encoder.setCodecFlags(AV_CODEC_FLAG_GLOBAL_HEADER);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// For stream copy, initialize immediately since we have all the info
|
|
402
|
+
if (isStreamCopy) {
|
|
403
|
+
if (!stream) {
|
|
404
|
+
throw new Error('Stream copy mode requires an input stream');
|
|
405
|
+
}
|
|
406
|
+
const ret = stream.codecpar.copy(outStream.codecpar);
|
|
407
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
|
|
408
|
+
// Set the timebases
|
|
409
|
+
const sourceTimeBase = stream.timeBase;
|
|
410
|
+
outStream.timeBase = new Rational(stream.timeBase.num, stream.timeBase.den);
|
|
411
|
+
// Copy frame rates and aspect ratios
|
|
412
|
+
outStream.avgFrameRate = stream.avgFrameRate;
|
|
413
|
+
if (stream.sampleAspectRatio.num > 0) {
|
|
414
|
+
outStream.sampleAspectRatio = stream.sampleAspectRatio;
|
|
415
|
+
}
|
|
416
|
+
outStream.rFrameRate = stream.rFrameRate;
|
|
417
|
+
// Copy duration
|
|
418
|
+
if (stream.duration > 0n) {
|
|
419
|
+
outStream.duration = stream.duration;
|
|
420
|
+
}
|
|
421
|
+
// Copy metadata
|
|
422
|
+
const metadata = stream.metadata;
|
|
423
|
+
if (metadata) {
|
|
424
|
+
outStream.metadata = metadata;
|
|
425
|
+
}
|
|
426
|
+
// Copy disposition
|
|
427
|
+
outStream.disposition = stream.disposition;
|
|
428
|
+
// Copy coded_side_data (HDR/Dolby Vision)
|
|
429
|
+
// Iterate over all side_data entries and copy them
|
|
430
|
+
const allSideData = stream.codecpar.getAllCodedSideData();
|
|
431
|
+
for (const sd of allSideData) {
|
|
432
|
+
outStream.codecpar.addCodedSideData(sd.type, sd.data);
|
|
433
|
+
}
|
|
434
|
+
this._streams.set(outStream.index, {
|
|
435
|
+
initialized: true,
|
|
436
|
+
outputStream: outStream,
|
|
437
|
+
inputStream: stream,
|
|
438
|
+
encoder: undefined,
|
|
439
|
+
sourceTimeBase,
|
|
440
|
+
isStreamCopy: true,
|
|
441
|
+
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
442
|
+
preMuxQueue: [],
|
|
443
|
+
preMuxQueueDataSize: 0,
|
|
444
|
+
eofReceived: false,
|
|
445
|
+
lastMuxDts: AV_NOPTS_VALUE,
|
|
446
|
+
tsRescaleDeltaLast: { value: AV_NOPTS_VALUE },
|
|
447
|
+
streamcopyStarted: false,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// Encoding path - lazy initialization
|
|
452
|
+
// stream is optional here - if provided, we copy metadata/disposition
|
|
453
|
+
// If not provided (encoder-only mode), stream will be initialized from first encoded frame
|
|
454
|
+
this._streams.set(outStream.index, {
|
|
455
|
+
initialized: false,
|
|
456
|
+
outputStream: outStream,
|
|
457
|
+
inputStream: stream,
|
|
458
|
+
encoder,
|
|
459
|
+
sourceTimeBase: undefined, // Will be set on initialization
|
|
460
|
+
isStreamCopy: false,
|
|
461
|
+
sqIdxMux: -1, // Will be set if sync queue is needed
|
|
462
|
+
preMuxQueue: [],
|
|
463
|
+
preMuxQueueDataSize: 0,
|
|
464
|
+
eofReceived: false,
|
|
465
|
+
lastMuxDts: AV_NOPTS_VALUE,
|
|
466
|
+
tsRescaleDeltaLast: { value: AV_NOPTS_VALUE },
|
|
467
|
+
streamcopyStarted: false,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return outStream.index;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get output stream by index.
|
|
474
|
+
*
|
|
475
|
+
* Returns the stream at the specified index.
|
|
476
|
+
* Use the stream index returned by addStream().
|
|
477
|
+
*
|
|
478
|
+
* @param index - Stream index (returned by addStream)
|
|
479
|
+
*
|
|
480
|
+
* @returns Stream or undefined if index is invalid
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```typescript
|
|
484
|
+
* const output = await Muxer.open('output.mp4');
|
|
485
|
+
* const videoIdx = output.addStream(encoder);
|
|
486
|
+
*
|
|
487
|
+
* // Get the output stream to inspect codec parameters
|
|
488
|
+
* const stream = output.getStream(videoIdx);
|
|
489
|
+
* if (stream) {
|
|
490
|
+
* console.log(`Output codec: ${stream.codecpar.codecId}`);
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* @see {@link addStream} For adding streams
|
|
495
|
+
* @see {@link video} For getting video streams
|
|
496
|
+
* @see {@link audio} For getting audio streams
|
|
497
|
+
*/
|
|
498
|
+
getStream(index) {
|
|
499
|
+
const streams = this.formatContext.streams;
|
|
500
|
+
if (!streams || index < 0 || index >= streams.length) {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
return streams[index];
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get video stream by index.
|
|
507
|
+
*
|
|
508
|
+
* Returns the nth video stream (0-based index).
|
|
509
|
+
* Returns undefined if stream doesn't exist.
|
|
510
|
+
*
|
|
511
|
+
* @param index - Video stream index (default: 0)
|
|
512
|
+
*
|
|
513
|
+
* @returns Video stream or undefined
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const output = await Muxer.open('output.mp4');
|
|
518
|
+
* output.addStream(videoEncoder);
|
|
519
|
+
*
|
|
520
|
+
* // Get first video stream
|
|
521
|
+
* const videoStream = output.video();
|
|
522
|
+
* if (videoStream) {
|
|
523
|
+
* console.log(`Video output: ${videoStream.codecpar.width}x${videoStream.codecpar.height}`);
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* @see {@link audio} For audio streams
|
|
528
|
+
* @see {@link getStream} For direct stream access
|
|
529
|
+
*/
|
|
530
|
+
video(index = 0) {
|
|
531
|
+
const streams = this.formatContext.streams;
|
|
532
|
+
if (!streams)
|
|
533
|
+
return undefined;
|
|
534
|
+
const videoStreams = streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_VIDEO);
|
|
535
|
+
return videoStreams[index];
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get audio stream by index.
|
|
539
|
+
*
|
|
540
|
+
* Returns the nth audio stream (0-based index).
|
|
541
|
+
* Returns undefined if stream doesn't exist.
|
|
542
|
+
*
|
|
543
|
+
* @param index - Audio stream index (default: 0)
|
|
544
|
+
*
|
|
545
|
+
* @returns Audio stream or undefined
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* const output = await Muxer.open('output.mp4');
|
|
550
|
+
* output.addStream(audioEncoder);
|
|
551
|
+
*
|
|
552
|
+
* // Get first audio stream
|
|
553
|
+
* const audioStream = output.audio();
|
|
554
|
+
* if (audioStream) {
|
|
555
|
+
* console.log(`Audio output: ${audioStream.codecpar.sampleRate}Hz`);
|
|
556
|
+
* }
|
|
557
|
+
* ```
|
|
558
|
+
*
|
|
559
|
+
* @see {@link video} For video streams
|
|
560
|
+
* @see {@link getStream} For direct stream access
|
|
561
|
+
*/
|
|
562
|
+
audio(index = 0) {
|
|
563
|
+
const streams = this.formatContext.streams;
|
|
564
|
+
if (!streams)
|
|
565
|
+
return undefined;
|
|
566
|
+
const audioStreams = streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_AUDIO);
|
|
567
|
+
return audioStreams[index];
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get output format.
|
|
571
|
+
*
|
|
572
|
+
* Returns the output format used for muxing.
|
|
573
|
+
* May be null if format context not initialized.
|
|
574
|
+
*
|
|
575
|
+
* @returns Output format or null
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```typescript
|
|
579
|
+
* const output = await Muxer.open('output.mp4');
|
|
580
|
+
* const format = output.outputFormat();
|
|
581
|
+
* if (format) {
|
|
582
|
+
* console.log(`Output format: ${format.name}`);
|
|
583
|
+
* }
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* @see {@link OutputFormat} For format details
|
|
587
|
+
*/
|
|
588
|
+
outputFormat() {
|
|
589
|
+
return this.formatContext.oformat;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Write a packet to the output.
|
|
593
|
+
*
|
|
594
|
+
* Writes muxed packet to the specified stream.
|
|
595
|
+
* Automatically handles:
|
|
596
|
+
* - Stream initialization on first packet (lazy initialization)
|
|
597
|
+
* - Codec parameter configuration from encoder or input stream
|
|
598
|
+
* - Header writing on first packet
|
|
599
|
+
* - Timestamp rescaling between source and output timebases
|
|
600
|
+
* - Sync queue for proper interleaving
|
|
601
|
+
*
|
|
602
|
+
* For encoder sources, the encoder must have processed at least one frame
|
|
603
|
+
* before packets can be written (encoder must be initialized).
|
|
604
|
+
*
|
|
605
|
+
* Uses FFmpeg CLI's sync queue pattern: buffers packets per stream and writes
|
|
606
|
+
* them in DTS order using av_compare_ts for timebase-aware comparison.
|
|
607
|
+
*
|
|
608
|
+
* To signal EOF for a stream, pass null as the packet.
|
|
609
|
+
* This tells the muxer that no more packets will be sent for this stream.
|
|
610
|
+
* The trailer is written only when close() is called.
|
|
611
|
+
*
|
|
612
|
+
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
613
|
+
*
|
|
614
|
+
* @param packet - Packet to write
|
|
615
|
+
*
|
|
616
|
+
* @param streamIndex - Target stream index
|
|
617
|
+
*
|
|
618
|
+
* @throws {Error} If stream invalid or encoder not initialized
|
|
619
|
+
*
|
|
620
|
+
* @throws {FFmpegError} If write fails
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* // Write encoded packet - header written automatically on first packet
|
|
625
|
+
* const packet = await encoder.encode(frame);
|
|
626
|
+
* if (packet) {
|
|
627
|
+
* await output.writePacket(packet, videoIdx);
|
|
628
|
+
* packet.free();
|
|
629
|
+
* }
|
|
630
|
+
* ```
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```typescript
|
|
634
|
+
* // Stream copy with packet processing
|
|
635
|
+
* for await (const packet of input.packets()) {
|
|
636
|
+
* if (packet.streamIndex === inputVideoIdx) {
|
|
637
|
+
* await output.writePacket(packet, outputVideoIdx);
|
|
638
|
+
* }
|
|
639
|
+
* packet.free();
|
|
640
|
+
* }
|
|
641
|
+
* ```
|
|
642
|
+
*
|
|
643
|
+
* @see {@link addStream} For adding streams
|
|
644
|
+
*/
|
|
645
|
+
async writePacket(packet, streamIndex) {
|
|
646
|
+
if (this.isClosed) {
|
|
647
|
+
throw new Error('Muxer is closed');
|
|
648
|
+
}
|
|
649
|
+
if (this.trailerWritten) {
|
|
650
|
+
throw new Error('Cannot write packets after output is finalized');
|
|
651
|
+
}
|
|
652
|
+
if (!this._streams.get(streamIndex)) {
|
|
653
|
+
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
654
|
+
}
|
|
655
|
+
// Initialize any encoder streams that are ready
|
|
656
|
+
for (const streamInfo of this._streams.values()) {
|
|
657
|
+
if (!streamInfo.initialized && streamInfo.encoder) {
|
|
658
|
+
const encoder = streamInfo.encoder;
|
|
659
|
+
const codecContext = encoder.getCodecContext();
|
|
660
|
+
// Skip if encoder not ready yet
|
|
661
|
+
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
// This encoder is ready, initialize it now
|
|
665
|
+
// Read codecType from codecContext, not from stream (which is still uninitialized)
|
|
666
|
+
// const codecType = codecContext.codecType;
|
|
667
|
+
// 1. Set stream timebase
|
|
668
|
+
if (streamInfo.outputStream.timeBase.num <= 0 || streamInfo.outputStream.timeBase.den <= 0) {
|
|
669
|
+
const tb = avAddQ(codecContext.timeBase, { num: 0, den: 1 });
|
|
670
|
+
streamInfo.outputStream.timeBase = new Rational(tb.num, tb.den);
|
|
671
|
+
}
|
|
672
|
+
// 2. Set stream avg_frame_rate, r_frame_rate and sample_aspect_ratio
|
|
673
|
+
const fr = codecContext.framerate;
|
|
674
|
+
streamInfo.outputStream.avgFrameRate = new Rational(fr.num, fr.den);
|
|
675
|
+
streamInfo.outputStream.sampleAspectRatio = codecContext.sampleAspectRatio;
|
|
676
|
+
// 3. Copy codec parameters from encoder context
|
|
677
|
+
const ret = streamInfo.outputStream.codecpar.fromContext(codecContext);
|
|
678
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
679
|
+
// 4. Copy metadata from input stream
|
|
680
|
+
if (streamInfo.inputStream) {
|
|
681
|
+
const metadata = streamInfo.inputStream.metadata;
|
|
682
|
+
if (metadata) {
|
|
683
|
+
streamInfo.outputStream.metadata = metadata;
|
|
684
|
+
}
|
|
685
|
+
// 5. Copy disposition from input stream
|
|
686
|
+
streamInfo.outputStream.disposition = streamInfo.inputStream.disposition;
|
|
687
|
+
// 6. Copy duration hint from input stream
|
|
688
|
+
if (streamInfo.inputStream.duration > 0n) {
|
|
689
|
+
const inputTb = streamInfo.inputStream.timeBase;
|
|
690
|
+
const outputTb = streamInfo.outputStream.timeBase;
|
|
691
|
+
const rescaledDuration = avRescaleQ(streamInfo.inputStream.duration, inputTb, outputTb);
|
|
692
|
+
streamInfo.outputStream.duration = rescaledDuration;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Update the source timebase for timestamp rescaling
|
|
696
|
+
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
697
|
+
// Mark as initialized
|
|
698
|
+
streamInfo.initialized = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const streamInfo = this._streams.get(streamIndex);
|
|
702
|
+
// Handle NULL packet - signals EOF for this stream (FFmpeg pattern: av_interleaved_write_frame(s, NULL))
|
|
703
|
+
// FFmpeg's behavior:
|
|
704
|
+
// - If muxer not started (uninitialized streams), buffer NULL in PreMuxQueue as EOF marker
|
|
705
|
+
// - If muxer started, send NULL to SyncQueue to signal EOF and flush
|
|
706
|
+
if (packet === null) {
|
|
707
|
+
// Mark stream as EOF received
|
|
708
|
+
streamInfo.eofReceived = true;
|
|
709
|
+
// Check if any streams are still uninitialized (PreMuxQueue phase)
|
|
710
|
+
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
711
|
+
// PHASE 1: Before muxer starts - buffer NULL packet in PreMuxQueue
|
|
712
|
+
// This matches FFmpeg's mux_queue_packet() which writes NULL to PreMuxQueue FIFO
|
|
713
|
+
if (uninitialized || this.headerWritePromise) {
|
|
714
|
+
// Buffer NULL as EOF marker (no size contribution)
|
|
715
|
+
streamInfo.preMuxQueue.push(null);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
719
|
+
if (!this.headerWritten) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// If using SyncQueue, send EOF for this stream
|
|
723
|
+
if (this.syncQueue && streamInfo.sqIdxMux >= 0) {
|
|
724
|
+
// Send NULL to signal EOF to sync queue
|
|
725
|
+
// Native side handles null correctly (sets sqframe.p = nullptr)
|
|
726
|
+
const ret = this.syncQueue.send(streamInfo.sqIdxMux, null);
|
|
727
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
728
|
+
if (this.options.exitOnError) {
|
|
729
|
+
FFmpegError.throwIfError(ret, 'Failed to send EOF to sync queue');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Receive and write any remaining packets from sync queue
|
|
733
|
+
while (!this.isClosed) {
|
|
734
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
735
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
736
|
+
break; // No more packets ready
|
|
737
|
+
}
|
|
738
|
+
if (recvRet === AVERROR_EOF) {
|
|
739
|
+
break; // All streams finished
|
|
740
|
+
}
|
|
741
|
+
if (recvRet >= 0) {
|
|
742
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
743
|
+
const pkt = this.sqPacket.clone();
|
|
744
|
+
if (!pkt) {
|
|
745
|
+
throw new Error('Failed to clone packet from sync queue');
|
|
746
|
+
}
|
|
747
|
+
pkt.streamIndex = recvRet;
|
|
748
|
+
await this.write(pkt, recvStreamInfo, recvRet);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return; // EOF signaled, nothing more to do
|
|
753
|
+
}
|
|
754
|
+
// Clone packet immediately - we will modify it and caller retains ownership
|
|
755
|
+
const clonedPacket = packet.clone();
|
|
756
|
+
if (!clonedPacket) {
|
|
757
|
+
throw new Error('Failed to clone packet for writing');
|
|
758
|
+
}
|
|
759
|
+
// Apply streamcopy filtering BEFORE buffering
|
|
760
|
+
// This ensures rejected packets never enter the queue/buffer
|
|
761
|
+
if (streamInfo.isStreamCopy) {
|
|
762
|
+
const shouldWrite = this.ofStreamcopy(clonedPacket, streamInfo, streamIndex);
|
|
763
|
+
if (!shouldWrite) {
|
|
764
|
+
clonedPacket.free(); // Free the clone since we won't use it
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Check if any streams are still uninitialized or header is being written
|
|
769
|
+
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
770
|
+
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
771
|
+
// PreMuxQueue is used during initialization phase ONLY (regardless of SyncQueue presence)
|
|
772
|
+
// After header write, PreMuxQueue is flushed in DTS-sorted order
|
|
773
|
+
if (uninitialized || this.headerWritePromise) {
|
|
774
|
+
// Check PreMuxQueue limits
|
|
775
|
+
const maxPackets = this.options.maxMuxingQueueSize ?? MAX_MUXING_QUEUE_SIZE;
|
|
776
|
+
const dataThreshold = this.options.muxingQueueDataThreshold ?? MUXING_QUEUE_DATA_THRESHOLD;
|
|
777
|
+
const currentPackets = streamInfo.preMuxQueue.length;
|
|
778
|
+
const currentBytes = streamInfo.preMuxQueueDataSize;
|
|
779
|
+
const packetSize = clonedPacket.size;
|
|
780
|
+
const thresholdReached = currentBytes + packetSize > dataThreshold;
|
|
781
|
+
const effectiveMaxPackets = thresholdReached ? maxPackets : Number.MAX_SAFE_INTEGER;
|
|
782
|
+
// Check if we would exceed packet limit (only if threshold reached)
|
|
783
|
+
if (currentPackets >= effectiveMaxPackets) {
|
|
784
|
+
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
785
|
+
throw new Error(
|
|
786
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
787
|
+
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
788
|
+
}
|
|
789
|
+
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
790
|
+
streamInfo.preMuxQueue.push(clonedPacket);
|
|
791
|
+
streamInfo.preMuxQueueDataSize += packetSize;
|
|
792
|
+
return; // Don't proceed to header write yet
|
|
793
|
+
}
|
|
794
|
+
// Automatically write header if not written yet
|
|
795
|
+
if (!this.headerWritten) {
|
|
796
|
+
this.headerWritePromise ??= (async () => {
|
|
797
|
+
this.startWriteWorker();
|
|
798
|
+
this.setupSyncQueues();
|
|
799
|
+
this.updateDefaultDisposition();
|
|
800
|
+
this.copyContainerMetadata();
|
|
801
|
+
const ret = await this.formatContext.writeHeader();
|
|
802
|
+
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
803
|
+
this.headerWritten = true;
|
|
804
|
+
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
805
|
+
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
806
|
+
await this.flushPreMuxQueues();
|
|
807
|
+
})();
|
|
808
|
+
await this.headerWritePromise;
|
|
809
|
+
if (this.headerWritten) {
|
|
810
|
+
this.headerWritePromise = undefined;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// PHASE 3: Write packet - normal muxing after header
|
|
814
|
+
if (this.syncQueue && streamInfo.sqIdxMux >= 0) {
|
|
815
|
+
// Use SyncQueue for packet interleaving
|
|
816
|
+
// NOTE: Do NOT set clonedPacket.timeBase here!
|
|
817
|
+
// Packet must keep its source timebase (encoder timebase) so muxFixupTs can rescale correctly
|
|
818
|
+
// Send packet to sync queue
|
|
819
|
+
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
820
|
+
// Handle errors from sq_send
|
|
821
|
+
if (ret < 0) {
|
|
822
|
+
if (ret === AVERROR_EOF) {
|
|
823
|
+
// Stream finished - this is normal, just return
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (this.options.exitOnError) {
|
|
827
|
+
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue');
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// Receive synchronized packets from queue and write to muxer
|
|
832
|
+
while (!this.isClosed) {
|
|
833
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
834
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
835
|
+
break; // No more packets ready
|
|
836
|
+
}
|
|
837
|
+
if (recvRet === AVERROR_EOF) {
|
|
838
|
+
break; // All streams finished
|
|
839
|
+
}
|
|
840
|
+
if (recvRet >= 0) {
|
|
841
|
+
// recvRet is the stream index
|
|
842
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
843
|
+
// Clone packet before writing (muxer takes ownership and will unref it)
|
|
844
|
+
// We need to keep sqPacket alive for the next receive() call
|
|
845
|
+
const pkt = this.sqPacket.clone();
|
|
846
|
+
if (!pkt) {
|
|
847
|
+
throw new Error('Failed to clone packet from sync queue');
|
|
848
|
+
}
|
|
849
|
+
pkt.streamIndex = recvRet;
|
|
850
|
+
// Write packet (muxer takes ownership)
|
|
851
|
+
await this.write(pkt, recvStreamInfo, recvRet);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// No sync queue needed - write directly
|
|
857
|
+
clonedPacket.streamIndex = streamIndex;
|
|
858
|
+
await this.write(clonedPacket, streamInfo, streamIndex);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Write a packet to the output synchronously.
|
|
863
|
+
* Synchronous version of writePacket.
|
|
864
|
+
*
|
|
865
|
+
* Writes muxed packet to the specified stream.
|
|
866
|
+
* Automatically handles:
|
|
867
|
+
* - Stream initialization on first packet (lazy initialization)
|
|
868
|
+
* - Codec parameter configuration from encoder or input stream
|
|
869
|
+
* - Header writing on first packet
|
|
870
|
+
* - Timestamp rescaling between source and output timebases
|
|
871
|
+
* - Sync queue for proper interleaving
|
|
872
|
+
*
|
|
873
|
+
* For encoder sources, the encoder must have processed at least one frame
|
|
874
|
+
* before packets can be written (encoder must be initialized).
|
|
875
|
+
*
|
|
876
|
+
* Uses FFmpeg CLI's sync queue pattern: buffers packets per stream and writes
|
|
877
|
+
* them in DTS order using av_compare_ts for timebase-aware comparison.
|
|
878
|
+
*
|
|
879
|
+
* To signal EOF for a stream, pass null as the packet.
|
|
880
|
+
* This tells the muxer that no more packets will be sent for this stream.
|
|
881
|
+
* The trailer is written only when close() is called.
|
|
882
|
+
*
|
|
883
|
+
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
884
|
+
*
|
|
885
|
+
* @param packet - Packet to write
|
|
886
|
+
*
|
|
887
|
+
* @param streamIndex - Target stream index
|
|
888
|
+
*
|
|
889
|
+
* @throws {Error} If stream invalid or encoder not initialized
|
|
890
|
+
*
|
|
891
|
+
* @throws {FFmpegError} If write fails
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* ```typescript
|
|
895
|
+
* // Write encoded packet - header written automatically on first packet
|
|
896
|
+
* const packet = encoder.encodeSync(frame);
|
|
897
|
+
* if (packet) {
|
|
898
|
+
* output.writePacketSync(packet, videoIdx);
|
|
899
|
+
* packet.free();
|
|
900
|
+
* }
|
|
901
|
+
* ```
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* ```typescript
|
|
905
|
+
* // Stream copy with packet processing
|
|
906
|
+
* for (const packet of input.packetsSync()) {
|
|
907
|
+
* if (packet.streamIndex === inputVideoIdx) {
|
|
908
|
+
* output.writePacketSync(packet, outputVideoIdx);
|
|
909
|
+
* }
|
|
910
|
+
* packet.free();
|
|
911
|
+
* }
|
|
912
|
+
* ```
|
|
913
|
+
*
|
|
914
|
+
* @see {@link writePacket} For async version
|
|
915
|
+
*/
|
|
916
|
+
writePacketSync(packet, streamIndex) {
|
|
917
|
+
if (this.isClosed) {
|
|
918
|
+
throw new Error('Muxer is closed');
|
|
919
|
+
}
|
|
920
|
+
if (this.trailerWritten) {
|
|
921
|
+
throw new Error('Cannot write packets after output is finalized');
|
|
922
|
+
}
|
|
923
|
+
if (!this._streams.get(streamIndex)) {
|
|
924
|
+
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
925
|
+
}
|
|
926
|
+
// Initialize any encoder streams that are ready
|
|
927
|
+
for (const streamInfo of this._streams.values()) {
|
|
928
|
+
if (!streamInfo.initialized && streamInfo.encoder) {
|
|
929
|
+
const encoder = streamInfo.encoder;
|
|
930
|
+
const codecContext = encoder.getCodecContext();
|
|
931
|
+
// Skip if encoder not ready yet
|
|
932
|
+
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
// This encoder is ready, initialize it now
|
|
936
|
+
// Read codecType from codecContext, not from stream (which is still uninitialized)
|
|
937
|
+
const codecType = codecContext.codecType;
|
|
938
|
+
// 1. Set stream timebase
|
|
939
|
+
// Use encoder's timebase unless user specified custom timebase
|
|
940
|
+
if (streamInfo.timeBase) {
|
|
941
|
+
// User specified custom timebase
|
|
942
|
+
streamInfo.outputStream.timeBase = new Rational(streamInfo.timeBase.num, streamInfo.timeBase.den);
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
// Use encoder's timebase directly
|
|
946
|
+
// The encoder timebase is already set from the first frame in Encoder.initialize()
|
|
947
|
+
streamInfo.outputStream.timeBase = new Rational(codecContext.timeBase.num, codecContext.timeBase.den);
|
|
948
|
+
}
|
|
949
|
+
// 2. Set stream avg_frame_rate, r_frame_rate and sample_aspect_ratio
|
|
950
|
+
if (codecType === AVMEDIA_TYPE_VIDEO) {
|
|
951
|
+
const fr = codecContext.framerate;
|
|
952
|
+
streamInfo.outputStream.avgFrameRate = new Rational(fr.num, fr.den);
|
|
953
|
+
streamInfo.outputStream.rFrameRate = new Rational(fr.num, fr.den);
|
|
954
|
+
streamInfo.outputStream.sampleAspectRatio = codecContext.sampleAspectRatio;
|
|
955
|
+
}
|
|
956
|
+
// 3. Copy codec parameters from encoder context
|
|
957
|
+
const ret = streamInfo.outputStream.codecpar.fromContext(codecContext);
|
|
958
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
959
|
+
// 4. Copy metadata from input stream
|
|
960
|
+
if (streamInfo.inputStream) {
|
|
961
|
+
const metadata = streamInfo.inputStream.metadata;
|
|
962
|
+
if (metadata) {
|
|
963
|
+
streamInfo.outputStream.metadata = metadata;
|
|
964
|
+
}
|
|
965
|
+
// 5. Copy disposition from input stream
|
|
966
|
+
streamInfo.outputStream.disposition = streamInfo.inputStream.disposition;
|
|
967
|
+
// 6. Copy duration hint from input stream
|
|
968
|
+
if (streamInfo.inputStream.duration > 0n) {
|
|
969
|
+
const inputTb = streamInfo.inputStream.timeBase;
|
|
970
|
+
const outputTb = streamInfo.outputStream.timeBase;
|
|
971
|
+
const rescaledDuration = avRescaleQ(streamInfo.inputStream.duration, inputTb, outputTb);
|
|
972
|
+
streamInfo.outputStream.duration = rescaledDuration;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// Update the source timebase for timestamp rescaling
|
|
976
|
+
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
977
|
+
// Mark as initialized
|
|
978
|
+
streamInfo.initialized = true;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const streamInfo = this._streams.get(streamIndex);
|
|
982
|
+
// Handle NULL packet - signals EOF for this stream (FFmpeg pattern: av_interleaved_write_frame(s, NULL))
|
|
983
|
+
// FFmpeg's behavior:
|
|
984
|
+
// - If muxer not started (uninitialized streams), buffer NULL in PreMuxQueue as EOF marker
|
|
985
|
+
// - If muxer started, send NULL to SyncQueue to signal EOF and flush
|
|
986
|
+
if (packet === null) {
|
|
987
|
+
// Mark stream as EOF received
|
|
988
|
+
streamInfo.eofReceived = true;
|
|
989
|
+
// Check if any streams are still uninitialized (PreMuxQueue phase)
|
|
990
|
+
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
991
|
+
// PHASE 1: Before muxer starts - buffer NULL packet in PreMuxQueue
|
|
992
|
+
// This matches FFmpeg's mux_queue_packet() which writes NULL to PreMuxQueue FIFO
|
|
993
|
+
if (uninitialized || this.headerWritePromise) {
|
|
994
|
+
// Buffer NULL as EOF marker (no size contribution)
|
|
995
|
+
streamInfo.preMuxQueue.push(null);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
// PHASE 2: After muxer started - send EOF to SyncQueue and flush
|
|
999
|
+
if (!this.headerWritten) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// If using SyncQueue, send EOF for this stream
|
|
1003
|
+
if (this.syncQueue && streamInfo.sqIdxMux >= 0) {
|
|
1004
|
+
// Send NULL to signal EOF to sync queue
|
|
1005
|
+
// Native side handles null correctly (sets sqframe.p = nullptr)
|
|
1006
|
+
const ret = this.syncQueue.send(streamInfo.sqIdxMux, null);
|
|
1007
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1008
|
+
if (this.options.exitOnError) {
|
|
1009
|
+
FFmpegError.throwIfError(ret, 'Failed to send EOF to sync queue');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Receive and write any remaining packets from sync queue
|
|
1013
|
+
while (!this.isClosed) {
|
|
1014
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
1015
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
1016
|
+
break; // No more packets ready
|
|
1017
|
+
}
|
|
1018
|
+
if (recvRet === AVERROR_EOF) {
|
|
1019
|
+
break; // All streams finished
|
|
1020
|
+
}
|
|
1021
|
+
if (recvRet >= 0) {
|
|
1022
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
1023
|
+
const pkt = this.sqPacket.clone();
|
|
1024
|
+
if (!pkt) {
|
|
1025
|
+
throw new Error('Failed to clone packet from sync queue');
|
|
1026
|
+
}
|
|
1027
|
+
pkt.streamIndex = recvRet;
|
|
1028
|
+
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return; // EOF signaled, nothing more to do
|
|
1033
|
+
}
|
|
1034
|
+
// Clone packet immediately - we will modify it and caller retains ownership
|
|
1035
|
+
const clonedPacket = packet.clone();
|
|
1036
|
+
if (!clonedPacket) {
|
|
1037
|
+
throw new Error('Failed to clone packet for writing');
|
|
1038
|
+
}
|
|
1039
|
+
// Apply streamcopy filtering BEFORE buffering
|
|
1040
|
+
// This ensures rejected packets never enter the queue/buffer
|
|
1041
|
+
if (streamInfo.isStreamCopy) {
|
|
1042
|
+
const shouldWrite = this.ofStreamcopy(clonedPacket, streamInfo, streamIndex);
|
|
1043
|
+
if (!shouldWrite) {
|
|
1044
|
+
clonedPacket.free(); // Free the clone since we won't use it
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Check if any streams are still uninitialized
|
|
1049
|
+
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
1050
|
+
// PHASE 1: Before header write - ALWAYS buffer in PreMuxQueue
|
|
1051
|
+
// PreMuxQueue is used during initialization phase ONLY (regardless of SyncQueue presence)
|
|
1052
|
+
// After header write, PreMuxQueue is flushed in DTS-sorted order
|
|
1053
|
+
if (uninitialized) {
|
|
1054
|
+
// Check PreMuxQueue limits
|
|
1055
|
+
const maxPackets = this.options.maxMuxingQueueSize ?? MAX_MUXING_QUEUE_SIZE;
|
|
1056
|
+
const dataThreshold = this.options.muxingQueueDataThreshold ?? MUXING_QUEUE_DATA_THRESHOLD;
|
|
1057
|
+
const currentPackets = streamInfo.preMuxQueue.length;
|
|
1058
|
+
const currentBytes = streamInfo.preMuxQueueDataSize;
|
|
1059
|
+
const packetSize = clonedPacket.size;
|
|
1060
|
+
const thresholdReached = currentBytes + packetSize > dataThreshold;
|
|
1061
|
+
const effectiveMaxPackets = thresholdReached ? maxPackets : Number.MAX_SAFE_INTEGER;
|
|
1062
|
+
// Check if we would exceed packet limit (only if threshold reached)
|
|
1063
|
+
if (currentPackets >= effectiveMaxPackets) {
|
|
1064
|
+
clonedPacket.free(); // Free the clone since we can't buffer it
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
1067
|
+
`Too many packets buffered for output stream ${streamIndex} (packets: ${currentPackets}, bytes: ${currentBytes}, threshold: ${dataThreshold}, max: ${maxPackets})`);
|
|
1068
|
+
}
|
|
1069
|
+
// Buffer in PreMuxQueue (per-stream FIFO)
|
|
1070
|
+
streamInfo.preMuxQueue.push(clonedPacket);
|
|
1071
|
+
streamInfo.preMuxQueueDataSize += packetSize;
|
|
1072
|
+
return; // Don't proceed to header write yet
|
|
1073
|
+
}
|
|
1074
|
+
// Automatically write header if not written yet
|
|
1075
|
+
if (!this.headerWritten) {
|
|
1076
|
+
this.setupSyncQueues();
|
|
1077
|
+
this.updateDefaultDisposition();
|
|
1078
|
+
this.copyContainerMetadata();
|
|
1079
|
+
const ret = this.formatContext.writeHeaderSync();
|
|
1080
|
+
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
1081
|
+
this.headerWritten = true;
|
|
1082
|
+
// PHASE 2: Flush PreMuxQueue in DTS-sorted order (once after header write)
|
|
1083
|
+
// Packets go: PreMuxQueue → SyncQueue (if present) → Muxer
|
|
1084
|
+
this.flushPreMuxQueuesSync();
|
|
1085
|
+
}
|
|
1086
|
+
// PHASE 3: Write packet - normal muxing after header
|
|
1087
|
+
if (this.syncQueue && streamInfo.sqIdxMux >= 0) {
|
|
1088
|
+
// Use SyncQueue for packet interleaving
|
|
1089
|
+
// NOTE: Do NOT set clonedPacket.timeBase here!
|
|
1090
|
+
// Packet must keep its source timebase (encoder timebase) so muxFixupTs can rescale correctly
|
|
1091
|
+
// Send packet to sync queue
|
|
1092
|
+
const ret = this.syncQueue.send(streamInfo.sqIdxMux, clonedPacket);
|
|
1093
|
+
// Handle errors from sq_send
|
|
1094
|
+
if (ret < 0) {
|
|
1095
|
+
if (ret === AVERROR_EOF) {
|
|
1096
|
+
// Stream finished - this is normal, just return
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (this.options.exitOnError) {
|
|
1100
|
+
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue');
|
|
1101
|
+
}
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
// Receive synchronized packets from queue and write to muxer
|
|
1105
|
+
while (!this.isClosed) {
|
|
1106
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
1107
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
1108
|
+
break; // No more packets ready
|
|
1109
|
+
}
|
|
1110
|
+
if (recvRet === AVERROR_EOF) {
|
|
1111
|
+
break; // All streams finished
|
|
1112
|
+
}
|
|
1113
|
+
if (recvRet >= 0) {
|
|
1114
|
+
// recvRet is the stream index
|
|
1115
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
1116
|
+
// Clone packet before writing (muxer takes ownership and will unref it)
|
|
1117
|
+
// We need to keep sqPacket alive for the next receive() call
|
|
1118
|
+
const pkt = this.sqPacket.clone();
|
|
1119
|
+
if (!pkt) {
|
|
1120
|
+
throw new Error('Failed to clone packet from sync queue');
|
|
1121
|
+
}
|
|
1122
|
+
pkt.streamIndex = recvRet;
|
|
1123
|
+
// Write packet (muxer takes ownership)
|
|
1124
|
+
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
// No sync queue needed - write directly
|
|
1130
|
+
clonedPacket.streamIndex = streamIndex;
|
|
1131
|
+
this.writeSync(clonedPacket, streamInfo, streamIndex);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Close muxer and free resources.
|
|
1136
|
+
*
|
|
1137
|
+
* Automatically writes trailer if header was written.
|
|
1138
|
+
* Closes the output file and releases all resources.
|
|
1139
|
+
* Safe to call multiple times.
|
|
1140
|
+
* Automatically called by Symbol.asyncDispose.
|
|
1141
|
+
*
|
|
1142
|
+
* @example
|
|
1143
|
+
* ```typescript
|
|
1144
|
+
* const output = await Muxer.open('output.mp4');
|
|
1145
|
+
* try {
|
|
1146
|
+
* // Use output - trailer written automatically on close
|
|
1147
|
+
* } finally {
|
|
1148
|
+
* await output.close();
|
|
1149
|
+
* }
|
|
1150
|
+
* ```
|
|
1151
|
+
*
|
|
1152
|
+
* @see {@link Symbol.asyncDispose} For automatic cleanup
|
|
1153
|
+
*/
|
|
1154
|
+
async close() {
|
|
1155
|
+
if (this.isClosed) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
this.isClosed = true;
|
|
1159
|
+
// Close write queue and wait for worker to finish
|
|
1160
|
+
if (this.writeQueue) {
|
|
1161
|
+
this.writeQueue.close();
|
|
1162
|
+
await this.writeWorkerPromise;
|
|
1163
|
+
}
|
|
1164
|
+
// Free PreMuxQueue packets
|
|
1165
|
+
for (const streamInfo of this._streams.values()) {
|
|
1166
|
+
// Free any packets in PreMuxQueue
|
|
1167
|
+
for (const pkt of streamInfo.preMuxQueue) {
|
|
1168
|
+
pkt?.free();
|
|
1169
|
+
}
|
|
1170
|
+
streamInfo.preMuxQueue = [];
|
|
1171
|
+
}
|
|
1172
|
+
// Free sync queue resources
|
|
1173
|
+
if (this.sqPacket) {
|
|
1174
|
+
this.sqPacket.free();
|
|
1175
|
+
this.sqPacket = undefined;
|
|
1176
|
+
}
|
|
1177
|
+
if (this.syncQueue) {
|
|
1178
|
+
this.syncQueue.free();
|
|
1179
|
+
this.syncQueue = undefined;
|
|
1180
|
+
}
|
|
1181
|
+
// Try to write trailer if header was written but trailer wasn't
|
|
1182
|
+
try {
|
|
1183
|
+
if (this.headerWritten && !this.trailerWritten) {
|
|
1184
|
+
await this.formatContext.writeTrailer();
|
|
1185
|
+
this.trailerWritten = true;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// Ignore errors
|
|
1190
|
+
}
|
|
1191
|
+
// Clear pb reference first to prevent use-after-free
|
|
1192
|
+
if (this.ioContext) {
|
|
1193
|
+
this.formatContext.pb = null;
|
|
1194
|
+
}
|
|
1195
|
+
// Determine if this is custom IO before freeing format context
|
|
1196
|
+
const isCustomIO = this.formatContext.hasFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
1197
|
+
// For file-based IO, close the file handle via closep
|
|
1198
|
+
// For custom IO, the context will be freed below
|
|
1199
|
+
if (this.ioContext && !isCustomIO) {
|
|
1200
|
+
try {
|
|
1201
|
+
await this.ioContext.closep();
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
// Ignore errors
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// Free format context
|
|
1208
|
+
if (this.formatContext) {
|
|
1209
|
+
try {
|
|
1210
|
+
this.formatContext.freeContext();
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
// Ignore errors
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// Now free custom IO context if present
|
|
1217
|
+
if (this.ioContext && isCustomIO) {
|
|
1218
|
+
try {
|
|
1219
|
+
this.ioContext.freeContext();
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
// Ignore errors
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Close muxer and free resources synchronously.
|
|
1228
|
+
* Synchronous version of close.
|
|
1229
|
+
*
|
|
1230
|
+
* Automatically writes trailer if header was written.
|
|
1231
|
+
* Closes the output file and releases all resources.
|
|
1232
|
+
* Safe to call multiple times.
|
|
1233
|
+
* Automatically called by Symbol.dispose.
|
|
1234
|
+
*
|
|
1235
|
+
* @example
|
|
1236
|
+
* ```typescript
|
|
1237
|
+
* const output = Muxer.openSync('output.mp4');
|
|
1238
|
+
* try {
|
|
1239
|
+
* // Use output - trailer written automatically on close
|
|
1240
|
+
* } finally {
|
|
1241
|
+
* output.closeSync();
|
|
1242
|
+
* }
|
|
1243
|
+
* ```
|
|
1244
|
+
*
|
|
1245
|
+
* @see {@link close} For async version
|
|
1246
|
+
*/
|
|
1247
|
+
closeSync() {
|
|
1248
|
+
if (this.isClosed) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
this.isClosed = true;
|
|
1252
|
+
// Free PreMuxQueue packets
|
|
1253
|
+
for (const streamInfo of this._streams.values()) {
|
|
1254
|
+
// Free any packets in PreMuxQueue
|
|
1255
|
+
for (const pkt of streamInfo.preMuxQueue) {
|
|
1256
|
+
pkt?.free();
|
|
1257
|
+
}
|
|
1258
|
+
streamInfo.preMuxQueue = [];
|
|
1259
|
+
}
|
|
1260
|
+
// Free sync queue resources
|
|
1261
|
+
if (this.sqPacket) {
|
|
1262
|
+
this.sqPacket.free();
|
|
1263
|
+
this.sqPacket = undefined;
|
|
1264
|
+
}
|
|
1265
|
+
if (this.syncQueue) {
|
|
1266
|
+
this.syncQueue.free();
|
|
1267
|
+
this.syncQueue = undefined;
|
|
1268
|
+
}
|
|
1269
|
+
// Try to write trailer if header was written but trailer wasn't
|
|
1270
|
+
try {
|
|
1271
|
+
if (this.headerWritten && !this.trailerWritten) {
|
|
1272
|
+
this.formatContext.writeTrailerSync();
|
|
1273
|
+
this.trailerWritten = true;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
catch {
|
|
1277
|
+
// Ignore errors
|
|
1278
|
+
}
|
|
1279
|
+
// Clear pb reference first to prevent use-after-free
|
|
1280
|
+
if (this.ioContext) {
|
|
1281
|
+
this.formatContext.pb = null;
|
|
1282
|
+
}
|
|
1283
|
+
// Determine if this is custom IO before freeing format context
|
|
1284
|
+
const isCustomIO = this.formatContext.hasFlags(AVFMT_FLAG_CUSTOM_IO);
|
|
1285
|
+
// For file-based IO, close the file handle via closep
|
|
1286
|
+
// For custom IO, the context will be freed below
|
|
1287
|
+
if (this.ioContext && !isCustomIO) {
|
|
1288
|
+
try {
|
|
1289
|
+
this.ioContext.closepSync();
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
// Ignore errors
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// Free format context
|
|
1296
|
+
if (this.formatContext) {
|
|
1297
|
+
try {
|
|
1298
|
+
this.formatContext.freeContext();
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
// Ignore errors
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
// Now free custom IO context if present
|
|
1305
|
+
if (this.ioContext && isCustomIO) {
|
|
1306
|
+
try {
|
|
1307
|
+
this.ioContext.freeContext();
|
|
1308
|
+
}
|
|
1309
|
+
catch {
|
|
1310
|
+
// Ignore errors
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get underlying format context.
|
|
1316
|
+
*
|
|
1317
|
+
* Returns the internal format context for advanced operations.
|
|
1318
|
+
*
|
|
1319
|
+
* @returns Format context
|
|
1320
|
+
*
|
|
1321
|
+
* @internal
|
|
1322
|
+
*/
|
|
1323
|
+
getFormatContext() {
|
|
1324
|
+
return this.formatContext;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Setup sync queues based on stream configuration.
|
|
1328
|
+
*
|
|
1329
|
+
* Called before writing header.
|
|
1330
|
+
* Muxing sync queue is created only if nb_interleaved > nb_av_enc
|
|
1331
|
+
* (i.e., when there are streamcopy streams).
|
|
1332
|
+
*
|
|
1333
|
+
* All streams are added as non-limiting (FFmpeg default without -shortest),
|
|
1334
|
+
* which means no timestamp-based synchronization - frames are output immediately.
|
|
1335
|
+
*
|
|
1336
|
+
* @internal
|
|
1337
|
+
*/
|
|
1338
|
+
setupSyncQueues() {
|
|
1339
|
+
const nbInterleaved = this._streams.size; // All streams are interleaved (no attachments)
|
|
1340
|
+
const nbAvEnc = Array.from(this._streams.values()).filter((s) => !s.isStreamCopy).length;
|
|
1341
|
+
// FFmpeg's condition: if there are streamcopy streams (nb_interleaved > nb_av_enc),
|
|
1342
|
+
// then ALL streams use the sync queue (but as non-limiting, so no actual sync happens)
|
|
1343
|
+
const needsSyncQueue = this.options.useSyncQueue && nbInterleaved > nbAvEnc;
|
|
1344
|
+
if (needsSyncQueue && !this.syncQueue) {
|
|
1345
|
+
// Create sync queue
|
|
1346
|
+
const bufDurationSec = this.options.syncQueueBufferDuration ?? SYNC_BUFFER_DURATION;
|
|
1347
|
+
const bufSizeUs = bufDurationSec * 1000000; // Convert to microseconds
|
|
1348
|
+
this.syncQueue = SyncQueue.create(SyncQueueType.PACKETS, bufSizeUs);
|
|
1349
|
+
this.sqPacket = new Packet();
|
|
1350
|
+
this.sqPacket.alloc();
|
|
1351
|
+
// Add all streams to sync queue
|
|
1352
|
+
// FFmpeg standard (without -shortest): limiting = 0 (non-limiting)
|
|
1353
|
+
// This means frames are output immediately without synchronization
|
|
1354
|
+
for (const streamInfo of this._streams.values()) {
|
|
1355
|
+
streamInfo.sqIdxMux = this.syncQueue.addStream(0); // 0 = non-limiting
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
else if (!needsSyncQueue && this.syncQueue) {
|
|
1359
|
+
// Free sync queue if we don't need it anymore
|
|
1360
|
+
this.sqPacket?.free();
|
|
1361
|
+
this.sqPacket = undefined;
|
|
1362
|
+
this.syncQueue.free();
|
|
1363
|
+
this.syncQueue = undefined;
|
|
1364
|
+
// Reset all sqIdxMux to -1
|
|
1365
|
+
for (const streamInfo of this._streams.values()) {
|
|
1366
|
+
streamInfo.sqIdxMux = -1;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Flush all PreMuxQueues in DTS-sorted order.
|
|
1372
|
+
*
|
|
1373
|
+
* Implements FFmpeg's PreMuxQueue flush algorithm from mux_task_start().
|
|
1374
|
+
* Repeatedly finds the stream with the earliest DTS packet and sends it:
|
|
1375
|
+
* - WITH SyncQueue: Sends to SyncQueue for interleaving
|
|
1376
|
+
* - WITHOUT SyncQueue: Writes directly to muxer
|
|
1377
|
+
* NULL packets (EOF markers) and packets with AV_NOPTS_VALUE have priority (sent first).
|
|
1378
|
+
*
|
|
1379
|
+
* @internal
|
|
1380
|
+
*/
|
|
1381
|
+
async flushPreMuxQueues() {
|
|
1382
|
+
while (true) {
|
|
1383
|
+
let minStreamInfo = null;
|
|
1384
|
+
let minStreamIndex = -1;
|
|
1385
|
+
let minDts = AV_NOPTS_VALUE;
|
|
1386
|
+
let minTimeBase = { num: 1, den: 1 };
|
|
1387
|
+
// 1. Find stream with earliest DTS across all PreMuxQueues
|
|
1388
|
+
// FFmpeg logic: NULL packets and AV_NOPTS_VALUE packets have priority
|
|
1389
|
+
for (const [streamIndex, streamInfo] of this._streams) {
|
|
1390
|
+
if (streamInfo.preMuxQueue.length === 0) {
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const pkt = streamInfo.preMuxQueue[0]; // Peek at first packet (can be null)
|
|
1394
|
+
// NULL packets (EOF markers) have highest priority (FFmpeg: if (!pkt) -> priority)
|
|
1395
|
+
// Packets with AV_NOPTS_VALUE also have priority
|
|
1396
|
+
if (!pkt || pkt.dts === AV_NOPTS_VALUE) {
|
|
1397
|
+
minStreamInfo = streamInfo;
|
|
1398
|
+
minStreamIndex = streamIndex;
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
// Compare DTS with current minimum
|
|
1402
|
+
if (minDts === AV_NOPTS_VALUE || avCompareTs(pkt.dts, pkt.timeBase, minDts, minTimeBase) < 0) {
|
|
1403
|
+
minStreamInfo = streamInfo;
|
|
1404
|
+
minStreamIndex = streamIndex;
|
|
1405
|
+
minDts = pkt.dts;
|
|
1406
|
+
minTimeBase = pkt.timeBase;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
// 2. No more packets - all queues empty
|
|
1410
|
+
if (!minStreamInfo) {
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1414
|
+
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1415
|
+
// 4. Handle NULL packet (EOF marker)
|
|
1416
|
+
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1417
|
+
if (!pkt) {
|
|
1418
|
+
// Signal EOF to SyncQueue for this stream
|
|
1419
|
+
if (this.syncQueue && minStreamInfo.sqIdxMux >= 0) {
|
|
1420
|
+
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, null);
|
|
1421
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1422
|
+
if (this.options.exitOnError) {
|
|
1423
|
+
FFmpegError.throwIfError(ret, 'Failed to send EOF to sync queue during PreMuxQueue flush');
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// If not using SyncQueue, nothing to do - stream finished without data
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
// 5. Normal packet - update data size and send
|
|
1431
|
+
minStreamInfo.preMuxQueueDataSize -= pkt.size;
|
|
1432
|
+
// 6. Send to SyncQueue or write directly
|
|
1433
|
+
pkt.streamIndex = minStreamIndex;
|
|
1434
|
+
if (this.syncQueue && minStreamInfo.sqIdxMux >= 0) {
|
|
1435
|
+
// Send to SyncQueue for interleaving
|
|
1436
|
+
// NOTE: Do NOT set pkt.timeBase here!
|
|
1437
|
+
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1438
|
+
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1439
|
+
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1440
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1441
|
+
if (this.options.exitOnError) {
|
|
1442
|
+
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
// Write directly to muxer
|
|
1448
|
+
await this.write(pkt, minStreamInfo, minStreamIndex);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
// If using SyncQueue, receive and write all interleaved packets
|
|
1452
|
+
if (this.syncQueue) {
|
|
1453
|
+
while (!this.isClosed) {
|
|
1454
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
1455
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
1456
|
+
break; // No more packets ready
|
|
1457
|
+
}
|
|
1458
|
+
if (recvRet === AVERROR_EOF) {
|
|
1459
|
+
break; // All streams finished
|
|
1460
|
+
}
|
|
1461
|
+
if (recvRet >= 0) {
|
|
1462
|
+
// recvRet is the stream index
|
|
1463
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
1464
|
+
// Clone packet before writing (muxer takes ownership and will unref it)
|
|
1465
|
+
const pkt = this.sqPacket.clone();
|
|
1466
|
+
if (!pkt) {
|
|
1467
|
+
throw new Error('Failed to clone packet from sync queue during PreMuxQueue flush');
|
|
1468
|
+
}
|
|
1469
|
+
pkt.streamIndex = recvRet;
|
|
1470
|
+
// Write packet (muxer takes ownership)
|
|
1471
|
+
await this.write(pkt, recvStreamInfo, recvRet);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Flush all PreMuxQueues in DTS-sorted order (synchronous version).
|
|
1478
|
+
*
|
|
1479
|
+
* Implements FFmpeg's PreMuxQueue flush algorithm from mux_task_start().
|
|
1480
|
+
* Repeatedly finds the stream with the earliest DTS packet and sends it:
|
|
1481
|
+
* - WITH SyncQueue: Sends to SyncQueue for interleaving
|
|
1482
|
+
* - WITHOUT SyncQueue: Writes directly to muxer
|
|
1483
|
+
* NULL packets (EOF markers) and packets with AV_NOPTS_VALUE have priority (sent first).
|
|
1484
|
+
*
|
|
1485
|
+
* @internal
|
|
1486
|
+
*/
|
|
1487
|
+
flushPreMuxQueuesSync() {
|
|
1488
|
+
while (true) {
|
|
1489
|
+
let minStreamInfo = null;
|
|
1490
|
+
let minStreamIndex = -1;
|
|
1491
|
+
let minDts = AV_NOPTS_VALUE;
|
|
1492
|
+
let minTimeBase = { num: 1, den: 1 };
|
|
1493
|
+
// 1. Find stream with earliest DTS across all PreMuxQueues
|
|
1494
|
+
// FFmpeg logic: NULL packets and AV_NOPTS_VALUE packets have priority
|
|
1495
|
+
for (const [streamIndex, streamInfo] of this._streams) {
|
|
1496
|
+
if (streamInfo.preMuxQueue.length === 0)
|
|
1497
|
+
continue;
|
|
1498
|
+
const pkt = streamInfo.preMuxQueue[0]; // Peek at first packet (can be null)
|
|
1499
|
+
// NULL packets (EOF markers) have highest priority (FFmpeg: if (!pkt) -> priority)
|
|
1500
|
+
// Packets with AV_NOPTS_VALUE also have priority
|
|
1501
|
+
if (!pkt || pkt.dts === AV_NOPTS_VALUE) {
|
|
1502
|
+
minStreamInfo = streamInfo;
|
|
1503
|
+
minStreamIndex = streamIndex;
|
|
1504
|
+
break;
|
|
1505
|
+
}
|
|
1506
|
+
// Compare DTS with current minimum
|
|
1507
|
+
if (minDts === AV_NOPTS_VALUE || avCompareTs(pkt.dts, pkt.timeBase, minDts, minTimeBase) < 0) {
|
|
1508
|
+
minStreamInfo = streamInfo;
|
|
1509
|
+
minStreamIndex = streamIndex;
|
|
1510
|
+
minDts = pkt.dts;
|
|
1511
|
+
minTimeBase = pkt.timeBase;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
// 2. No more packets - all queues empty
|
|
1515
|
+
if (!minStreamInfo)
|
|
1516
|
+
break;
|
|
1517
|
+
// 3. Take packet from stream with earliest DTS (or NULL for EOF)
|
|
1518
|
+
const pkt = minStreamInfo.preMuxQueue.shift();
|
|
1519
|
+
// 4. Handle NULL packet (EOF marker)
|
|
1520
|
+
// FFmpeg: if (pkt) { send packet } else { tq_send_finish() }
|
|
1521
|
+
if (!pkt) {
|
|
1522
|
+
// Signal EOF to SyncQueue for this stream
|
|
1523
|
+
if (this.syncQueue && minStreamInfo.sqIdxMux >= 0) {
|
|
1524
|
+
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, null);
|
|
1525
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1526
|
+
if (this.options.exitOnError) {
|
|
1527
|
+
FFmpegError.throwIfError(ret, 'Failed to send EOF to sync queue during PreMuxQueue flush');
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
// If not using SyncQueue, nothing to do - stream finished without data
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
// 5. Normal packet - update data size and send
|
|
1535
|
+
minStreamInfo.preMuxQueueDataSize -= pkt.size;
|
|
1536
|
+
// 6. Send to SyncQueue or write directly
|
|
1537
|
+
pkt.streamIndex = minStreamIndex;
|
|
1538
|
+
if (this.syncQueue && minStreamInfo.sqIdxMux >= 0) {
|
|
1539
|
+
// Send to SyncQueue for interleaving
|
|
1540
|
+
// NOTE: Do NOT set pkt.timeBase here!
|
|
1541
|
+
// Packet must keep its source timebase so muxFixupTs can rescale correctly
|
|
1542
|
+
// pkt.timeBase = minStreamInfo.stream.timeBase; // ❌ WRONG!
|
|
1543
|
+
const ret = this.syncQueue.send(minStreamInfo.sqIdxMux, pkt);
|
|
1544
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1545
|
+
if (this.options.exitOnError) {
|
|
1546
|
+
FFmpegError.throwIfError(ret, 'Failed to send packet to sync queue during PreMuxQueue flush');
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
// Write directly to muxer
|
|
1552
|
+
this.writeSync(pkt, minStreamInfo, minStreamIndex);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
// If using SyncQueue, receive and write all interleaved packets
|
|
1556
|
+
if (this.syncQueue) {
|
|
1557
|
+
while (!this.isClosed) {
|
|
1558
|
+
const recvRet = this.syncQueue.receive(-1, this.sqPacket);
|
|
1559
|
+
if (recvRet === AVERROR_EAGAIN) {
|
|
1560
|
+
break; // No more packets ready
|
|
1561
|
+
}
|
|
1562
|
+
if (recvRet === AVERROR_EOF) {
|
|
1563
|
+
break; // All streams finished
|
|
1564
|
+
}
|
|
1565
|
+
if (recvRet >= 0) {
|
|
1566
|
+
// recvRet is the stream index
|
|
1567
|
+
const recvStreamInfo = this._streams.get(recvRet);
|
|
1568
|
+
// Clone packet before writing (muxer takes ownership and will unref it)
|
|
1569
|
+
const pkt = this.sqPacket.clone();
|
|
1570
|
+
if (!pkt) {
|
|
1571
|
+
throw new Error('Failed to clone packet from sync queue during PreMuxQueue flush');
|
|
1572
|
+
}
|
|
1573
|
+
pkt.streamIndex = recvRet;
|
|
1574
|
+
// Write packet (muxer takes ownership)
|
|
1575
|
+
this.writeSync(pkt, recvStreamInfo, recvRet);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Write a packet to the output.
|
|
1582
|
+
*
|
|
1583
|
+
* @param pkt - Packet to write
|
|
1584
|
+
*
|
|
1585
|
+
* @param streamInfo - Stream description
|
|
1586
|
+
*
|
|
1587
|
+
* @param streamIndex - Stream index
|
|
1588
|
+
*
|
|
1589
|
+
* @internal
|
|
1590
|
+
*/
|
|
1591
|
+
async write(pkt, streamInfo, streamIndex) {
|
|
1592
|
+
if (this.writeQueue) {
|
|
1593
|
+
// Use async queue for serialized writes
|
|
1594
|
+
await this.writeQueue.send({ pkt, streamInfo, streamIndex });
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
// Direct write without serialization
|
|
1598
|
+
await this.writeInternal(pkt, streamInfo, streamIndex);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Internal write implementation.
|
|
1603
|
+
* Called either directly or through the write worker.
|
|
1604
|
+
*
|
|
1605
|
+
* @param pkt - Packet to write
|
|
1606
|
+
*
|
|
1607
|
+
* @param streamInfo - Stream description
|
|
1608
|
+
*
|
|
1609
|
+
* @param streamIndex - Stream index
|
|
1610
|
+
*
|
|
1611
|
+
* @internal
|
|
1612
|
+
*/
|
|
1613
|
+
async writeInternal(pkt, streamInfo, streamIndex) {
|
|
1614
|
+
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
1615
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex);
|
|
1616
|
+
// Write the packet (muxer takes ownership and will unref it)
|
|
1617
|
+
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
1618
|
+
const ret = await this.formatContext.interleavedWriteFrame(pkt);
|
|
1619
|
+
// Handle write errors
|
|
1620
|
+
if (ret < 0 && ret !== AVERROR_EOF) {
|
|
1621
|
+
if (this.options.exitOnError) {
|
|
1622
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Start background worker for async write queue.
|
|
1628
|
+
* Processes write jobs sequentially to prevent race conditions.
|
|
1629
|
+
*
|
|
1630
|
+
* @internal
|
|
1631
|
+
*/
|
|
1632
|
+
startWriteWorker() {
|
|
1633
|
+
if (!this.options.useAsyncWrite || this._streams.size <= 1) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
this.writeQueue ??= new AsyncQueue(1); // size=1 for strict serialization
|
|
1637
|
+
this.writeWorkerPromise ??= (async () => {
|
|
1638
|
+
while (true) {
|
|
1639
|
+
const job = await this.writeQueue.receive();
|
|
1640
|
+
if (!job)
|
|
1641
|
+
break; // Queue closed
|
|
1642
|
+
await this.writeInternal(job.pkt, job.streamInfo, job.streamIndex);
|
|
1643
|
+
}
|
|
1644
|
+
})();
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Write a packet to the output synchronously.
|
|
1648
|
+
* Synchronous version of write.
|
|
1649
|
+
*
|
|
1650
|
+
* @param pkt - Packet to write
|
|
1651
|
+
*
|
|
1652
|
+
* @param streamInfo - Stream description
|
|
1653
|
+
*
|
|
1654
|
+
* @param streamIndex - Stream index
|
|
1655
|
+
*
|
|
1656
|
+
* @internal
|
|
1657
|
+
*/
|
|
1658
|
+
writeSync(pkt, streamInfo, streamIndex) {
|
|
1659
|
+
// Fix timestamps (rescale, DTS>PTS fix, monotonic DTS enforcement)
|
|
1660
|
+
this.muxFixupTs(pkt, streamInfo, streamIndex);
|
|
1661
|
+
// Write the packet (muxer takes ownership and will unref it)
|
|
1662
|
+
// NOTE: Caller must clone packet if they need to keep it (e.g., for SyncQueue)
|
|
1663
|
+
const ret = this.formatContext.interleavedWriteFrameSync(pkt);
|
|
1664
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Streamcopy packet filtering and timestamp offset.
|
|
1668
|
+
*
|
|
1669
|
+
* Applies streamcopy-specific logic before muxing:
|
|
1670
|
+
* 1. Recording time limit check
|
|
1671
|
+
* 2. Skip non-keyframe packets at start (unless copyInitialNonkeyframes)
|
|
1672
|
+
* 3. Skip packets before ts_copy_start (unless copyPriorStart)
|
|
1673
|
+
* 4. Skip packets before startTime
|
|
1674
|
+
* 5. Apply start_time timestamp offset
|
|
1675
|
+
*
|
|
1676
|
+
* @param pkt - Packet to process
|
|
1677
|
+
*
|
|
1678
|
+
* @param streamInfo - Stream description
|
|
1679
|
+
*
|
|
1680
|
+
* @param streamIndex - Stream index
|
|
1681
|
+
*
|
|
1682
|
+
* @returns true if packet should be written, false if packet should be skipped
|
|
1683
|
+
*
|
|
1684
|
+
* @throws {Error} If recording time limit reached
|
|
1685
|
+
*
|
|
1686
|
+
* @internal
|
|
1687
|
+
*/
|
|
1688
|
+
ofStreamcopy(pkt, streamInfo, streamIndex) {
|
|
1689
|
+
const outputStream = this.formatContext.streams[streamIndex];
|
|
1690
|
+
if (!outputStream) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
// Get DTS in AV_TIME_BASE for comparison
|
|
1694
|
+
// Use packet DTS directly
|
|
1695
|
+
const dts = pkt.dts !== AV_NOPTS_VALUE ? avRescaleQ(pkt.dts, pkt.timeBase, AV_TIME_BASE_Q) : AV_NOPTS_VALUE;
|
|
1696
|
+
const startTimeUs = this.options.startTime !== undefined ? BigInt(Math.floor(this.options.startTime * 1000000)) : AV_NOPTS_VALUE;
|
|
1697
|
+
// 1. Skip non-keyframes at start
|
|
1698
|
+
const copyInitialNonkeyframes = this.options.copyInitialNonkeyframes ?? false;
|
|
1699
|
+
if (!streamInfo.streamcopyStarted && !pkt.isKeyframe && !copyInitialNonkeyframes) {
|
|
1700
|
+
return false; // skip packet
|
|
1701
|
+
}
|
|
1702
|
+
// 2. Copy from specific start point
|
|
1703
|
+
if (!streamInfo.streamcopyStarted) {
|
|
1704
|
+
const copyPriorStart = this.options.copyPriorStart ?? -1;
|
|
1705
|
+
// Calculate ts_copy_start
|
|
1706
|
+
// Since we don't have input file timestamps, ts_copy_start is simply startTime or 0
|
|
1707
|
+
const tsCopyStart = startTimeUs !== AV_NOPTS_VALUE ? startTimeUs : 0n;
|
|
1708
|
+
// Only check ts_copy_start if copyPriorStart is not set (0 or -1)
|
|
1709
|
+
if (copyPriorStart !== 1 && tsCopyStart > 0n) {
|
|
1710
|
+
const pktTsUs = pkt.pts !== AV_NOPTS_VALUE ? avRescaleQ(pkt.pts, pkt.timeBase, AV_TIME_BASE_Q) : dts;
|
|
1711
|
+
if (pktTsUs !== AV_NOPTS_VALUE && pktTsUs < tsCopyStart) {
|
|
1712
|
+
return false; // skip packet
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
// 3. Skip packets before startTime
|
|
1716
|
+
if (startTimeUs !== AV_NOPTS_VALUE && dts !== AV_NOPTS_VALUE && dts < startTimeUs) {
|
|
1717
|
+
return false; // skip packet
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
// 4. Apply start_time timestamp offset
|
|
1721
|
+
// FFmpeg uses: start_time = (of->start_time == AV_NOPTS_VALUE) ? 0 : of->start_time
|
|
1722
|
+
const startForOffset = startTimeUs !== AV_NOPTS_VALUE ? startTimeUs : 0n;
|
|
1723
|
+
const tsOffset = avRescaleQ(startForOffset, AV_TIME_BASE_Q, pkt.timeBase);
|
|
1724
|
+
if (pkt.pts !== AV_NOPTS_VALUE) {
|
|
1725
|
+
pkt.pts -= tsOffset;
|
|
1726
|
+
}
|
|
1727
|
+
if (pkt.dts === AV_NOPTS_VALUE) {
|
|
1728
|
+
// If DTS missing, use our estimated DTS
|
|
1729
|
+
if (dts !== AV_NOPTS_VALUE) {
|
|
1730
|
+
pkt.dts = avRescaleQ(dts, AV_TIME_BASE_Q, pkt.timeBase);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
else if (outputStream.codecpar.codecType === AVMEDIA_TYPE_AUDIO) {
|
|
1734
|
+
// Audio: PTS = DTS - ts_offset
|
|
1735
|
+
pkt.pts = pkt.dts - tsOffset;
|
|
1736
|
+
}
|
|
1737
|
+
if (pkt.dts !== AV_NOPTS_VALUE) {
|
|
1738
|
+
pkt.dts -= tsOffset;
|
|
1739
|
+
}
|
|
1740
|
+
// Mark streamcopy as started
|
|
1741
|
+
streamInfo.streamcopyStarted = true;
|
|
1742
|
+
return true; // Packet should be written
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Fix packet timestamps before muxing.
|
|
1746
|
+
*
|
|
1747
|
+
* Performs timestamp corrections:
|
|
1748
|
+
* 1. Rescales timestamps to output timebase (av_rescale_delta for audio streamcopy)
|
|
1749
|
+
* 2. Sets pkt.timeBase to output stream timebase
|
|
1750
|
+
* 3. Fixes invalid DTS > PTS relationships
|
|
1751
|
+
* 4. Enforces monotonic DTS (never decreasing)
|
|
1752
|
+
*
|
|
1753
|
+
* @param pkt - Packet to fix
|
|
1754
|
+
*
|
|
1755
|
+
* @param streamInfo - Stream description
|
|
1756
|
+
*
|
|
1757
|
+
* @param streamIndex - Stream index
|
|
1758
|
+
*
|
|
1759
|
+
* @internal
|
|
1760
|
+
*/
|
|
1761
|
+
muxFixupTs(pkt, streamInfo, streamIndex) {
|
|
1762
|
+
const outputStream = this.formatContext.streams[streamIndex];
|
|
1763
|
+
if (!outputStream)
|
|
1764
|
+
return;
|
|
1765
|
+
const codecType = streamInfo.outputStream.codecpar.codecType;
|
|
1766
|
+
const dstTb = outputStream.timeBase;
|
|
1767
|
+
// const srcTb = streamInfo.sourceTimeBase!;
|
|
1768
|
+
// Check if timestamps are valid before rescaling
|
|
1769
|
+
// FFmpeg's av_rescale_q/av_rescale_delta don't accept AV_NOPTS_VALUE
|
|
1770
|
+
if (pkt.dts === AV_NOPTS_VALUE && pkt.pts === AV_NOPTS_VALUE) {
|
|
1771
|
+
// Set packet timebase anyway for muxer
|
|
1772
|
+
pkt.timeBase = dstTb;
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
// 1. Rescale timestamps to the stream timebase
|
|
1776
|
+
if (codecType === AVMEDIA_TYPE_AUDIO && streamInfo.isStreamCopy) {
|
|
1777
|
+
let duration = avGetAudioFrameDuration2(streamInfo.outputStream.codecpar, pkt.size);
|
|
1778
|
+
if (!duration) {
|
|
1779
|
+
duration = streamInfo.outputStream.codecpar.frameSize;
|
|
1780
|
+
}
|
|
1781
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
1782
|
+
const sampleRate = streamInfo.outputStream.codecpar.sampleRate;
|
|
1783
|
+
const fsTb = { num: 1, den: sampleRate };
|
|
1784
|
+
pkt.dts = avRescaleDelta(srcTb, pkt.dts, fsTb, duration, streamInfo.tsRescaleDeltaLast, dstTb);
|
|
1785
|
+
pkt.pts = pkt.dts;
|
|
1786
|
+
pkt.duration = avRescaleQ(pkt.duration, srcTb, dstTb);
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
// For video or encoded audio, use regular rescaling
|
|
1790
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
1791
|
+
pkt.rescaleTs(srcTb, dstTb);
|
|
1792
|
+
}
|
|
1793
|
+
// 2. Set packet timeBase
|
|
1794
|
+
// av_interleaved_write_frame uses this for sorting!
|
|
1795
|
+
pkt.timeBase = dstTb;
|
|
1796
|
+
// 3. Fix DTS > PTS (invalid relationship)
|
|
1797
|
+
// FFmpeg formula: median of (pts, dts, last_mux_dts+1)
|
|
1798
|
+
if (pkt.dts !== AV_NOPTS_VALUE && pkt.pts !== AV_NOPTS_VALUE && pkt.dts > pkt.pts) {
|
|
1799
|
+
const last = streamInfo.lastMuxDts !== AV_NOPTS_VALUE ? streamInfo.lastMuxDts + 1n : 0n;
|
|
1800
|
+
const min = pkt.pts < pkt.dts ? (pkt.pts < last ? pkt.pts : last) : pkt.dts < last ? pkt.dts : last;
|
|
1801
|
+
const max = pkt.pts > pkt.dts ? (pkt.pts > last ? pkt.pts : last) : pkt.dts > last ? pkt.dts : last;
|
|
1802
|
+
const median = pkt.pts + pkt.dts + last - min - max;
|
|
1803
|
+
pkt.pts = median;
|
|
1804
|
+
pkt.dts = median;
|
|
1805
|
+
}
|
|
1806
|
+
// 4. Enforce monotonic DTS
|
|
1807
|
+
if ((codecType === AVMEDIA_TYPE_AUDIO || codecType === AVMEDIA_TYPE_VIDEO) && pkt.dts !== AV_NOPTS_VALUE && streamInfo.lastMuxDts !== AV_NOPTS_VALUE) {
|
|
1808
|
+
// FFmpeg: max = last_mux_dts + !(oformat->flags & AVFMT_TS_NONSTRICT)
|
|
1809
|
+
// AVFMT_TS_NONSTRICT allows non-strict monotonic timestamps (equal DTS is OK)
|
|
1810
|
+
const tsNonStrict = this.formatContext.oformat?.hasFlags(AVFMT_TS_NONSTRICT) ?? false;
|
|
1811
|
+
const max = streamInfo.lastMuxDts + (tsNonStrict ? 0n : 1n);
|
|
1812
|
+
if (pkt.dts < max) {
|
|
1813
|
+
// Adjust PTS if it would create invalid relationship
|
|
1814
|
+
if (pkt.pts !== AV_NOPTS_VALUE && pkt.pts >= pkt.dts) {
|
|
1815
|
+
pkt.pts = pkt.pts > max ? pkt.pts : max;
|
|
1816
|
+
}
|
|
1817
|
+
pkt.dts = max;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
// 5. Update last mux DTS for next packet
|
|
1821
|
+
streamInfo.lastMuxDts = pkt.dts;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Copy container metadata from input to output.
|
|
1825
|
+
*
|
|
1826
|
+
* Automatically copies global metadata from input Demuxer to output format context.
|
|
1827
|
+
* Only copies once (on first call). Removes duration/creation_time metadata.
|
|
1828
|
+
*
|
|
1829
|
+
* @internal
|
|
1830
|
+
*/
|
|
1831
|
+
copyContainerMetadata() {
|
|
1832
|
+
if (this.containerMetadataCopied || !this.options.input) {
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
const mediaInput = 'input' in this.options.input ? this.options.input.input : this.options.input;
|
|
1836
|
+
const inputFormatContext = mediaInput.getFormatContext();
|
|
1837
|
+
const inputMetadata = inputFormatContext.metadata;
|
|
1838
|
+
if (inputMetadata) {
|
|
1839
|
+
// Keys that FFmpeg removes after copying
|
|
1840
|
+
const keysToSkip = new Set(['duration', 'creation_time', 'company_name', 'product_name', 'product_version']);
|
|
1841
|
+
// Get all input metadata entries
|
|
1842
|
+
const entries = inputMetadata.getAll();
|
|
1843
|
+
// Filter out keys that should be skipped
|
|
1844
|
+
const filteredEntries = {};
|
|
1845
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1846
|
+
if (!keysToSkip.has(key)) {
|
|
1847
|
+
filteredEntries[key] = value;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// Create new dictionary with filtered entries
|
|
1851
|
+
const metadata = Dictionary.fromObject(filteredEntries);
|
|
1852
|
+
// Set metadata to format context
|
|
1853
|
+
// This will copy the dictionary content via av_dict_copy
|
|
1854
|
+
this.formatContext.metadata = metadata;
|
|
1855
|
+
}
|
|
1856
|
+
this.containerMetadataCopied = true;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Auto-set DEFAULT disposition for first stream of each type.
|
|
1860
|
+
*
|
|
1861
|
+
* FFmpeg automatically sets DEFAULT flag for the first stream of each type
|
|
1862
|
+
* if no stream of that type has DEFAULT set yet.
|
|
1863
|
+
*
|
|
1864
|
+
* @internal
|
|
1865
|
+
*/
|
|
1866
|
+
updateDefaultDisposition() {
|
|
1867
|
+
// Group streams by media type
|
|
1868
|
+
const streamsByType = new Map();
|
|
1869
|
+
for (const streamInfo of this._streams.values()) {
|
|
1870
|
+
const codecType = streamInfo.outputStream.codecpar.codecType;
|
|
1871
|
+
if (!streamsByType.has(codecType)) {
|
|
1872
|
+
streamsByType.set(codecType, []);
|
|
1873
|
+
}
|
|
1874
|
+
streamsByType.get(codecType).push(streamInfo.outputStream);
|
|
1875
|
+
}
|
|
1876
|
+
// For each media type, check if any stream has DEFAULT disposition
|
|
1877
|
+
// If not, set DEFAULT on first stream
|
|
1878
|
+
for (const [_, streams] of streamsByType.entries()) {
|
|
1879
|
+
// Skip if only one stream of this type
|
|
1880
|
+
if (streams.length < 2) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
// Check if any stream already has DEFAULT disposition
|
|
1884
|
+
const hasDefault = streams.some((s) => s.hasDisposition(AV_DISPOSITION_DEFAULT));
|
|
1885
|
+
if (!hasDefault) {
|
|
1886
|
+
// Find first stream that is not an attached picture
|
|
1887
|
+
const firstNonAttachedPic = streams.find((s) => !s.hasDisposition(AV_DISPOSITION_ATTACHED_PIC));
|
|
1888
|
+
if (firstNonAttachedPic) {
|
|
1889
|
+
// Set DEFAULT on first non-attached-picture stream
|
|
1890
|
+
firstNonAttachedPic.setDisposition(AV_DISPOSITION_DEFAULT);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Dispose of muxer.
|
|
1897
|
+
*
|
|
1898
|
+
* Implements AsyncDisposable interface for automatic cleanup.
|
|
1899
|
+
* Equivalent to calling close().
|
|
1900
|
+
*
|
|
1901
|
+
* @example
|
|
1902
|
+
* ```typescript
|
|
1903
|
+
* {
|
|
1904
|
+
* await using output = await Muxer.open('output.mp4');
|
|
1905
|
+
* // Use output...
|
|
1906
|
+
* } // Automatically closed
|
|
1907
|
+
* ```
|
|
1908
|
+
*
|
|
1909
|
+
* @see {@link close} For manual cleanup
|
|
1910
|
+
*/
|
|
1911
|
+
async [Symbol.asyncDispose]() {
|
|
1912
|
+
await this.close();
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Dispose of muxer synchronously.
|
|
1916
|
+
*
|
|
1917
|
+
* Implements Disposable interface for automatic cleanup.
|
|
1918
|
+
* Equivalent to calling closeSync().
|
|
1919
|
+
*
|
|
1920
|
+
* @example
|
|
1921
|
+
* ```typescript
|
|
1922
|
+
* {
|
|
1923
|
+
* using output = Muxer.openSync('output.mp4');
|
|
1924
|
+
* // Use output...
|
|
1925
|
+
* } // Automatically closed
|
|
1926
|
+
* ```
|
|
1927
|
+
*
|
|
1928
|
+
* @see {@link closeSync} For manual cleanup
|
|
1929
|
+
*/
|
|
1930
|
+
[Symbol.dispose]() {
|
|
1931
|
+
this.closeSync();
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
//# sourceMappingURL=muxer.js.map
|