node-av 3.1.3 → 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.
Files changed (156) hide show
  1. package/README.md +65 -52
  2. package/binding.gyp +4 -0
  3. package/dist/api/audio-frame-buffer.d.ts +201 -0
  4. package/dist/api/audio-frame-buffer.js +275 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +319 -78
  7. package/dist/api/bitstream-filter.js +680 -151
  8. package/dist/api/bitstream-filter.js.map +1 -1
  9. package/dist/api/constants.d.ts +44 -0
  10. package/dist/api/constants.js +45 -0
  11. package/dist/api/constants.js.map +1 -0
  12. package/dist/api/data/test_av1.ivf +0 -0
  13. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  14. package/dist/api/data/test_vp8.ivf +0 -0
  15. package/dist/api/data/test_vp9.ivf +0 -0
  16. package/dist/api/decoder.d.ts +279 -17
  17. package/dist/api/decoder.js +998 -209
  18. package/dist/api/decoder.js.map +1 -1
  19. package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
  20. package/dist/api/demuxer.js +1968 -0
  21. package/dist/api/demuxer.js.map +1 -0
  22. package/dist/api/encoder.d.ts +308 -50
  23. package/dist/api/encoder.js +1133 -111
  24. package/dist/api/encoder.js.map +1 -1
  25. package/dist/api/filter-presets.d.ts +12 -5
  26. package/dist/api/filter-presets.js +21 -7
  27. package/dist/api/filter-presets.js.map +1 -1
  28. package/dist/api/filter.d.ts +406 -40
  29. package/dist/api/filter.js +966 -139
  30. package/dist/api/filter.js.map +1 -1
  31. package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
  32. package/dist/api/fmp4-stream.js +539 -0
  33. package/dist/api/fmp4-stream.js.map +1 -0
  34. package/dist/api/hardware.d.ts +58 -6
  35. package/dist/api/hardware.js +127 -11
  36. package/dist/api/hardware.js.map +1 -1
  37. package/dist/api/index.d.ts +6 -4
  38. package/dist/api/index.js +14 -8
  39. package/dist/api/index.js.map +1 -1
  40. package/dist/api/io-stream.d.ts +3 -3
  41. package/dist/api/io-stream.js +5 -4
  42. package/dist/api/io-stream.js.map +1 -1
  43. package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
  44. package/dist/api/muxer.js +1934 -0
  45. package/dist/api/muxer.js.map +1 -0
  46. package/dist/api/pipeline.d.ts +77 -29
  47. package/dist/api/pipeline.js +435 -425
  48. package/dist/api/pipeline.js.map +1 -1
  49. package/dist/api/rtp-stream.d.ts +312 -0
  50. package/dist/api/rtp-stream.js +630 -0
  51. package/dist/api/rtp-stream.js.map +1 -0
  52. package/dist/api/types.d.ts +476 -55
  53. package/dist/api/utilities/async-queue.d.ts +91 -0
  54. package/dist/api/utilities/async-queue.js +162 -0
  55. package/dist/api/utilities/async-queue.js.map +1 -0
  56. package/dist/api/utilities/audio-sample.d.ts +1 -1
  57. package/dist/api/utilities/image.d.ts +1 -1
  58. package/dist/api/utilities/index.d.ts +2 -0
  59. package/dist/api/utilities/index.js +4 -0
  60. package/dist/api/utilities/index.js.map +1 -1
  61. package/dist/api/utilities/media-type.d.ts +1 -1
  62. package/dist/api/utilities/pixel-format.d.ts +1 -1
  63. package/dist/api/utilities/sample-format.d.ts +1 -1
  64. package/dist/api/utilities/scheduler.d.ts +169 -0
  65. package/dist/api/utilities/scheduler.js +136 -0
  66. package/dist/api/utilities/scheduler.js.map +1 -0
  67. package/dist/api/utilities/streaming.d.ts +74 -15
  68. package/dist/api/utilities/streaming.js +170 -12
  69. package/dist/api/utilities/streaming.js.map +1 -1
  70. package/dist/api/utilities/timestamp.d.ts +1 -1
  71. package/dist/api/webrtc-stream.d.ts +288 -0
  72. package/dist/api/webrtc-stream.js +440 -0
  73. package/dist/api/webrtc-stream.js.map +1 -0
  74. package/dist/constants/constants.d.ts +51 -1
  75. package/dist/constants/constants.js +47 -1
  76. package/dist/constants/constants.js.map +1 -1
  77. package/dist/constants/encoders.d.ts +2 -1
  78. package/dist/constants/encoders.js +4 -3
  79. package/dist/constants/encoders.js.map +1 -1
  80. package/dist/constants/hardware.d.ts +26 -0
  81. package/dist/constants/hardware.js +27 -0
  82. package/dist/constants/hardware.js.map +1 -0
  83. package/dist/constants/index.d.ts +1 -0
  84. package/dist/constants/index.js +1 -0
  85. package/dist/constants/index.js.map +1 -1
  86. package/dist/lib/binding.d.ts +19 -8
  87. package/dist/lib/binding.js.map +1 -1
  88. package/dist/lib/codec-context.d.ts +87 -0
  89. package/dist/lib/codec-context.js +125 -4
  90. package/dist/lib/codec-context.js.map +1 -1
  91. package/dist/lib/codec-parameters.d.ts +183 -1
  92. package/dist/lib/codec-parameters.js +209 -0
  93. package/dist/lib/codec-parameters.js.map +1 -1
  94. package/dist/lib/codec-parser.d.ts +23 -0
  95. package/dist/lib/codec-parser.js +25 -0
  96. package/dist/lib/codec-parser.js.map +1 -1
  97. package/dist/lib/codec.d.ts +26 -4
  98. package/dist/lib/codec.js +35 -0
  99. package/dist/lib/codec.js.map +1 -1
  100. package/dist/lib/dictionary.js +1 -0
  101. package/dist/lib/dictionary.js.map +1 -1
  102. package/dist/lib/error.js +1 -1
  103. package/dist/lib/error.js.map +1 -1
  104. package/dist/lib/filter-context.d.ts +52 -11
  105. package/dist/lib/filter-context.js +56 -12
  106. package/dist/lib/filter-context.js.map +1 -1
  107. package/dist/lib/filter-graph.d.ts +9 -0
  108. package/dist/lib/filter-graph.js +13 -0
  109. package/dist/lib/filter-graph.js.map +1 -1
  110. package/dist/lib/filter.d.ts +21 -0
  111. package/dist/lib/filter.js +28 -0
  112. package/dist/lib/filter.js.map +1 -1
  113. package/dist/lib/format-context.d.ts +48 -14
  114. package/dist/lib/format-context.js +76 -7
  115. package/dist/lib/format-context.js.map +1 -1
  116. package/dist/lib/frame.d.ts +168 -0
  117. package/dist/lib/frame.js +212 -0
  118. package/dist/lib/frame.js.map +1 -1
  119. package/dist/lib/hardware-device-context.d.ts +3 -2
  120. package/dist/lib/hardware-device-context.js.map +1 -1
  121. package/dist/lib/index.d.ts +1 -0
  122. package/dist/lib/index.js +2 -0
  123. package/dist/lib/index.js.map +1 -1
  124. package/dist/lib/input-format.d.ts +21 -0
  125. package/dist/lib/input-format.js +42 -2
  126. package/dist/lib/input-format.js.map +1 -1
  127. package/dist/lib/native-types.d.ts +48 -26
  128. package/dist/lib/option.d.ts +25 -13
  129. package/dist/lib/option.js +28 -0
  130. package/dist/lib/option.js.map +1 -1
  131. package/dist/lib/output-format.d.ts +22 -1
  132. package/dist/lib/output-format.js +28 -0
  133. package/dist/lib/output-format.js.map +1 -1
  134. package/dist/lib/packet.d.ts +35 -0
  135. package/dist/lib/packet.js +52 -2
  136. package/dist/lib/packet.js.map +1 -1
  137. package/dist/lib/stream.d.ts +126 -0
  138. package/dist/lib/stream.js +188 -5
  139. package/dist/lib/stream.js.map +1 -1
  140. package/dist/lib/sync-queue.d.ts +179 -0
  141. package/dist/lib/sync-queue.js +197 -0
  142. package/dist/lib/sync-queue.js.map +1 -0
  143. package/dist/lib/types.d.ts +27 -1
  144. package/dist/lib/utilities.d.ts +281 -53
  145. package/dist/lib/utilities.js +298 -55
  146. package/dist/lib/utilities.js.map +1 -1
  147. package/package.json +20 -19
  148. package/dist/api/fmp4.js +0 -710
  149. package/dist/api/fmp4.js.map +0 -1
  150. package/dist/api/media-input.js +0 -1075
  151. package/dist/api/media-input.js.map +0 -1
  152. package/dist/api/media-output.js +0 -1040
  153. package/dist/api/media-output.js.map +0 -1
  154. package/dist/api/webrtc.d.ts +0 -664
  155. package/dist/api/webrtc.js +0 -1132
  156. package/dist/api/webrtc.js.map +0 -1
@@ -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