node-av 1.3.0 → 2.1.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 +47 -40
- package/binding.gyp +12 -0
- package/dist/api/bitstream-filter.d.ts +134 -2
- package/dist/api/bitstream-filter.js +200 -2
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +261 -105
- package/dist/api/decoder.js +384 -171
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +338 -74
- package/dist/api/encoder.js +546 -188
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-presets.d.ts +479 -1513
- package/dist/api/filter-presets.js +1044 -2005
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +370 -150
- package/dist/api/filter.js +647 -364
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +25 -31
- package/dist/api/hardware.js +36 -70
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +6 -0
- package/dist/api/io-stream.js +2 -0
- package/dist/api/io-stream.js.map +1 -1
- package/dist/api/media-input.d.ts +208 -2
- package/dist/api/media-input.js +356 -8
- package/dist/api/media-input.js.map +1 -1
- package/dist/api/media-output.d.ts +142 -104
- package/dist/api/media-output.js +446 -179
- package/dist/api/media-output.js.map +1 -1
- package/dist/api/pipeline.d.ts +82 -17
- package/dist/api/pipeline.js +80 -42
- package/dist/api/pipeline.js.map +1 -1
- package/dist/api/types.d.ts +24 -57
- package/dist/api/utils.js +2 -0
- package/dist/api/utils.js.map +1 -1
- package/dist/lib/audio-fifo.d.ts +103 -0
- package/dist/lib/audio-fifo.js +109 -0
- package/dist/lib/audio-fifo.js.map +1 -1
- package/dist/lib/binding.d.ts +1 -0
- package/dist/lib/binding.js.map +1 -1
- package/dist/lib/bitstream-filter-context.d.ts +79 -0
- package/dist/lib/bitstream-filter-context.js +83 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -1
- package/dist/lib/bitstream-filter.d.ts +2 -0
- package/dist/lib/bitstream-filter.js +2 -0
- package/dist/lib/bitstream-filter.js.map +1 -1
- package/dist/lib/codec-context.d.ts +168 -0
- package/dist/lib/codec-context.js +178 -0
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/codec-parameters.d.ts +3 -0
- package/dist/lib/codec-parameters.js +3 -0
- package/dist/lib/codec-parameters.js.map +1 -1
- package/dist/lib/codec-parser.d.ts +6 -0
- package/dist/lib/codec-parser.js +6 -0
- package/dist/lib/codec-parser.js.map +1 -1
- package/dist/lib/codec.d.ts +12 -0
- package/dist/lib/codec.js +12 -0
- package/dist/lib/codec.js.map +1 -1
- package/dist/lib/dictionary.d.ts +18 -2
- package/dist/lib/dictionary.js +18 -2
- package/dist/lib/dictionary.js.map +1 -1
- package/dist/lib/error.d.ts +8 -0
- package/dist/lib/error.js +9 -0
- package/dist/lib/error.js.map +1 -1
- package/dist/lib/filter-context.d.ts +119 -2
- package/dist/lib/filter-context.js +119 -0
- package/dist/lib/filter-context.js.map +1 -1
- package/dist/lib/filter-graph.d.ts +80 -0
- package/dist/lib/filter-graph.js +84 -0
- package/dist/lib/filter-graph.js.map +1 -1
- package/dist/lib/filter-inout.d.ts +1 -0
- package/dist/lib/filter-inout.js +1 -0
- package/dist/lib/filter-inout.js.map +1 -1
- package/dist/lib/filter.d.ts +2 -0
- package/dist/lib/filter.js +2 -0
- package/dist/lib/filter.js.map +1 -1
- package/dist/lib/format-context.d.ts +356 -20
- package/dist/lib/format-context.js +375 -23
- package/dist/lib/format-context.js.map +1 -1
- package/dist/lib/frame.d.ts +84 -1
- package/dist/lib/frame.js +96 -0
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/hardware-device-context.d.ts +8 -0
- package/dist/lib/hardware-device-context.js +8 -0
- package/dist/lib/hardware-device-context.js.map +1 -1
- package/dist/lib/hardware-frames-context.d.ts +55 -0
- package/dist/lib/hardware-frames-context.js +57 -0
- package/dist/lib/hardware-frames-context.js.map +1 -1
- package/dist/lib/input-format.d.ts +43 -3
- package/dist/lib/input-format.js +48 -0
- package/dist/lib/input-format.js.map +1 -1
- package/dist/lib/io-context.d.ts +212 -0
- package/dist/lib/io-context.js +228 -0
- package/dist/lib/io-context.js.map +1 -1
- package/dist/lib/log.d.ts +2 -0
- package/dist/lib/log.js +2 -0
- package/dist/lib/log.js.map +1 -1
- package/dist/lib/native-types.d.ts +39 -1
- package/dist/lib/option.d.ts +90 -0
- package/dist/lib/option.js +97 -0
- package/dist/lib/option.js.map +1 -1
- package/dist/lib/output-format.d.ts +4 -0
- package/dist/lib/output-format.js +4 -0
- package/dist/lib/output-format.js.map +1 -1
- package/dist/lib/packet.d.ts +7 -0
- package/dist/lib/packet.js +7 -0
- package/dist/lib/packet.js.map +1 -1
- package/dist/lib/rational.d.ts +1 -0
- package/dist/lib/rational.js +1 -0
- package/dist/lib/rational.js.map +1 -1
- package/dist/lib/software-resample-context.d.ts +64 -0
- package/dist/lib/software-resample-context.js +66 -0
- package/dist/lib/software-resample-context.js.map +1 -1
- package/dist/lib/software-scale-context.d.ts +98 -0
- package/dist/lib/software-scale-context.js +102 -0
- package/dist/lib/software-scale-context.js.map +1 -1
- package/dist/lib/stream.d.ts +1 -0
- package/dist/lib/stream.js +1 -0
- package/dist/lib/stream.js.map +1 -1
- package/dist/lib/utilities.d.ts +60 -0
- package/dist/lib/utilities.js +60 -0
- package/dist/lib/utilities.js.map +1 -1
- package/package.json +18 -18
- package/release_notes.md +0 -29
package/dist/api/media-output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdirSync } from 'fs';
|
|
1
2
|
import { mkdir } from 'fs/promises';
|
|
2
3
|
import { dirname, resolve } from 'path';
|
|
3
4
|
import { AVFMT_FLAG_CUSTOM_IO, AVFMT_NOFILE, AVIO_FLAG_WRITE } from '../constants/constants.js';
|
|
@@ -7,6 +8,8 @@ import { Encoder } from './encoder.js';
|
|
|
7
8
|
* High-level media output for writing and muxing media files.
|
|
8
9
|
*
|
|
9
10
|
* Provides simplified access to media muxing and file writing operations.
|
|
11
|
+
* Automatically manages header and trailer writing - header is written on first packet,
|
|
12
|
+
* trailer is written on close. Supports lazy initialization for both encoders and streams.
|
|
10
13
|
* Handles stream configuration, packet writing, and format management.
|
|
11
14
|
* Supports files, URLs, and custom I/O with automatic cleanup.
|
|
12
15
|
* Essential component for media encoding pipelines and transcoding.
|
|
@@ -22,14 +25,11 @@ import { Encoder } from './encoder.js';
|
|
|
22
25
|
* const videoIdx = output.addStream(videoEncoder);
|
|
23
26
|
* const audioIdx = output.addStream(audioEncoder);
|
|
24
27
|
*
|
|
25
|
-
* // Write header
|
|
26
|
-
* await output.writeHeader();
|
|
27
|
-
*
|
|
28
|
-
* // Write packets
|
|
28
|
+
* // Write packets - header written automatically on first packet
|
|
29
29
|
* await output.writePacket(packet, videoIdx);
|
|
30
30
|
*
|
|
31
|
-
* //
|
|
32
|
-
* await
|
|
31
|
+
* // Close - trailer written automatically
|
|
32
|
+
* // (automatic with await using)
|
|
33
33
|
* ```
|
|
34
34
|
*
|
|
35
35
|
* @example
|
|
@@ -40,8 +40,8 @@ import { Encoder } from './encoder.js';
|
|
|
40
40
|
*
|
|
41
41
|
* // Copy stream configuration
|
|
42
42
|
* const videoIdx = output.addStream(input.video());
|
|
43
|
-
* await output.writeHeader();
|
|
44
43
|
*
|
|
44
|
+
* // Process packets - header/trailer handled automatically
|
|
45
45
|
* for await (const packet of input.packets()) {
|
|
46
46
|
* await output.writePacket(packet, videoIdx);
|
|
47
47
|
* packet.free();
|
|
@@ -58,7 +58,8 @@ export class MediaOutput {
|
|
|
58
58
|
ioContext;
|
|
59
59
|
headerWritten = false;
|
|
60
60
|
trailerWritten = false;
|
|
61
|
-
|
|
61
|
+
isClosed = false;
|
|
62
|
+
headerWritePromise;
|
|
62
63
|
/**
|
|
63
64
|
* @internal
|
|
64
65
|
*/
|
|
@@ -75,7 +76,9 @@ export class MediaOutput {
|
|
|
75
76
|
* Direct mapping to avformat_alloc_output_context2() and avio_open2().
|
|
76
77
|
*
|
|
77
78
|
* @param target - File path, URL, or I/O callbacks
|
|
79
|
+
*
|
|
78
80
|
* @param options - Output configuration options
|
|
81
|
+
*
|
|
79
82
|
* @returns Opened media output instance
|
|
80
83
|
*
|
|
81
84
|
* @throws {Error} If format required for custom I/O
|
|
@@ -193,25 +196,142 @@ export class MediaOutput {
|
|
|
193
196
|
throw error;
|
|
194
197
|
}
|
|
195
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Open media output for writing synchronously.
|
|
201
|
+
* Synchronous version of open.
|
|
202
|
+
*
|
|
203
|
+
* Creates and configures output context for muxing.
|
|
204
|
+
* Automatically creates directories for file output.
|
|
205
|
+
* Supports files, URLs, and custom I/O callbacks.
|
|
206
|
+
*
|
|
207
|
+
* Direct mapping to avformat_alloc_output_context2() and avio_open2().
|
|
208
|
+
*
|
|
209
|
+
* @param target - File path, URL, or I/O callbacks
|
|
210
|
+
*
|
|
211
|
+
* @param options - Output configuration options
|
|
212
|
+
*
|
|
213
|
+
* @returns Opened media output instance
|
|
214
|
+
*
|
|
215
|
+
* @throws {Error} If format required for custom I/O
|
|
216
|
+
*
|
|
217
|
+
* @throws {FFmpegError} If allocation or opening fails
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // Create file output
|
|
222
|
+
* using output = MediaOutput.openSync('output.mp4');
|
|
223
|
+
* ```
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* // Create output with specific format
|
|
228
|
+
* using output = MediaOutput.openSync('output.ts', {
|
|
229
|
+
* format: 'mpegts'
|
|
230
|
+
* });
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @see {@link open} For async version
|
|
234
|
+
*/
|
|
235
|
+
static openSync(target, options) {
|
|
236
|
+
const output = new MediaOutput();
|
|
237
|
+
try {
|
|
238
|
+
if (typeof target === 'string') {
|
|
239
|
+
// File or stream URL - resolve relative paths and create directories
|
|
240
|
+
// Check if it's a URL (starts with protocol://) or a file path
|
|
241
|
+
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target);
|
|
242
|
+
const resolvedTarget = isUrl ? target : resolve(target);
|
|
243
|
+
// Create directory structure for local files (not URLs)
|
|
244
|
+
if (!isUrl && target !== '') {
|
|
245
|
+
const dir = dirname(resolvedTarget);
|
|
246
|
+
mkdirSync(dir, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
// Allocate output context
|
|
249
|
+
const ret = output.formatContext.allocOutputContext2(null, options?.format ?? null, resolvedTarget === '' ? null : resolvedTarget);
|
|
250
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
251
|
+
// Check if we need to open IO
|
|
252
|
+
const oformat = output.formatContext.oformat;
|
|
253
|
+
if (resolvedTarget && oformat && !(oformat.flags & AVFMT_NOFILE)) {
|
|
254
|
+
// For file-based formats, we need to open the file using avio_open2
|
|
255
|
+
// FFmpeg will manage the AVIOContext internally
|
|
256
|
+
output.ioContext = new IOContext();
|
|
257
|
+
const openRet = output.ioContext.open2Sync(resolvedTarget, AVIO_FLAG_WRITE);
|
|
258
|
+
FFmpegError.throwIfError(openRet, `Failed to open output file: ${resolvedTarget}`);
|
|
259
|
+
output.formatContext.pb = output.ioContext;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Custom IO with callbacks - format is required
|
|
264
|
+
if (!options?.format) {
|
|
265
|
+
throw new Error('Format must be specified for custom IO');
|
|
266
|
+
}
|
|
267
|
+
const ret = output.formatContext.allocOutputContext2(null, options.format, null);
|
|
268
|
+
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
269
|
+
// Setup custom IO with callbacks
|
|
270
|
+
output.ioContext = new IOContext();
|
|
271
|
+
output.ioContext.allocContextWithCallbacks(options.bufferSize ?? 4096, 1, target.read, target.write, target.seek);
|
|
272
|
+
output.ioContext.maxPacketSize = options.bufferSize ?? 4096;
|
|
273
|
+
output.formatContext.pb = output.ioContext;
|
|
274
|
+
output.formatContext.flags = AVFMT_FLAG_CUSTOM_IO;
|
|
275
|
+
}
|
|
276
|
+
return output;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
// Cleanup on error
|
|
280
|
+
if (output.ioContext) {
|
|
281
|
+
try {
|
|
282
|
+
const isCustomIO = (output.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
283
|
+
if (isCustomIO) {
|
|
284
|
+
// Clear the pb reference first
|
|
285
|
+
output.formatContext.pb = null;
|
|
286
|
+
// For custom IO with callbacks, free the context
|
|
287
|
+
output.ioContext.freeContext();
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// For file-based IO, close the file handle
|
|
291
|
+
output.ioContext.closepSync();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Ignore errors
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (output.formatContext) {
|
|
299
|
+
try {
|
|
300
|
+
output.formatContext.freeContext();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Ignore errors
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
196
309
|
/**
|
|
197
310
|
* Add a stream to the output.
|
|
198
311
|
*
|
|
199
312
|
* Configures output stream from encoder or input stream.
|
|
200
|
-
* Must be called before
|
|
313
|
+
* Must be called before writing any packets.
|
|
201
314
|
* Returns stream index for packet writing.
|
|
202
315
|
*
|
|
203
|
-
*
|
|
316
|
+
* Streams are initialized lazily - codec parameters are configured
|
|
317
|
+
* automatically when the first packet is written. This allows encoders
|
|
318
|
+
* to be initialized from frame properties.
|
|
319
|
+
*
|
|
320
|
+
* Direct mapping to avformat_new_stream().
|
|
204
321
|
*
|
|
205
322
|
* @param source - Encoder or stream to add
|
|
323
|
+
*
|
|
206
324
|
* @param options - Stream configuration options
|
|
325
|
+
*
|
|
207
326
|
* @param options.timeBase - Optional custom timebase for the stream
|
|
327
|
+
*
|
|
208
328
|
* @returns Stream index for packet writing
|
|
209
329
|
*
|
|
210
|
-
* @throws {Error} If called after
|
|
330
|
+
* @throws {Error} If called after packets have been written or output closed
|
|
211
331
|
*
|
|
212
332
|
* @example
|
|
213
333
|
* ```typescript
|
|
214
|
-
* // Add stream from encoder
|
|
334
|
+
* // Add stream from encoder (lazy initialization)
|
|
215
335
|
* const videoIdx = output.addStream(videoEncoder);
|
|
216
336
|
* const audioIdx = output.addStream(audioEncoder);
|
|
217
337
|
* ```
|
|
@@ -228,68 +348,74 @@ export class MediaOutput {
|
|
|
228
348
|
* @see {@link Encoder} For transcoding source
|
|
229
349
|
*/
|
|
230
350
|
addStream(source, options) {
|
|
231
|
-
if (this.
|
|
351
|
+
if (this.isClosed) {
|
|
232
352
|
throw new Error('MediaOutput is closed');
|
|
233
353
|
}
|
|
234
354
|
if (this.headerWritten) {
|
|
235
|
-
throw new Error('Cannot add streams after
|
|
355
|
+
throw new Error('Cannot add streams after packets have been written');
|
|
236
356
|
}
|
|
237
357
|
const stream = this.formatContext.newStream(null);
|
|
238
358
|
if (!stream) {
|
|
239
359
|
throw new Error('Failed to create new stream');
|
|
240
360
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
361
|
+
const isStreamCopy = !(source instanceof Encoder);
|
|
362
|
+
// For stream copy, initialize immediately since we have all the info
|
|
363
|
+
if (isStreamCopy) {
|
|
364
|
+
const inputStream = source;
|
|
365
|
+
const ret = inputStream.codecpar.copy(stream.codecpar);
|
|
366
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
|
|
367
|
+
// Set the timebases
|
|
368
|
+
const sourceTimeBase = inputStream.timeBase;
|
|
369
|
+
stream.timeBase = options?.timeBase ? new Rational(options.timeBase.num, options.timeBase.den) : inputStream.timeBase;
|
|
370
|
+
this.streams.set(stream.index, {
|
|
371
|
+
initialized: true,
|
|
372
|
+
stream,
|
|
373
|
+
source,
|
|
374
|
+
timeBase: options?.timeBase,
|
|
375
|
+
sourceTimeBase,
|
|
376
|
+
isStreamCopy: true,
|
|
377
|
+
bufferedPackets: [],
|
|
378
|
+
});
|
|
255
379
|
}
|
|
256
380
|
else {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
this.streams.set(stream.index, {
|
|
268
|
-
stream,
|
|
269
|
-
timeBase: stream.timeBase,
|
|
270
|
-
isStreamCopy,
|
|
271
|
-
sourceTimeBase,
|
|
272
|
-
});
|
|
381
|
+
this.streams.set(stream.index, {
|
|
382
|
+
initialized: false,
|
|
383
|
+
stream,
|
|
384
|
+
source,
|
|
385
|
+
timeBase: options?.timeBase,
|
|
386
|
+
sourceTimeBase: undefined, // Will be set on initialization
|
|
387
|
+
isStreamCopy: false,
|
|
388
|
+
bufferedPackets: [],
|
|
389
|
+
});
|
|
390
|
+
}
|
|
273
391
|
return stream.index;
|
|
274
392
|
}
|
|
275
393
|
/**
|
|
276
394
|
* Write a packet to the output.
|
|
277
395
|
*
|
|
278
396
|
* Writes muxed packet to the specified stream.
|
|
279
|
-
* Automatically handles
|
|
280
|
-
*
|
|
397
|
+
* Automatically handles:
|
|
398
|
+
* - Stream initialization on first packet (lazy initialization)
|
|
399
|
+
* - Codec parameter configuration from encoder or input stream
|
|
400
|
+
* - Header writing on first packet
|
|
401
|
+
* - Timestamp rescaling between source and output timebases
|
|
402
|
+
*
|
|
403
|
+
* For encoder sources, the encoder must have processed at least one frame
|
|
404
|
+
* before packets can be written (encoder must be initialized).
|
|
281
405
|
*
|
|
282
|
-
* Direct mapping to av_interleaved_write_frame().
|
|
406
|
+
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
283
407
|
*
|
|
284
408
|
* @param packet - Packet to write
|
|
409
|
+
*
|
|
285
410
|
* @param streamIndex - Target stream index
|
|
286
|
-
*
|
|
411
|
+
*
|
|
412
|
+
* @throws {Error} If stream invalid or encoder not initialized
|
|
287
413
|
*
|
|
288
414
|
* @throws {FFmpegError} If write fails
|
|
289
415
|
*
|
|
290
416
|
* @example
|
|
291
417
|
* ```typescript
|
|
292
|
-
* // Write encoded packet
|
|
418
|
+
* // Write encoded packet - header written automatically on first packet
|
|
293
419
|
* const packet = await encoder.encode(frame);
|
|
294
420
|
* if (packet) {
|
|
295
421
|
* await output.writePacket(packet, videoIdx);
|
|
@@ -309,119 +435,216 @@ export class MediaOutput {
|
|
|
309
435
|
* ```
|
|
310
436
|
*
|
|
311
437
|
* @see {@link addStream} For adding streams
|
|
312
|
-
* @see {@link writeHeader} Must be called first
|
|
313
438
|
*/
|
|
314
439
|
async writePacket(packet, streamIndex) {
|
|
315
|
-
if (this.
|
|
440
|
+
if (this.isClosed) {
|
|
316
441
|
throw new Error('MediaOutput is closed');
|
|
317
442
|
}
|
|
318
|
-
if (!this.headerWritten) {
|
|
319
|
-
throw new Error('Header must be written before packets');
|
|
320
|
-
}
|
|
321
443
|
if (this.trailerWritten) {
|
|
322
|
-
throw new Error('Cannot write packets after
|
|
444
|
+
throw new Error('Cannot write packets after output is finalized');
|
|
323
445
|
}
|
|
324
|
-
|
|
325
|
-
if (!streamInfo) {
|
|
446
|
+
if (!this.streams.get(streamIndex)) {
|
|
326
447
|
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
327
448
|
}
|
|
449
|
+
// Initialize any encoder streams that are ready
|
|
450
|
+
for (const streamInfo of this.streams.values()) {
|
|
451
|
+
if (!streamInfo.initialized && streamInfo.source instanceof Encoder) {
|
|
452
|
+
const encoder = streamInfo.source;
|
|
453
|
+
const codecContext = encoder.getCodecContext();
|
|
454
|
+
// Skip if encoder not ready yet
|
|
455
|
+
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// This encoder is ready, initialize it now
|
|
459
|
+
const ret = streamInfo.stream.codecpar.fromContext(codecContext);
|
|
460
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
461
|
+
// Update the timebase from the encoder
|
|
462
|
+
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
463
|
+
// Output stream uses encoder's timebase (or custom if specified)
|
|
464
|
+
streamInfo.stream.timeBase = streamInfo.timeBase ? new Rational(streamInfo.timeBase.num, streamInfo.timeBase.den) : codecContext.timeBase;
|
|
465
|
+
// Mark as initialized
|
|
466
|
+
streamInfo.initialized = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const streamInfo = this.streams.get(streamIndex);
|
|
470
|
+
// Check if any streams are still uninitialized
|
|
471
|
+
const uninitialized = Array.from(this.streams.values()).some((s) => !s.initialized);
|
|
472
|
+
if (uninitialized) {
|
|
473
|
+
const clonedPacket = packet.clone();
|
|
474
|
+
packet.free();
|
|
475
|
+
if (clonedPacket) {
|
|
476
|
+
streamInfo.bufferedPackets.push(clonedPacket);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Automatically write header if not written yet
|
|
481
|
+
// Use a promise to ensure only one thread writes the header
|
|
482
|
+
if (!this.headerWritten) {
|
|
483
|
+
this.headerWritePromise ??= (async () => {
|
|
484
|
+
const ret = await this.formatContext.writeHeader();
|
|
485
|
+
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
486
|
+
this.headerWritten = true;
|
|
487
|
+
})();
|
|
488
|
+
// All threads wait for the header to be written
|
|
489
|
+
await this.headerWritePromise;
|
|
490
|
+
}
|
|
328
491
|
// Set stream index
|
|
329
492
|
packet.streamIndex = streamIndex;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
493
|
+
const write = async (pkt) => {
|
|
494
|
+
// Rescale packet timestamps if source and output timebases differ
|
|
495
|
+
// Note: The stream's timebase may have been changed by writeHeader (e.g., MP4 uses 1/time_scale)
|
|
496
|
+
if (streamInfo.sourceTimeBase) {
|
|
497
|
+
const outputStream = this.formatContext.streams?.[streamIndex];
|
|
498
|
+
if (outputStream) {
|
|
499
|
+
// Only rescale if timebases actually differ
|
|
500
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
501
|
+
const dstTb = outputStream.timeBase;
|
|
502
|
+
if (srcTb.num !== dstTb.num || srcTb.den !== dstTb.den) {
|
|
503
|
+
pkt.rescaleTs(streamInfo.sourceTimeBase, outputStream.timeBase);
|
|
504
|
+
}
|
|
340
505
|
}
|
|
341
506
|
}
|
|
507
|
+
// Write the packet
|
|
508
|
+
const ret = await this.formatContext.interleavedWriteFrame(pkt);
|
|
509
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
510
|
+
};
|
|
511
|
+
// Write any buffered packets first
|
|
512
|
+
for (const bufferedPacket of streamInfo.bufferedPackets) {
|
|
513
|
+
await write(bufferedPacket);
|
|
514
|
+
bufferedPacket.free();
|
|
342
515
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
516
|
+
streamInfo.bufferedPackets = [];
|
|
517
|
+
// Write the current packet
|
|
518
|
+
await write(packet);
|
|
346
519
|
}
|
|
347
520
|
/**
|
|
348
|
-
* Write
|
|
521
|
+
* Write a packet to the output synchronously.
|
|
522
|
+
* Synchronous version of writePacket.
|
|
523
|
+
*
|
|
524
|
+
* Writes muxed packet to the specified stream.
|
|
525
|
+
* Automatically handles:
|
|
526
|
+
* - Stream initialization on first packet (lazy initialization)
|
|
527
|
+
* - Codec parameter configuration from encoder or input stream
|
|
528
|
+
* - Header writing on first packet
|
|
529
|
+
* - Timestamp rescaling between source and output timebases
|
|
349
530
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* Finalizes stream parameters and initializes muxer.
|
|
531
|
+
* For encoder sources, the encoder must have processed at least one frame
|
|
532
|
+
* before packets can be written (encoder must be initialized).
|
|
353
533
|
*
|
|
354
|
-
* Direct mapping to avformat_write_header().
|
|
534
|
+
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
355
535
|
*
|
|
356
|
-
* @
|
|
536
|
+
* @param packet - Packet to write
|
|
537
|
+
*
|
|
538
|
+
* @param streamIndex - Target stream index
|
|
539
|
+
*
|
|
540
|
+
* @throws {Error} If stream invalid or encoder not initialized
|
|
357
541
|
*
|
|
358
542
|
* @throws {FFmpegError} If write fails
|
|
359
543
|
*
|
|
360
544
|
* @example
|
|
361
545
|
* ```typescript
|
|
362
|
-
* //
|
|
363
|
-
* const
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
546
|
+
* // Write encoded packet - header written automatically on first packet
|
|
547
|
+
* const packet = encoder.encodeSync(frame);
|
|
548
|
+
* if (packet) {
|
|
549
|
+
* output.writePacketSync(packet, videoIdx);
|
|
550
|
+
* packet.free();
|
|
551
|
+
* }
|
|
367
552
|
* ```
|
|
368
553
|
*
|
|
369
|
-
* @see {@link addStream} Must add streams first
|
|
370
|
-
* @see {@link writePacket} Can write packets after
|
|
371
|
-
* @see {@link writeTrailer} Must call at end
|
|
372
|
-
*/
|
|
373
|
-
async writeHeader() {
|
|
374
|
-
if (this.closed) {
|
|
375
|
-
throw new Error('MediaOutput is closed');
|
|
376
|
-
}
|
|
377
|
-
if (this.headerWritten) {
|
|
378
|
-
throw new Error('Header already written');
|
|
379
|
-
}
|
|
380
|
-
const ret = await this.formatContext.writeHeader();
|
|
381
|
-
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
382
|
-
this.headerWritten = true;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Write file trailer.
|
|
386
|
-
*
|
|
387
|
-
* Writes format trailer and finalizes the file.
|
|
388
|
-
* Must be called after all packets are written.
|
|
389
|
-
* Flushes any buffered data and updates file headers.
|
|
390
|
-
*
|
|
391
|
-
* Direct mapping to av_write_trailer().
|
|
392
|
-
*
|
|
393
|
-
* @throws {Error} If header not written or already written
|
|
394
|
-
*
|
|
395
|
-
* @throws {FFmpegError} If write fails
|
|
396
|
-
*
|
|
397
554
|
* @example
|
|
398
555
|
* ```typescript
|
|
399
|
-
* //
|
|
400
|
-
*
|
|
401
|
-
*
|
|
556
|
+
* // Stream copy with packet processing
|
|
557
|
+
* for (const packet of input.packetsSync()) {
|
|
558
|
+
* if (packet.streamIndex === inputVideoIdx) {
|
|
559
|
+
* output.writePacketSync(packet, outputVideoIdx);
|
|
560
|
+
* }
|
|
561
|
+
* packet.free();
|
|
562
|
+
* }
|
|
402
563
|
* ```
|
|
403
564
|
*
|
|
404
|
-
* @see {@link
|
|
405
|
-
* @see {@link close} For cleanup after trailer
|
|
565
|
+
* @see {@link writePacket} For async version
|
|
406
566
|
*/
|
|
407
|
-
|
|
408
|
-
if (this.
|
|
567
|
+
writePacketSync(packet, streamIndex) {
|
|
568
|
+
if (this.isClosed) {
|
|
409
569
|
throw new Error('MediaOutput is closed');
|
|
410
570
|
}
|
|
571
|
+
if (this.trailerWritten) {
|
|
572
|
+
throw new Error('Cannot write packets after output is finalized');
|
|
573
|
+
}
|
|
574
|
+
if (!this.streams.get(streamIndex)) {
|
|
575
|
+
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
576
|
+
}
|
|
577
|
+
// Initialize any encoder streams that are ready
|
|
578
|
+
for (const streamInfo of this.streams.values()) {
|
|
579
|
+
if (!streamInfo.initialized && streamInfo.source instanceof Encoder) {
|
|
580
|
+
const encoder = streamInfo.source;
|
|
581
|
+
const codecContext = encoder.getCodecContext();
|
|
582
|
+
// Skip if encoder not ready yet
|
|
583
|
+
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
// This encoder is ready, initialize it now
|
|
587
|
+
const ret = streamInfo.stream.codecpar.fromContext(codecContext);
|
|
588
|
+
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
589
|
+
// Update the timebase from the encoder
|
|
590
|
+
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
591
|
+
// Output stream uses encoder's timebase (or custom if specified)
|
|
592
|
+
streamInfo.stream.timeBase = streamInfo.timeBase ? new Rational(streamInfo.timeBase.num, streamInfo.timeBase.den) : codecContext.timeBase;
|
|
593
|
+
// Mark as initialized
|
|
594
|
+
streamInfo.initialized = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const streamInfo = this.streams.get(streamIndex);
|
|
598
|
+
// Check if any streams are still uninitialized
|
|
599
|
+
const uninitialized = Array.from(this.streams.values()).some((s) => !s.initialized);
|
|
600
|
+
if (uninitialized) {
|
|
601
|
+
const clonedPacket = packet.clone();
|
|
602
|
+
packet.free();
|
|
603
|
+
if (clonedPacket) {
|
|
604
|
+
streamInfo.bufferedPackets.push(clonedPacket);
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Automatically write header if not written yet
|
|
411
609
|
if (!this.headerWritten) {
|
|
412
|
-
|
|
610
|
+
const ret = this.formatContext.writeHeaderSync();
|
|
611
|
+
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
612
|
+
this.headerWritten = true;
|
|
413
613
|
}
|
|
414
|
-
|
|
415
|
-
|
|
614
|
+
// Set stream index
|
|
615
|
+
packet.streamIndex = streamIndex;
|
|
616
|
+
const write = (pkt) => {
|
|
617
|
+
// Rescale packet timestamps if source and output timebases differ
|
|
618
|
+
// Note: The stream's timebase may have been changed by writeHeader (e.g., MP4 uses 1/time_scale)
|
|
619
|
+
if (streamInfo.sourceTimeBase) {
|
|
620
|
+
const outputStream = this.formatContext.streams?.[streamIndex];
|
|
621
|
+
if (outputStream) {
|
|
622
|
+
// Only rescale if timebases actually differ
|
|
623
|
+
const srcTb = streamInfo.sourceTimeBase;
|
|
624
|
+
const dstTb = outputStream.timeBase;
|
|
625
|
+
if (srcTb.num !== dstTb.num || srcTb.den !== dstTb.den) {
|
|
626
|
+
pkt.rescaleTs(streamInfo.sourceTimeBase, outputStream.timeBase);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Write the packet
|
|
631
|
+
const ret = this.formatContext.interleavedWriteFrameSync(pkt);
|
|
632
|
+
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
633
|
+
};
|
|
634
|
+
// Write any buffered packets first
|
|
635
|
+
for (const bufferedPacket of streamInfo.bufferedPackets) {
|
|
636
|
+
write(bufferedPacket);
|
|
637
|
+
bufferedPacket.free();
|
|
416
638
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
639
|
+
streamInfo.bufferedPackets = [];
|
|
640
|
+
// Write the current packet
|
|
641
|
+
write(packet);
|
|
420
642
|
}
|
|
421
643
|
/**
|
|
422
644
|
* Close media output and free resources.
|
|
423
645
|
*
|
|
424
|
-
*
|
|
646
|
+
* Automatically writes trailer if header was written.
|
|
647
|
+
* Closes the output file and releases all resources.
|
|
425
648
|
* Safe to call multiple times.
|
|
426
649
|
* Automatically called by Symbol.asyncDispose.
|
|
427
650
|
*
|
|
@@ -429,7 +652,7 @@ export class MediaOutput {
|
|
|
429
652
|
* ```typescript
|
|
430
653
|
* const output = await MediaOutput.open('output.mp4');
|
|
431
654
|
* try {
|
|
432
|
-
* // Use output
|
|
655
|
+
* // Use output - trailer written automatically on close
|
|
433
656
|
* } finally {
|
|
434
657
|
* await output.close();
|
|
435
658
|
* }
|
|
@@ -438,14 +661,23 @@ export class MediaOutput {
|
|
|
438
661
|
* @see {@link Symbol.asyncDispose} For automatic cleanup
|
|
439
662
|
*/
|
|
440
663
|
async close() {
|
|
441
|
-
if (this.
|
|
664
|
+
if (this.isClosed) {
|
|
442
665
|
return;
|
|
443
666
|
}
|
|
444
|
-
this.
|
|
667
|
+
this.isClosed = true;
|
|
668
|
+
// Free any buffered packets
|
|
669
|
+
for (const streamInfo of this.streams.values()) {
|
|
670
|
+
// Free any buffered packets
|
|
671
|
+
for (const pkt of streamInfo.bufferedPackets) {
|
|
672
|
+
pkt.free();
|
|
673
|
+
}
|
|
674
|
+
streamInfo.bufferedPackets = [];
|
|
675
|
+
}
|
|
445
676
|
// Try to write trailer if header was written but trailer wasn't
|
|
446
677
|
try {
|
|
447
678
|
if (this.headerWritten && !this.trailerWritten) {
|
|
448
679
|
await this.formatContext.writeTrailer();
|
|
680
|
+
this.trailerWritten = true;
|
|
449
681
|
}
|
|
450
682
|
}
|
|
451
683
|
catch {
|
|
@@ -487,67 +719,83 @@ export class MediaOutput {
|
|
|
487
719
|
}
|
|
488
720
|
}
|
|
489
721
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* Returns internal stream info for the specified index.
|
|
493
|
-
*
|
|
494
|
-
* @param streamIndex - Stream index
|
|
495
|
-
* @returns Stream info or undefined
|
|
496
|
-
*
|
|
497
|
-
* @example
|
|
498
|
-
* ```typescript
|
|
499
|
-
* const info = output.getStreamInfo(0);
|
|
500
|
-
* console.log(`Stream 0 timebase: ${info?.timeBase.num}/${info?.timeBase.den}`);
|
|
501
|
-
* ```
|
|
502
|
-
*/
|
|
503
|
-
getStreamInfo(streamIndex) {
|
|
504
|
-
return this.streams.get(streamIndex);
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Get all stream indices.
|
|
508
|
-
*
|
|
509
|
-
* Returns array of all added stream indices.
|
|
510
|
-
*
|
|
511
|
-
* @returns Array of stream indices
|
|
512
|
-
*
|
|
513
|
-
* @example
|
|
514
|
-
* ```typescript
|
|
515
|
-
* const indices = output.getStreamIndices();
|
|
516
|
-
* console.log(`Output has ${indices.length} streams`);
|
|
517
|
-
* ```
|
|
518
|
-
*/
|
|
519
|
-
getStreamIndices() {
|
|
520
|
-
return Array.from(this.streams.keys());
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Check if header has been written.
|
|
722
|
+
* Close media output and free resources synchronously.
|
|
723
|
+
* Synchronous version of close.
|
|
524
724
|
*
|
|
525
|
-
*
|
|
725
|
+
* Automatically writes trailer if header was written.
|
|
726
|
+
* Closes the output file and releases all resources.
|
|
727
|
+
* Safe to call multiple times.
|
|
728
|
+
* Automatically called by Symbol.dispose.
|
|
526
729
|
*
|
|
527
730
|
* @example
|
|
528
731
|
* ```typescript
|
|
529
|
-
*
|
|
530
|
-
*
|
|
732
|
+
* const output = MediaOutput.openSync('output.mp4');
|
|
733
|
+
* try {
|
|
734
|
+
* // Use output - trailer written automatically on close
|
|
735
|
+
* } finally {
|
|
736
|
+
* output.closeSync();
|
|
531
737
|
* }
|
|
532
738
|
* ```
|
|
533
|
-
*/
|
|
534
|
-
isHeaderWritten() {
|
|
535
|
-
return this.headerWritten;
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Check if trailer has been written.
|
|
539
739
|
*
|
|
540
|
-
* @
|
|
541
|
-
*
|
|
542
|
-
* @example
|
|
543
|
-
* ```typescript
|
|
544
|
-
* if (!output.isTrailerWritten()) {
|
|
545
|
-
* await output.writeTrailer();
|
|
546
|
-
* }
|
|
547
|
-
* ```
|
|
740
|
+
* @see {@link close} For async version
|
|
548
741
|
*/
|
|
549
|
-
|
|
550
|
-
|
|
742
|
+
closeSync() {
|
|
743
|
+
if (this.isClosed) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
this.isClosed = true;
|
|
747
|
+
// Free any buffered packets
|
|
748
|
+
for (const streamInfo of this.streams.values()) {
|
|
749
|
+
// Free any buffered packets
|
|
750
|
+
for (const pkt of streamInfo.bufferedPackets) {
|
|
751
|
+
pkt.free();
|
|
752
|
+
}
|
|
753
|
+
streamInfo.bufferedPackets = [];
|
|
754
|
+
}
|
|
755
|
+
// Try to write trailer if header was written but trailer wasn't
|
|
756
|
+
try {
|
|
757
|
+
if (this.headerWritten && !this.trailerWritten) {
|
|
758
|
+
this.formatContext.writeTrailerSync();
|
|
759
|
+
this.trailerWritten = true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// Ignore errors
|
|
764
|
+
}
|
|
765
|
+
// Clear pb reference first to prevent use-after-free
|
|
766
|
+
if (this.ioContext) {
|
|
767
|
+
this.formatContext.pb = null;
|
|
768
|
+
}
|
|
769
|
+
// Determine if this is custom IO before freeing format context
|
|
770
|
+
const isCustomIO = (this.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
771
|
+
// For file-based IO, close the file handle via closep
|
|
772
|
+
// For custom IO, the context will be freed below
|
|
773
|
+
if (this.ioContext && !isCustomIO) {
|
|
774
|
+
try {
|
|
775
|
+
this.ioContext.closepSync();
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Ignore errors
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Free format context
|
|
782
|
+
if (this.formatContext) {
|
|
783
|
+
try {
|
|
784
|
+
this.formatContext.freeContext();
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// Ignore errors
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Now free custom IO context if present
|
|
791
|
+
if (this.ioContext && isCustomIO) {
|
|
792
|
+
try {
|
|
793
|
+
this.ioContext.freeContext();
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
// Ignore errors
|
|
797
|
+
}
|
|
798
|
+
}
|
|
551
799
|
}
|
|
552
800
|
/**
|
|
553
801
|
* Get underlying format context.
|
|
@@ -580,5 +828,24 @@ export class MediaOutput {
|
|
|
580
828
|
async [Symbol.asyncDispose]() {
|
|
581
829
|
await this.close();
|
|
582
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Dispose of media output synchronously.
|
|
833
|
+
*
|
|
834
|
+
* Implements Disposable interface for automatic cleanup.
|
|
835
|
+
* Equivalent to calling closeSync().
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```typescript
|
|
839
|
+
* {
|
|
840
|
+
* using output = MediaOutput.openSync('output.mp4');
|
|
841
|
+
* // Use output...
|
|
842
|
+
* } // Automatically closed
|
|
843
|
+
* ```
|
|
844
|
+
*
|
|
845
|
+
* @see {@link closeSync} For manual cleanup
|
|
846
|
+
*/
|
|
847
|
+
[Symbol.dispose]() {
|
|
848
|
+
this.closeSync();
|
|
849
|
+
}
|
|
583
850
|
}
|
|
584
851
|
//# sourceMappingURL=media-output.js.map
|