node-av 3.1.3 → 5.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 +88 -52
- package/binding.gyp +23 -11
- 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 +320 -78
- package/dist/api/bitstream-filter.js +684 -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 +454 -77
- package/dist/api/decoder.js +1081 -271
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/{media-input.d.ts → demuxer.d.ts} +295 -45
- package/dist/api/demuxer.js +1965 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/encoder.d.ts +423 -132
- package/dist/api/encoder.js +1089 -240
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-complex.d.ts +769 -0
- package/dist/api/filter-complex.js +1596 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +80 -5
- package/dist/api/filter-presets.js +117 -7
- package/dist/api/filter-presets.js.map +1 -1
- package/dist/api/filter.d.ts +561 -125
- package/dist/api/filter.js +1083 -274
- 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 +8 -4
- package/dist/api/index.js +17 -8
- package/dist/api/index.js.map +1 -1
- package/dist/api/io-stream.d.ts +6 -6
- 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} +280 -66
- 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 +449 -439
- 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 +533 -56
- 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 +11 -1
- package/dist/api/utilities/audio-sample.js +10 -0
- package/dist/api/utilities/audio-sample.js.map +1 -1
- package/dist/api/utilities/channel-layout.d.ts +1 -0
- package/dist/api/utilities/channel-layout.js +1 -0
- package/dist/api/utilities/channel-layout.js.map +1 -1
- package/dist/api/utilities/image.d.ts +39 -1
- package/dist/api/utilities/image.js +38 -0
- package/dist/api/utilities/image.js.map +1 -1
- package/dist/api/utilities/index.d.ts +3 -0
- package/dist/api/utilities/index.js +6 -0
- package/dist/api/utilities/index.js.map +1 -1
- package/dist/api/utilities/media-type.d.ts +2 -1
- package/dist/api/utilities/media-type.js +1 -0
- package/dist/api/utilities/media-type.js.map +1 -1
- package/dist/api/utilities/pixel-format.d.ts +4 -1
- package/dist/api/utilities/pixel-format.js +3 -0
- package/dist/api/utilities/pixel-format.js.map +1 -1
- package/dist/api/utilities/sample-format.d.ts +6 -1
- package/dist/api/utilities/sample-format.js +5 -0
- package/dist/api/utilities/sample-format.js.map +1 -1
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +105 -15
- package/dist/api/utilities/streaming.js +201 -12
- package/dist/api/utilities/streaming.js.map +1 -1
- package/dist/api/utilities/timestamp.d.ts +15 -1
- package/dist/api/utilities/timestamp.js +14 -0
- package/dist/api/utilities/timestamp.js.map +1 -1
- package/dist/api/utilities/whisper-model.d.ts +310 -0
- package/dist/api/utilities/whisper-model.js +528 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- 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/api/whisper.d.ts +324 -0
- package/dist/api/whisper.js +362 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/constants.d.ts +54 -2
- package/dist/constants/constants.js +48 -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/ffmpeg/index.d.ts +3 -3
- package/dist/ffmpeg/index.js +3 -3
- package/dist/ffmpeg/utils.d.ts +27 -0
- package/dist/ffmpeg/utils.js +28 -16
- package/dist/ffmpeg/utils.js.map +1 -1
- package/dist/lib/binding.d.ts +22 -11
- 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 +229 -1
- package/dist/lib/codec-parameters.js +264 -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/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- 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 +264 -1
- package/dist/lib/frame.js +351 -1
- 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 +2 -0
- package/dist/lib/index.js +4 -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 +76 -27
- 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/rational.d.ts +18 -0
- package/dist/lib/rational.js +19 -0
- package/dist/lib/rational.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 +49 -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 +2 -2
- package/package.json +37 -26
- 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
package/dist/api/media-output.js
DELETED
|
@@ -1,1040 +0,0 @@
|
|
|
1
|
-
import { mkdirSync } from 'fs';
|
|
2
|
-
import { mkdir } from 'fs/promises';
|
|
3
|
-
import { dirname, resolve } from 'path';
|
|
4
|
-
import { AV_CODEC_FLAG_GLOBAL_HEADER, AVFMT_FLAG_CUSTOM_IO, AVFMT_GLOBALHEADER, AVFMT_NOFILE, AVIO_FLAG_WRITE, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, } from '../constants/constants.js';
|
|
5
|
-
import { FFmpegError, FormatContext, IOContext, Rational } from '../lib/index.js';
|
|
6
|
-
import { Encoder } from './encoder.js';
|
|
7
|
-
/**
|
|
8
|
-
* High-level media output for writing and muxing media files.
|
|
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.
|
|
13
|
-
* Handles stream configuration, packet writing, and format management.
|
|
14
|
-
* Supports files, URLs, and custom I/O with automatic cleanup.
|
|
15
|
-
* Essential component for media encoding pipelines and transcoding.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* import { MediaOutput } from 'node-av/api';
|
|
20
|
-
*
|
|
21
|
-
* // Create output file
|
|
22
|
-
* await using output = await MediaOutput.open('output.mp4');
|
|
23
|
-
*
|
|
24
|
-
* // Add streams from encoders
|
|
25
|
-
* const videoIdx = output.addStream(videoEncoder);
|
|
26
|
-
* const audioIdx = output.addStream(audioEncoder);
|
|
27
|
-
*
|
|
28
|
-
* // Write packets - header written automatically on first packet
|
|
29
|
-
* await output.writePacket(packet, videoIdx);
|
|
30
|
-
*
|
|
31
|
-
* // Close - trailer written automatically
|
|
32
|
-
* // (automatic with await using)
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```typescript
|
|
37
|
-
* // Stream copy
|
|
38
|
-
* await using input = await MediaInput.open('input.mp4');
|
|
39
|
-
* await using output = await MediaOutput.open('output.mp4');
|
|
40
|
-
*
|
|
41
|
-
* // Copy stream configuration
|
|
42
|
-
* const videoIdx = output.addStream(input.video());
|
|
43
|
-
*
|
|
44
|
-
* // Process packets - header/trailer handled automatically
|
|
45
|
-
* for await (const packet of input.packets()) {
|
|
46
|
-
* await output.writePacket(packet, videoIdx);
|
|
47
|
-
* packet.free();
|
|
48
|
-
* }
|
|
49
|
-
* ```
|
|
50
|
-
*
|
|
51
|
-
* @see {@link MediaInput} For reading media files
|
|
52
|
-
* @see {@link Encoder} For encoding frames to packets
|
|
53
|
-
* @see {@link FormatContext} For low-level API
|
|
54
|
-
*/
|
|
55
|
-
export class MediaOutput {
|
|
56
|
-
formatContext;
|
|
57
|
-
_streams = new Map();
|
|
58
|
-
ioContext;
|
|
59
|
-
headerWritten = false;
|
|
60
|
-
trailerWritten = false;
|
|
61
|
-
isClosed = false;
|
|
62
|
-
headerWritePromise;
|
|
63
|
-
/**
|
|
64
|
-
* @internal
|
|
65
|
-
*/
|
|
66
|
-
constructor() {
|
|
67
|
-
this.formatContext = new FormatContext();
|
|
68
|
-
}
|
|
69
|
-
static async open(target, options) {
|
|
70
|
-
const output = new MediaOutput();
|
|
71
|
-
try {
|
|
72
|
-
if (typeof target === 'string') {
|
|
73
|
-
// File or stream URL - resolve relative paths and create directories
|
|
74
|
-
// Check if it's a URL (starts with protocol://) or a file path
|
|
75
|
-
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target);
|
|
76
|
-
const resolvedTarget = isUrl ? target : resolve(target);
|
|
77
|
-
// Create directory structure for local files (not URLs)
|
|
78
|
-
if (!isUrl && target !== '') {
|
|
79
|
-
const dir = dirname(resolvedTarget);
|
|
80
|
-
await mkdir(dir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
// Allocate output context
|
|
83
|
-
const ret = output.formatContext.allocOutputContext2(null, options?.format ?? null, resolvedTarget === '' ? null : resolvedTarget);
|
|
84
|
-
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
85
|
-
// Set format options if provided
|
|
86
|
-
if (options?.options) {
|
|
87
|
-
for (const [key, value] of Object.entries(options.options)) {
|
|
88
|
-
output.formatContext.setOption(key, value);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Check if we need to open IO
|
|
92
|
-
const oformat = output.formatContext.oformat;
|
|
93
|
-
if (resolvedTarget && oformat && !(oformat.flags & AVFMT_NOFILE)) {
|
|
94
|
-
// For file-based formats, we need to open the file using avio_open2
|
|
95
|
-
// FFmpeg will manage the AVIOContext internally
|
|
96
|
-
output.ioContext = new IOContext();
|
|
97
|
-
const openRet = await output.ioContext.open2(resolvedTarget, AVIO_FLAG_WRITE);
|
|
98
|
-
FFmpegError.throwIfError(openRet, `Failed to open output file: ${resolvedTarget}`);
|
|
99
|
-
output.formatContext.pb = output.ioContext;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
// Custom IO with callbacks - format is required
|
|
104
|
-
if (!options?.format) {
|
|
105
|
-
throw new Error('Format must be specified for custom IO');
|
|
106
|
-
}
|
|
107
|
-
const ret = output.formatContext.allocOutputContext2(null, options.format, null);
|
|
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
|
-
// Setup custom IO with callbacks
|
|
116
|
-
output.ioContext = new IOContext();
|
|
117
|
-
output.ioContext.allocContextWithCallbacks(options.bufferSize ?? 4096, 1, target.read, target.write, target.seek);
|
|
118
|
-
output.ioContext.maxPacketSize = options.bufferSize ?? 4096;
|
|
119
|
-
output.formatContext.pb = output.ioContext;
|
|
120
|
-
output.formatContext.flags = AVFMT_FLAG_CUSTOM_IO;
|
|
121
|
-
}
|
|
122
|
-
return output;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
// Cleanup on error
|
|
126
|
-
if (output.ioContext) {
|
|
127
|
-
try {
|
|
128
|
-
const isCustomIO = (output.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
129
|
-
if (isCustomIO) {
|
|
130
|
-
// Clear the pb reference first
|
|
131
|
-
output.formatContext.pb = null;
|
|
132
|
-
// For custom IO with callbacks, free the context
|
|
133
|
-
output.ioContext.freeContext();
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// For file-based IO, close the file handle
|
|
137
|
-
await output.ioContext.closep();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
// Ignore errors
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (output.formatContext) {
|
|
145
|
-
try {
|
|
146
|
-
output.formatContext.freeContext();
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// Ignore errors
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
static openSync(target, options) {
|
|
156
|
-
const output = new MediaOutput();
|
|
157
|
-
try {
|
|
158
|
-
if (typeof target === 'string') {
|
|
159
|
-
// File or stream URL - resolve relative paths and create directories
|
|
160
|
-
// Check if it's a URL (starts with protocol://) or a file path
|
|
161
|
-
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(target);
|
|
162
|
-
const resolvedTarget = isUrl ? target : resolve(target);
|
|
163
|
-
// Create directory structure for local files (not URLs)
|
|
164
|
-
if (!isUrl && target !== '') {
|
|
165
|
-
const dir = dirname(resolvedTarget);
|
|
166
|
-
mkdirSync(dir, { recursive: true });
|
|
167
|
-
}
|
|
168
|
-
// Allocate output context
|
|
169
|
-
const ret = output.formatContext.allocOutputContext2(null, options?.format ?? null, resolvedTarget === '' ? null : resolvedTarget);
|
|
170
|
-
FFmpegError.throwIfError(ret, 'Failed to allocate output context');
|
|
171
|
-
// Set format options if provided
|
|
172
|
-
if (options?.options) {
|
|
173
|
-
for (const [key, value] of Object.entries(options.options)) {
|
|
174
|
-
output.formatContext.setOption(key, value);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Check if we need to open IO
|
|
178
|
-
const oformat = output.formatContext.oformat;
|
|
179
|
-
if (resolvedTarget && oformat && !(oformat.flags & AVFMT_NOFILE)) {
|
|
180
|
-
// For file-based formats, we need to open the file using avio_open2
|
|
181
|
-
// FFmpeg will manage the AVIOContext internally
|
|
182
|
-
output.ioContext = new IOContext();
|
|
183
|
-
const openRet = output.ioContext.open2Sync(resolvedTarget, AVIO_FLAG_WRITE);
|
|
184
|
-
FFmpegError.throwIfError(openRet, `Failed to open output file: ${resolvedTarget}`);
|
|
185
|
-
output.formatContext.pb = output.ioContext;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
// Custom IO with callbacks - format is required
|
|
190
|
-
if (!options?.format) {
|
|
191
|
-
throw new Error('Format must be specified for custom IO');
|
|
192
|
-
}
|
|
193
|
-
const ret = output.formatContext.allocOutputContext2(null, options.format, null);
|
|
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
|
-
// Setup custom IO with callbacks
|
|
202
|
-
output.ioContext = new IOContext();
|
|
203
|
-
output.ioContext.allocContextWithCallbacks(options.bufferSize ?? 4096, 1, target.read, target.write, target.seek);
|
|
204
|
-
output.ioContext.maxPacketSize = options.bufferSize ?? 4096;
|
|
205
|
-
output.formatContext.pb = output.ioContext;
|
|
206
|
-
output.formatContext.flags = AVFMT_FLAG_CUSTOM_IO;
|
|
207
|
-
}
|
|
208
|
-
return output;
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
// Cleanup on error
|
|
212
|
-
if (output.ioContext) {
|
|
213
|
-
try {
|
|
214
|
-
const isCustomIO = (output.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
215
|
-
if (isCustomIO) {
|
|
216
|
-
// Clear the pb reference first
|
|
217
|
-
output.formatContext.pb = null;
|
|
218
|
-
// For custom IO with callbacks, free the context
|
|
219
|
-
output.ioContext.freeContext();
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// For file-based IO, close the file handle
|
|
223
|
-
output.ioContext.closepSync();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Ignore errors
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (output.formatContext) {
|
|
231
|
-
try {
|
|
232
|
-
output.formatContext.freeContext();
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Ignore errors
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Check if output is open.
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```typescript
|
|
246
|
-
* if (!output.isOutputOpen) {
|
|
247
|
-
* console.log('Output is not open');
|
|
248
|
-
* }
|
|
249
|
-
* ```
|
|
250
|
-
*/
|
|
251
|
-
get isOutputOpen() {
|
|
252
|
-
return !this.isClosed;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Check if output is initialized.
|
|
256
|
-
*
|
|
257
|
-
* All streams have been initialized.
|
|
258
|
-
* This occurs after the first packet has been written to each stream.
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```typescript
|
|
262
|
-
* if (!output.isOutputInitialized) {
|
|
263
|
-
* console.log('Output is not initialized');
|
|
264
|
-
* }
|
|
265
|
-
* ```
|
|
266
|
-
*/
|
|
267
|
-
get isOutputInitialized() {
|
|
268
|
-
if (this._streams.size === 0) {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
if (this.isClosed) {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
return Array.from(this._streams).every(([_, stream]) => stream.initialized);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Get all streams in the media.
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* ```typescript
|
|
281
|
-
* for (const stream of output.streams) {
|
|
282
|
-
* console.log(`Stream ${stream.index}: ${stream.codecpar.codecType}`);
|
|
283
|
-
* }
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
get streams() {
|
|
287
|
-
return this.formatContext.streams;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Get format name.
|
|
291
|
-
*
|
|
292
|
-
* Returns 'unknown' if output is closed or format is not available.
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* ```typescript
|
|
296
|
-
* console.log(`Format: ${output.formatName}`); // "mov,mp4,m4a,3gp,3g2,mj2"
|
|
297
|
-
* ```
|
|
298
|
-
*/
|
|
299
|
-
get formatName() {
|
|
300
|
-
if (this.isClosed) {
|
|
301
|
-
return 'unknown';
|
|
302
|
-
}
|
|
303
|
-
return this.formatContext.oformat?.name ?? 'unknown';
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Get format long name.
|
|
307
|
-
*
|
|
308
|
-
* Returns 'Unknown Format' if output is closed or format is not available.
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* ```typescript
|
|
312
|
-
* console.log(`Format: ${output.formatLongName}`); // "QuickTime / MOV"
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
get formatLongName() {
|
|
316
|
-
if (this.isClosed) {
|
|
317
|
-
return 'Unknown Format';
|
|
318
|
-
}
|
|
319
|
-
return this.formatContext.oformat?.longName ?? 'Unknown Format';
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Get MIME type of the output format.
|
|
323
|
-
*
|
|
324
|
-
* Returns format's native MIME type.
|
|
325
|
-
* For DASH/HLS formats, use {@link getStreamMimeType} for stream-specific MIME types.
|
|
326
|
-
*
|
|
327
|
-
* Returns null if output is closed or format is not available.
|
|
328
|
-
*
|
|
329
|
-
* @example
|
|
330
|
-
* ```typescript
|
|
331
|
-
* console.log(mp4Output.mimeType); // "video/mp4"
|
|
332
|
-
* console.log(dashOutput.mimeType); // null (DASH has no global MIME type)
|
|
333
|
-
*
|
|
334
|
-
* // For DASH/HLS, get MIME type per stream:
|
|
335
|
-
* console.log(dashOutput.getStreamMimeType(0)); // "video/mp4"
|
|
336
|
-
* ```
|
|
337
|
-
*/
|
|
338
|
-
get mimeType() {
|
|
339
|
-
if (this.isClosed) {
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
return this.formatContext.oformat?.mimeType ?? null;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Add a stream to the output.
|
|
346
|
-
*
|
|
347
|
-
* Configures output stream from encoder or input stream.
|
|
348
|
-
* Must be called before writing any packets.
|
|
349
|
-
* Returns stream index for packet writing.
|
|
350
|
-
*
|
|
351
|
-
* Streams are initialized lazily - codec parameters are configured
|
|
352
|
-
* automatically when the first packet is written. This allows encoders
|
|
353
|
-
* to be initialized from frame properties.
|
|
354
|
-
*
|
|
355
|
-
* Direct mapping to avformat_new_stream().
|
|
356
|
-
*
|
|
357
|
-
* @param source - Encoder or stream to add
|
|
358
|
-
*
|
|
359
|
-
* @param options - Stream configuration options
|
|
360
|
-
*
|
|
361
|
-
* @param options.timeBase - Optional custom timebase for the stream
|
|
362
|
-
*
|
|
363
|
-
* @returns Stream index for packet writing
|
|
364
|
-
*
|
|
365
|
-
* @throws {Error} If called after packets have been written or output closed
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* ```typescript
|
|
369
|
-
* // Add stream from encoder (lazy initialization)
|
|
370
|
-
* const videoIdx = output.addStream(videoEncoder);
|
|
371
|
-
* const audioIdx = output.addStream(audioEncoder);
|
|
372
|
-
* ```
|
|
373
|
-
*
|
|
374
|
-
* @example
|
|
375
|
-
* ```typescript
|
|
376
|
-
* // Stream copy with custom timebase
|
|
377
|
-
* const streamIdx = output.addStream(input.video(), {
|
|
378
|
-
* timeBase: { num: 1, den: 90000 }
|
|
379
|
-
* });
|
|
380
|
-
* ```
|
|
381
|
-
*
|
|
382
|
-
* @see {@link writePacket} For writing packets to streams
|
|
383
|
-
* @see {@link Encoder} For transcoding source
|
|
384
|
-
*/
|
|
385
|
-
addStream(source, options) {
|
|
386
|
-
if (this.isClosed) {
|
|
387
|
-
throw new Error('MediaOutput is closed');
|
|
388
|
-
}
|
|
389
|
-
if (this.headerWritten) {
|
|
390
|
-
throw new Error('Cannot add streams after packets have been written');
|
|
391
|
-
}
|
|
392
|
-
const stream = this.formatContext.newStream(null);
|
|
393
|
-
if (!stream) {
|
|
394
|
-
throw new Error('Failed to create new stream');
|
|
395
|
-
}
|
|
396
|
-
const isStreamCopy = !(source instanceof Encoder);
|
|
397
|
-
// Auto-set GLOBAL_HEADER flag if format requires it
|
|
398
|
-
if (!isStreamCopy) {
|
|
399
|
-
const encoder = source;
|
|
400
|
-
const oformat = this.formatContext.oformat;
|
|
401
|
-
if (oformat && oformat.flags & AVFMT_GLOBALHEADER) {
|
|
402
|
-
// Get current flags and add GLOBAL_HEADER flag
|
|
403
|
-
const currentFlags = encoder.getCodecFlags();
|
|
404
|
-
const newFlags = (currentFlags | AV_CODEC_FLAG_GLOBAL_HEADER);
|
|
405
|
-
encoder.setCodecFlags(newFlags);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// For stream copy, initialize immediately since we have all the info
|
|
409
|
-
if (isStreamCopy) {
|
|
410
|
-
const inputStream = source;
|
|
411
|
-
const ret = inputStream.codecpar.copy(stream.codecpar);
|
|
412
|
-
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
|
|
413
|
-
// Set the timebases
|
|
414
|
-
const sourceTimeBase = inputStream.timeBase;
|
|
415
|
-
if (options?.timeBase) {
|
|
416
|
-
stream.timeBase = new Rational(options.timeBase.num, options.timeBase.den);
|
|
417
|
-
}
|
|
418
|
-
this._streams.set(stream.index, {
|
|
419
|
-
initialized: true,
|
|
420
|
-
stream,
|
|
421
|
-
source,
|
|
422
|
-
timeBase: options?.timeBase,
|
|
423
|
-
sourceTimeBase,
|
|
424
|
-
isStreamCopy: true,
|
|
425
|
-
bufferedPackets: [],
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
this._streams.set(stream.index, {
|
|
430
|
-
initialized: false,
|
|
431
|
-
stream,
|
|
432
|
-
source,
|
|
433
|
-
timeBase: options?.timeBase,
|
|
434
|
-
sourceTimeBase: undefined, // Will be set on initialization
|
|
435
|
-
isStreamCopy: false,
|
|
436
|
-
bufferedPackets: [],
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
return stream.index;
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Get output stream by index.
|
|
443
|
-
*
|
|
444
|
-
* Returns the stream at the specified index.
|
|
445
|
-
* Use the stream index returned by addStream().
|
|
446
|
-
*
|
|
447
|
-
* @param index - Stream index (returned by addStream)
|
|
448
|
-
*
|
|
449
|
-
* @returns Stream or undefined if index is invalid
|
|
450
|
-
*
|
|
451
|
-
* @example
|
|
452
|
-
* ```typescript
|
|
453
|
-
* const output = await MediaOutput.open('output.mp4');
|
|
454
|
-
* const videoIdx = output.addStream(encoder);
|
|
455
|
-
*
|
|
456
|
-
* // Get the output stream to inspect codec parameters
|
|
457
|
-
* const stream = output.getStream(videoIdx);
|
|
458
|
-
* if (stream) {
|
|
459
|
-
* console.log(`Output codec: ${stream.codecpar.codecId}`);
|
|
460
|
-
* }
|
|
461
|
-
* ```
|
|
462
|
-
*
|
|
463
|
-
* @see {@link addStream} For adding streams
|
|
464
|
-
* @see {@link video} For getting video streams
|
|
465
|
-
* @see {@link audio} For getting audio streams
|
|
466
|
-
*/
|
|
467
|
-
getStream(index) {
|
|
468
|
-
const streams = this.formatContext.streams;
|
|
469
|
-
if (!streams || index < 0 || index >= streams.length) {
|
|
470
|
-
return undefined;
|
|
471
|
-
}
|
|
472
|
-
return streams[index];
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Get video stream by index.
|
|
476
|
-
*
|
|
477
|
-
* Returns the nth video stream (0-based index).
|
|
478
|
-
* Returns undefined if stream doesn't exist.
|
|
479
|
-
*
|
|
480
|
-
* @param index - Video stream index (default: 0)
|
|
481
|
-
*
|
|
482
|
-
* @returns Video stream or undefined
|
|
483
|
-
*
|
|
484
|
-
* @example
|
|
485
|
-
* ```typescript
|
|
486
|
-
* const output = await MediaOutput.open('output.mp4');
|
|
487
|
-
* output.addStream(videoEncoder);
|
|
488
|
-
*
|
|
489
|
-
* // Get first video stream
|
|
490
|
-
* const videoStream = output.video();
|
|
491
|
-
* if (videoStream) {
|
|
492
|
-
* console.log(`Video output: ${videoStream.codecpar.width}x${videoStream.codecpar.height}`);
|
|
493
|
-
* }
|
|
494
|
-
* ```
|
|
495
|
-
*
|
|
496
|
-
* @see {@link audio} For audio streams
|
|
497
|
-
* @see {@link getStream} For direct stream access
|
|
498
|
-
*/
|
|
499
|
-
video(index = 0) {
|
|
500
|
-
const streams = this.formatContext.streams;
|
|
501
|
-
if (!streams)
|
|
502
|
-
return undefined;
|
|
503
|
-
const videoStreams = streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_VIDEO);
|
|
504
|
-
return videoStreams[index];
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Get audio stream by index.
|
|
508
|
-
*
|
|
509
|
-
* Returns the nth audio stream (0-based index).
|
|
510
|
-
* Returns undefined if stream doesn't exist.
|
|
511
|
-
*
|
|
512
|
-
* @param index - Audio stream index (default: 0)
|
|
513
|
-
*
|
|
514
|
-
* @returns Audio stream or undefined
|
|
515
|
-
*
|
|
516
|
-
* @example
|
|
517
|
-
* ```typescript
|
|
518
|
-
* const output = await MediaOutput.open('output.mp4');
|
|
519
|
-
* output.addStream(audioEncoder);
|
|
520
|
-
*
|
|
521
|
-
* // Get first audio stream
|
|
522
|
-
* const audioStream = output.audio();
|
|
523
|
-
* if (audioStream) {
|
|
524
|
-
* console.log(`Audio output: ${audioStream.codecpar.sampleRate}Hz`);
|
|
525
|
-
* }
|
|
526
|
-
* ```
|
|
527
|
-
*
|
|
528
|
-
* @see {@link video} For video streams
|
|
529
|
-
* @see {@link getStream} For direct stream access
|
|
530
|
-
*/
|
|
531
|
-
audio(index = 0) {
|
|
532
|
-
const streams = this.formatContext.streams;
|
|
533
|
-
if (!streams)
|
|
534
|
-
return undefined;
|
|
535
|
-
const audioStreams = streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_AUDIO);
|
|
536
|
-
return audioStreams[index];
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Get output format.
|
|
540
|
-
*
|
|
541
|
-
* Returns the output format used for muxing.
|
|
542
|
-
* May be null if format context not initialized.
|
|
543
|
-
*
|
|
544
|
-
* @returns Output format or null
|
|
545
|
-
*
|
|
546
|
-
* @example
|
|
547
|
-
* ```typescript
|
|
548
|
-
* const output = await MediaOutput.open('output.mp4');
|
|
549
|
-
* const format = output.outputFormat();
|
|
550
|
-
* if (format) {
|
|
551
|
-
* console.log(`Output format: ${format.name}`);
|
|
552
|
-
* }
|
|
553
|
-
* ```
|
|
554
|
-
*
|
|
555
|
-
* @see {@link OutputFormat} For format details
|
|
556
|
-
*/
|
|
557
|
-
outputFormat() {
|
|
558
|
-
return this.formatContext.oformat;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Write a packet to the output.
|
|
562
|
-
*
|
|
563
|
-
* Writes muxed packet to the specified stream.
|
|
564
|
-
* Automatically handles:
|
|
565
|
-
* - Stream initialization on first packet (lazy initialization)
|
|
566
|
-
* - Codec parameter configuration from encoder or input stream
|
|
567
|
-
* - Header writing on first packet
|
|
568
|
-
* - Timestamp rescaling between source and output timebases
|
|
569
|
-
*
|
|
570
|
-
* For encoder sources, the encoder must have processed at least one frame
|
|
571
|
-
* before packets can be written (encoder must be initialized).
|
|
572
|
-
*
|
|
573
|
-
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
574
|
-
*
|
|
575
|
-
* @param packet - Packet to write
|
|
576
|
-
*
|
|
577
|
-
* @param streamIndex - Target stream index
|
|
578
|
-
*
|
|
579
|
-
* @throws {Error} If stream invalid or encoder not initialized
|
|
580
|
-
*
|
|
581
|
-
* @throws {FFmpegError} If write fails
|
|
582
|
-
*
|
|
583
|
-
* @example
|
|
584
|
-
* ```typescript
|
|
585
|
-
* // Write encoded packet - header written automatically on first packet
|
|
586
|
-
* const packet = await encoder.encode(frame);
|
|
587
|
-
* if (packet) {
|
|
588
|
-
* await output.writePacket(packet, videoIdx);
|
|
589
|
-
* packet.free();
|
|
590
|
-
* }
|
|
591
|
-
* ```
|
|
592
|
-
*
|
|
593
|
-
* @example
|
|
594
|
-
* ```typescript
|
|
595
|
-
* // Stream copy with packet processing
|
|
596
|
-
* for await (const packet of input.packets()) {
|
|
597
|
-
* if (packet.streamIndex === inputVideoIdx) {
|
|
598
|
-
* await output.writePacket(packet, outputVideoIdx);
|
|
599
|
-
* }
|
|
600
|
-
* packet.free();
|
|
601
|
-
* }
|
|
602
|
-
* ```
|
|
603
|
-
*
|
|
604
|
-
* @see {@link addStream} For adding streams
|
|
605
|
-
*/
|
|
606
|
-
async writePacket(packet, streamIndex) {
|
|
607
|
-
if (this.isClosed) {
|
|
608
|
-
throw new Error('MediaOutput is closed');
|
|
609
|
-
}
|
|
610
|
-
if (this.trailerWritten) {
|
|
611
|
-
throw new Error('Cannot write packets after output is finalized');
|
|
612
|
-
}
|
|
613
|
-
if (!this._streams.get(streamIndex)) {
|
|
614
|
-
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
615
|
-
}
|
|
616
|
-
// Initialize any encoder streams that are ready
|
|
617
|
-
for (const streamInfo of this._streams.values()) {
|
|
618
|
-
if (!streamInfo.initialized && streamInfo.source instanceof Encoder) {
|
|
619
|
-
const encoder = streamInfo.source;
|
|
620
|
-
const codecContext = encoder.getCodecContext();
|
|
621
|
-
// Skip if encoder not ready yet
|
|
622
|
-
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
// This encoder is ready, initialize it now
|
|
626
|
-
const ret = streamInfo.stream.codecpar.fromContext(codecContext);
|
|
627
|
-
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
628
|
-
// Update the timebase from the encoder
|
|
629
|
-
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
630
|
-
// Set frame rate from encoder
|
|
631
|
-
const fr = codecContext.framerate;
|
|
632
|
-
streamInfo.stream.avgFrameRate = new Rational(fr.num, fr.den);
|
|
633
|
-
// Set output stream timebase
|
|
634
|
-
if (streamInfo.timeBase) {
|
|
635
|
-
// User specified custom timebase
|
|
636
|
-
streamInfo.stream.timeBase = new Rational(streamInfo.timeBase.num, streamInfo.timeBase.den);
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
// Default: 1/framerate
|
|
640
|
-
const fps = codecContext.framerate.num / codecContext.framerate.den;
|
|
641
|
-
streamInfo.stream.timeBase = new Rational(1, Math.round(fps));
|
|
642
|
-
}
|
|
643
|
-
// Mark as initialized
|
|
644
|
-
streamInfo.initialized = true;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
const streamInfo = this._streams.get(streamIndex);
|
|
648
|
-
// Check if any streams are still uninitialized
|
|
649
|
-
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
650
|
-
if (uninitialized) {
|
|
651
|
-
const clonedPacket = packet.clone();
|
|
652
|
-
packet.free();
|
|
653
|
-
if (clonedPacket) {
|
|
654
|
-
streamInfo.bufferedPackets.push(clonedPacket);
|
|
655
|
-
}
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
// Automatically write header if not written yet
|
|
659
|
-
// Use a promise to ensure only one thread writes the header
|
|
660
|
-
if (!this.headerWritten) {
|
|
661
|
-
this.headerWritePromise ??= (async () => {
|
|
662
|
-
const ret = await this.formatContext.writeHeader();
|
|
663
|
-
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
664
|
-
this.headerWritten = true;
|
|
665
|
-
})();
|
|
666
|
-
// All threads wait for the header to be written
|
|
667
|
-
await this.headerWritePromise;
|
|
668
|
-
}
|
|
669
|
-
// Set stream index
|
|
670
|
-
packet.streamIndex = streamIndex;
|
|
671
|
-
const write = async (pkt) => {
|
|
672
|
-
// Rescale packet timestamps if source and output timebases differ
|
|
673
|
-
// Note: The stream's timebase may have been changed by writeHeader (e.g., MP4 uses 1/time_scale)
|
|
674
|
-
if (streamInfo.sourceTimeBase) {
|
|
675
|
-
const outputStream = this.formatContext.streams?.[streamIndex];
|
|
676
|
-
if (outputStream) {
|
|
677
|
-
// Only rescale if timebases actually differ
|
|
678
|
-
const srcTb = streamInfo.sourceTimeBase;
|
|
679
|
-
const dstTb = outputStream.timeBase;
|
|
680
|
-
if (srcTb.num !== dstTb.num || srcTb.den !== dstTb.den) {
|
|
681
|
-
pkt.rescaleTs(streamInfo.sourceTimeBase, outputStream.timeBase);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
// Write the packet
|
|
686
|
-
const ret = await this.formatContext.interleavedWriteFrame(pkt);
|
|
687
|
-
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
688
|
-
};
|
|
689
|
-
// Write any buffered packets first
|
|
690
|
-
for (const bufferedPacket of streamInfo.bufferedPackets) {
|
|
691
|
-
await write(bufferedPacket);
|
|
692
|
-
bufferedPacket.free();
|
|
693
|
-
}
|
|
694
|
-
streamInfo.bufferedPackets = [];
|
|
695
|
-
// Write the current packet
|
|
696
|
-
await write(packet);
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Write a packet to the output synchronously.
|
|
700
|
-
* Synchronous version of writePacket.
|
|
701
|
-
*
|
|
702
|
-
* Writes muxed packet to the specified stream.
|
|
703
|
-
* Automatically handles:
|
|
704
|
-
* - Stream initialization on first packet (lazy initialization)
|
|
705
|
-
* - Codec parameter configuration from encoder or input stream
|
|
706
|
-
* - Header writing on first packet
|
|
707
|
-
* - Timestamp rescaling between source and output timebases
|
|
708
|
-
*
|
|
709
|
-
* For encoder sources, the encoder must have processed at least one frame
|
|
710
|
-
* before packets can be written (encoder must be initialized).
|
|
711
|
-
*
|
|
712
|
-
* Direct mapping to avformat_write_header() (on first packet) and av_interleaved_write_frame().
|
|
713
|
-
*
|
|
714
|
-
* @param packet - Packet to write
|
|
715
|
-
*
|
|
716
|
-
* @param streamIndex - Target stream index
|
|
717
|
-
*
|
|
718
|
-
* @throws {Error} If stream invalid or encoder not initialized
|
|
719
|
-
*
|
|
720
|
-
* @throws {FFmpegError} If write fails
|
|
721
|
-
*
|
|
722
|
-
* @example
|
|
723
|
-
* ```typescript
|
|
724
|
-
* // Write encoded packet - header written automatically on first packet
|
|
725
|
-
* const packet = encoder.encodeSync(frame);
|
|
726
|
-
* if (packet) {
|
|
727
|
-
* output.writePacketSync(packet, videoIdx);
|
|
728
|
-
* packet.free();
|
|
729
|
-
* }
|
|
730
|
-
* ```
|
|
731
|
-
*
|
|
732
|
-
* @example
|
|
733
|
-
* ```typescript
|
|
734
|
-
* // Stream copy with packet processing
|
|
735
|
-
* for (const packet of input.packetsSync()) {
|
|
736
|
-
* if (packet.streamIndex === inputVideoIdx) {
|
|
737
|
-
* output.writePacketSync(packet, outputVideoIdx);
|
|
738
|
-
* }
|
|
739
|
-
* packet.free();
|
|
740
|
-
* }
|
|
741
|
-
* ```
|
|
742
|
-
*
|
|
743
|
-
* @see {@link writePacket} For async version
|
|
744
|
-
*/
|
|
745
|
-
writePacketSync(packet, streamIndex) {
|
|
746
|
-
if (this.isClosed) {
|
|
747
|
-
throw new Error('MediaOutput is closed');
|
|
748
|
-
}
|
|
749
|
-
if (this.trailerWritten) {
|
|
750
|
-
throw new Error('Cannot write packets after output is finalized');
|
|
751
|
-
}
|
|
752
|
-
if (!this._streams.get(streamIndex)) {
|
|
753
|
-
throw new Error(`Invalid stream index: ${streamIndex}`);
|
|
754
|
-
}
|
|
755
|
-
// Initialize any encoder streams that are ready
|
|
756
|
-
for (const streamInfo of this._streams.values()) {
|
|
757
|
-
if (!streamInfo.initialized && streamInfo.source instanceof Encoder) {
|
|
758
|
-
const encoder = streamInfo.source;
|
|
759
|
-
const codecContext = encoder.getCodecContext();
|
|
760
|
-
// Skip if encoder not ready yet
|
|
761
|
-
if (!encoder.isEncoderInitialized || !codecContext) {
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
// This encoder is ready, initialize it now
|
|
765
|
-
const ret = streamInfo.stream.codecpar.fromContext(codecContext);
|
|
766
|
-
FFmpegError.throwIfError(ret, 'Failed to copy codec parameters from encoder');
|
|
767
|
-
// Update the timebase from the encoder
|
|
768
|
-
streamInfo.sourceTimeBase = codecContext.timeBase;
|
|
769
|
-
// Set frame rate from encoder
|
|
770
|
-
const fr = codecContext.framerate;
|
|
771
|
-
streamInfo.stream.avgFrameRate = new Rational(fr.num, fr.den);
|
|
772
|
-
// Set output stream timebase
|
|
773
|
-
if (streamInfo.timeBase) {
|
|
774
|
-
// User specified custom timebase
|
|
775
|
-
streamInfo.stream.timeBase = new Rational(streamInfo.timeBase.num, streamInfo.timeBase.den);
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
// Default: 1/framerate (MP4/DASH muxer will multiply by 2 until >= 10000)
|
|
779
|
-
const fps = codecContext.framerate.num / codecContext.framerate.den;
|
|
780
|
-
streamInfo.stream.timeBase = new Rational(1, Math.round(fps));
|
|
781
|
-
}
|
|
782
|
-
// Mark as initialized
|
|
783
|
-
streamInfo.initialized = true;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const streamInfo = this._streams.get(streamIndex);
|
|
787
|
-
// Check if any streams are still uninitialized
|
|
788
|
-
const uninitialized = Array.from(this._streams.values()).some((s) => !s.initialized);
|
|
789
|
-
if (uninitialized) {
|
|
790
|
-
const clonedPacket = packet.clone();
|
|
791
|
-
packet.free();
|
|
792
|
-
if (clonedPacket) {
|
|
793
|
-
streamInfo.bufferedPackets.push(clonedPacket);
|
|
794
|
-
}
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
// Automatically write header if not written yet
|
|
798
|
-
if (!this.headerWritten) {
|
|
799
|
-
const ret = this.formatContext.writeHeaderSync();
|
|
800
|
-
FFmpegError.throwIfError(ret, 'Failed to write header');
|
|
801
|
-
this.headerWritten = true;
|
|
802
|
-
}
|
|
803
|
-
// Set stream index
|
|
804
|
-
packet.streamIndex = streamIndex;
|
|
805
|
-
const write = (pkt) => {
|
|
806
|
-
// Rescale packet timestamps if source and output timebases differ
|
|
807
|
-
// Note: The stream's timebase may have been changed by writeHeader (e.g., MP4 uses 1/time_scale)
|
|
808
|
-
if (streamInfo.sourceTimeBase) {
|
|
809
|
-
const outputStream = this.formatContext.streams?.[streamIndex];
|
|
810
|
-
if (outputStream) {
|
|
811
|
-
// Only rescale if timebases actually differ
|
|
812
|
-
const srcTb = streamInfo.sourceTimeBase;
|
|
813
|
-
const dstTb = outputStream.timeBase;
|
|
814
|
-
if (srcTb.num !== dstTb.num || srcTb.den !== dstTb.den) {
|
|
815
|
-
pkt.rescaleTs(streamInfo.sourceTimeBase, outputStream.timeBase);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
// Write the packet
|
|
820
|
-
const ret = this.formatContext.interleavedWriteFrameSync(pkt);
|
|
821
|
-
FFmpegError.throwIfError(ret, 'Failed to write packet');
|
|
822
|
-
};
|
|
823
|
-
// Write any buffered packets first
|
|
824
|
-
for (const bufferedPacket of streamInfo.bufferedPackets) {
|
|
825
|
-
write(bufferedPacket);
|
|
826
|
-
bufferedPacket.free();
|
|
827
|
-
}
|
|
828
|
-
streamInfo.bufferedPackets = [];
|
|
829
|
-
// Write the current packet
|
|
830
|
-
write(packet);
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Close media output and free resources.
|
|
834
|
-
*
|
|
835
|
-
* Automatically writes trailer if header was written.
|
|
836
|
-
* Closes the output file and releases all resources.
|
|
837
|
-
* Safe to call multiple times.
|
|
838
|
-
* Automatically called by Symbol.asyncDispose.
|
|
839
|
-
*
|
|
840
|
-
* @example
|
|
841
|
-
* ```typescript
|
|
842
|
-
* const output = await MediaOutput.open('output.mp4');
|
|
843
|
-
* try {
|
|
844
|
-
* // Use output - trailer written automatically on close
|
|
845
|
-
* } finally {
|
|
846
|
-
* await output.close();
|
|
847
|
-
* }
|
|
848
|
-
* ```
|
|
849
|
-
*
|
|
850
|
-
* @see {@link Symbol.asyncDispose} For automatic cleanup
|
|
851
|
-
*/
|
|
852
|
-
async close() {
|
|
853
|
-
if (this.isClosed) {
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
this.isClosed = true;
|
|
857
|
-
// Free any buffered packets
|
|
858
|
-
for (const streamInfo of this._streams.values()) {
|
|
859
|
-
// Free any buffered packets
|
|
860
|
-
for (const pkt of streamInfo.bufferedPackets) {
|
|
861
|
-
pkt.free();
|
|
862
|
-
}
|
|
863
|
-
streamInfo.bufferedPackets = [];
|
|
864
|
-
}
|
|
865
|
-
// Try to write trailer if header was written but trailer wasn't
|
|
866
|
-
try {
|
|
867
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
868
|
-
await this.formatContext.writeTrailer();
|
|
869
|
-
this.trailerWritten = true;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
catch {
|
|
873
|
-
// Ignore errors
|
|
874
|
-
}
|
|
875
|
-
// Clear pb reference first to prevent use-after-free
|
|
876
|
-
if (this.ioContext) {
|
|
877
|
-
this.formatContext.pb = null;
|
|
878
|
-
}
|
|
879
|
-
// Determine if this is custom IO before freeing format context
|
|
880
|
-
const isCustomIO = (this.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
881
|
-
// For file-based IO, close the file handle via closep
|
|
882
|
-
// For custom IO, the context will be freed below
|
|
883
|
-
if (this.ioContext && !isCustomIO) {
|
|
884
|
-
try {
|
|
885
|
-
await this.ioContext.closep();
|
|
886
|
-
}
|
|
887
|
-
catch {
|
|
888
|
-
// Ignore errors
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
// Free format context
|
|
892
|
-
if (this.formatContext) {
|
|
893
|
-
try {
|
|
894
|
-
this.formatContext.freeContext();
|
|
895
|
-
}
|
|
896
|
-
catch {
|
|
897
|
-
// Ignore errors
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
// Now free custom IO context if present
|
|
901
|
-
if (this.ioContext && isCustomIO) {
|
|
902
|
-
try {
|
|
903
|
-
this.ioContext.freeContext();
|
|
904
|
-
}
|
|
905
|
-
catch {
|
|
906
|
-
// Ignore errors
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Close media output and free resources synchronously.
|
|
912
|
-
* Synchronous version of close.
|
|
913
|
-
*
|
|
914
|
-
* Automatically writes trailer if header was written.
|
|
915
|
-
* Closes the output file and releases all resources.
|
|
916
|
-
* Safe to call multiple times.
|
|
917
|
-
* Automatically called by Symbol.dispose.
|
|
918
|
-
*
|
|
919
|
-
* @example
|
|
920
|
-
* ```typescript
|
|
921
|
-
* const output = MediaOutput.openSync('output.mp4');
|
|
922
|
-
* try {
|
|
923
|
-
* // Use output - trailer written automatically on close
|
|
924
|
-
* } finally {
|
|
925
|
-
* output.closeSync();
|
|
926
|
-
* }
|
|
927
|
-
* ```
|
|
928
|
-
*
|
|
929
|
-
* @see {@link close} For async version
|
|
930
|
-
*/
|
|
931
|
-
closeSync() {
|
|
932
|
-
if (this.isClosed) {
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
this.isClosed = true;
|
|
936
|
-
// Free any buffered packets
|
|
937
|
-
for (const streamInfo of this._streams.values()) {
|
|
938
|
-
// Free any buffered packets
|
|
939
|
-
for (const pkt of streamInfo.bufferedPackets) {
|
|
940
|
-
pkt.free();
|
|
941
|
-
}
|
|
942
|
-
streamInfo.bufferedPackets = [];
|
|
943
|
-
}
|
|
944
|
-
// Try to write trailer if header was written but trailer wasn't
|
|
945
|
-
try {
|
|
946
|
-
if (this.headerWritten && !this.trailerWritten) {
|
|
947
|
-
this.formatContext.writeTrailerSync();
|
|
948
|
-
this.trailerWritten = true;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
catch {
|
|
952
|
-
// Ignore errors
|
|
953
|
-
}
|
|
954
|
-
// Clear pb reference first to prevent use-after-free
|
|
955
|
-
if (this.ioContext) {
|
|
956
|
-
this.formatContext.pb = null;
|
|
957
|
-
}
|
|
958
|
-
// Determine if this is custom IO before freeing format context
|
|
959
|
-
const isCustomIO = (this.formatContext.flags & AVFMT_FLAG_CUSTOM_IO) !== 0;
|
|
960
|
-
// For file-based IO, close the file handle via closep
|
|
961
|
-
// For custom IO, the context will be freed below
|
|
962
|
-
if (this.ioContext && !isCustomIO) {
|
|
963
|
-
try {
|
|
964
|
-
this.ioContext.closepSync();
|
|
965
|
-
}
|
|
966
|
-
catch {
|
|
967
|
-
// Ignore errors
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
// Free format context
|
|
971
|
-
if (this.formatContext) {
|
|
972
|
-
try {
|
|
973
|
-
this.formatContext.freeContext();
|
|
974
|
-
}
|
|
975
|
-
catch {
|
|
976
|
-
// Ignore errors
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
// Now free custom IO context if present
|
|
980
|
-
if (this.ioContext && isCustomIO) {
|
|
981
|
-
try {
|
|
982
|
-
this.ioContext.freeContext();
|
|
983
|
-
}
|
|
984
|
-
catch {
|
|
985
|
-
// Ignore errors
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
/**
|
|
990
|
-
* Get underlying format context.
|
|
991
|
-
*
|
|
992
|
-
* Returns the internal format context for advanced operations.
|
|
993
|
-
*
|
|
994
|
-
* @returns Format context
|
|
995
|
-
*
|
|
996
|
-
* @internal
|
|
997
|
-
*/
|
|
998
|
-
getFormatContext() {
|
|
999
|
-
return this.formatContext;
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Dispose of media output.
|
|
1003
|
-
*
|
|
1004
|
-
* Implements AsyncDisposable interface for automatic cleanup.
|
|
1005
|
-
* Equivalent to calling close().
|
|
1006
|
-
*
|
|
1007
|
-
* @example
|
|
1008
|
-
* ```typescript
|
|
1009
|
-
* {
|
|
1010
|
-
* await using output = await MediaOutput.open('output.mp4');
|
|
1011
|
-
* // Use output...
|
|
1012
|
-
* } // Automatically closed
|
|
1013
|
-
* ```
|
|
1014
|
-
*
|
|
1015
|
-
* @see {@link close} For manual cleanup
|
|
1016
|
-
*/
|
|
1017
|
-
async [Symbol.asyncDispose]() {
|
|
1018
|
-
await this.close();
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Dispose of media output synchronously.
|
|
1022
|
-
*
|
|
1023
|
-
* Implements Disposable interface for automatic cleanup.
|
|
1024
|
-
* Equivalent to calling closeSync().
|
|
1025
|
-
*
|
|
1026
|
-
* @example
|
|
1027
|
-
* ```typescript
|
|
1028
|
-
* {
|
|
1029
|
-
* using output = MediaOutput.openSync('output.mp4');
|
|
1030
|
-
* // Use output...
|
|
1031
|
-
* } // Automatically closed
|
|
1032
|
-
* ```
|
|
1033
|
-
*
|
|
1034
|
-
* @see {@link closeSync} For manual cleanup
|
|
1035
|
-
*/
|
|
1036
|
-
[Symbol.dispose]() {
|
|
1037
|
-
this.closeSync();
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
//# sourceMappingURL=media-output.js.map
|