@revizly/node-av 5.2.2-beta.1

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