node-av-light 0.0.0-stage → 7.1.0-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 (311) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +710 -2
  3. package/dist/api/audio-frame-buffer.d.ts +636 -0
  4. package/dist/api/audio-frame-buffer.js +1027 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +938 -0
  7. package/dist/api/bitstream-filter.js +1427 -0
  8. package/dist/api/bitstream-filter.js.map +1 -0
  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_h264.h264 +0 -0
  14. package/dist/api/data/test_hevc.h265 +0 -0
  15. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  16. package/dist/api/data/test_vp8.ivf +0 -0
  17. package/dist/api/data/test_vp9.ivf +0 -0
  18. package/dist/api/decoder.d.ts +1413 -0
  19. package/dist/api/decoder.js +2367 -0
  20. package/dist/api/decoder.js.map +1 -0
  21. package/dist/api/demuxer.d.ts +1295 -0
  22. package/dist/api/demuxer.js +2344 -0
  23. package/dist/api/demuxer.js.map +1 -0
  24. package/dist/api/device.d.ts +589 -0
  25. package/dist/api/device.js +964 -0
  26. package/dist/api/device.js.map +1 -0
  27. package/dist/api/encoder-pool.d.ts +262 -0
  28. package/dist/api/encoder-pool.js +397 -0
  29. package/dist/api/encoder-pool.js.map +1 -0
  30. package/dist/api/encoder.d.ts +1509 -0
  31. package/dist/api/encoder.js +2619 -0
  32. package/dist/api/encoder.js.map +1 -0
  33. package/dist/api/filter-complex.d.ts +828 -0
  34. package/dist/api/filter-complex.js +1661 -0
  35. package/dist/api/filter-complex.js.map +1 -0
  36. package/dist/api/filter-presets.d.ts +800 -0
  37. package/dist/api/filter-presets.js +1554 -0
  38. package/dist/api/filter-presets.js.map +1 -0
  39. package/dist/api/filter.d.ts +1339 -0
  40. package/dist/api/filter.js +2075 -0
  41. package/dist/api/filter.js.map +1 -0
  42. package/dist/api/fmp4-stream.d.ts +797 -0
  43. package/dist/api/fmp4-stream.js +1428 -0
  44. package/dist/api/fmp4-stream.js.map +1 -0
  45. package/dist/api/hardware.d.ts +677 -0
  46. package/dist/api/hardware.js +1300 -0
  47. package/dist/api/hardware.js.map +1 -0
  48. package/dist/api/index.d.ts +21 -0
  49. package/dist/api/index.js +38 -0
  50. package/dist/api/index.js.map +1 -0
  51. package/dist/api/io-stream.d.ts +308 -0
  52. package/dist/api/io-stream.js +358 -0
  53. package/dist/api/io-stream.js.map +1 -0
  54. package/dist/api/muxer.d.ts +1311 -0
  55. package/dist/api/muxer.js +2672 -0
  56. package/dist/api/muxer.js.map +1 -0
  57. package/dist/api/pipeline.d.ts +801 -0
  58. package/dist/api/pipeline.js +1344 -0
  59. package/dist/api/pipeline.js.map +1 -0
  60. package/dist/api/probe.d.ts +128 -0
  61. package/dist/api/probe.js +227 -0
  62. package/dist/api/probe.js.map +1 -0
  63. package/dist/api/rtp-stream.d.ts +497 -0
  64. package/dist/api/rtp-stream.js +1131 -0
  65. package/dist/api/rtp-stream.js.map +1 -0
  66. package/dist/api/scaler.d.ts +561 -0
  67. package/dist/api/scaler.js +918 -0
  68. package/dist/api/scaler.js.map +1 -0
  69. package/dist/api/types.d.ts +17 -0
  70. package/dist/api/types.js +2 -0
  71. package/dist/api/types.js.map +1 -0
  72. package/dist/api/utilities/async-queue.d.ts +149 -0
  73. package/dist/api/utilities/async-queue.js +254 -0
  74. package/dist/api/utilities/async-queue.js.map +1 -0
  75. package/dist/api/utilities/audio-sample.d.ts +117 -0
  76. package/dist/api/utilities/audio-sample.js +112 -0
  77. package/dist/api/utilities/audio-sample.js.map +1 -0
  78. package/dist/api/utilities/audio-transcode.d.ts +20 -0
  79. package/dist/api/utilities/audio-transcode.js +27 -0
  80. package/dist/api/utilities/audio-transcode.js.map +1 -0
  81. package/dist/api/utilities/channel-layout.d.ts +76 -0
  82. package/dist/api/utilities/channel-layout.js +80 -0
  83. package/dist/api/utilities/channel-layout.js.map +1 -0
  84. package/dist/api/utilities/codec-format.d.ts +87 -0
  85. package/dist/api/utilities/codec-format.js +117 -0
  86. package/dist/api/utilities/codec-format.js.map +1 -0
  87. package/dist/api/utilities/context-options.d.ts +25 -0
  88. package/dist/api/utilities/context-options.js +23 -0
  89. package/dist/api/utilities/context-options.js.map +1 -0
  90. package/dist/api/utilities/electron-shared-texture.d.ts +368 -0
  91. package/dist/api/utilities/electron-shared-texture.js +540 -0
  92. package/dist/api/utilities/electron-shared-texture.js.map +1 -0
  93. package/dist/api/utilities/image.d.ts +207 -0
  94. package/dist/api/utilities/image.js +213 -0
  95. package/dist/api/utilities/image.js.map +1 -0
  96. package/dist/api/utilities/index.d.ts +14 -0
  97. package/dist/api/utilities/index.js +27 -0
  98. package/dist/api/utilities/index.js.map +1 -0
  99. package/dist/api/utilities/live-decoding.d.ts +15 -0
  100. package/dist/api/utilities/live-decoding.js +32 -0
  101. package/dist/api/utilities/live-decoding.js.map +1 -0
  102. package/dist/api/utilities/media-type.d.ts +49 -0
  103. package/dist/api/utilities/media-type.js +53 -0
  104. package/dist/api/utilities/media-type.js.map +1 -0
  105. package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
  106. package/dist/api/utilities/packet-age-watchdog.js +594 -0
  107. package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
  108. package/dist/api/utilities/pixel-format.d.ts +89 -0
  109. package/dist/api/utilities/pixel-format.js +97 -0
  110. package/dist/api/utilities/pixel-format.js.map +1 -0
  111. package/dist/api/utilities/sample-format.d.ts +129 -0
  112. package/dist/api/utilities/sample-format.js +141 -0
  113. package/dist/api/utilities/sample-format.js.map +1 -0
  114. package/dist/api/utilities/scale-size.d.ts +51 -0
  115. package/dist/api/utilities/scale-size.js +55 -0
  116. package/dist/api/utilities/scale-size.js.map +1 -0
  117. package/dist/api/utilities/scheduler.d.ts +138 -0
  118. package/dist/api/utilities/scheduler.js +98 -0
  119. package/dist/api/utilities/scheduler.js.map +1 -0
  120. package/dist/api/utilities/streaming.d.ts +186 -0
  121. package/dist/api/utilities/streaming.js +309 -0
  122. package/dist/api/utilities/streaming.js.map +1 -0
  123. package/dist/api/utilities/timestamp.d.ts +193 -0
  124. package/dist/api/utilities/timestamp.js +206 -0
  125. package/dist/api/utilities/timestamp.js.map +1 -0
  126. package/dist/api/utilities/video-transcode.d.ts +218 -0
  127. package/dist/api/utilities/video-transcode.js +624 -0
  128. package/dist/api/utilities/video-transcode.js.map +1 -0
  129. package/dist/api/utilities/whisper-model.d.ts +330 -0
  130. package/dist/api/utilities/whisper-model.js +587 -0
  131. package/dist/api/utilities/whisper-model.js.map +1 -0
  132. package/dist/api/utils.d.ts +19 -0
  133. package/dist/api/utils.js +39 -0
  134. package/dist/api/utils.js.map +1 -0
  135. package/dist/api/webrtc-stream.d.ts +294 -0
  136. package/dist/api/webrtc-stream.js +546 -0
  137. package/dist/api/webrtc-stream.js.map +1 -0
  138. package/dist/api/whisper.d.ts +326 -0
  139. package/dist/api/whisper.js +402 -0
  140. package/dist/api/whisper.js.map +1 -0
  141. package/dist/constants/bsf-options.d.ts +364 -0
  142. package/dist/constants/bsf-options.js +7 -0
  143. package/dist/constants/bsf-options.js.map +1 -0
  144. package/dist/constants/channel-layouts.d.ts +53 -0
  145. package/dist/constants/channel-layouts.js +57 -0
  146. package/dist/constants/channel-layouts.js.map +1 -0
  147. package/dist/constants/constants.d.ts +2435 -0
  148. package/dist/constants/constants.js +1989 -0
  149. package/dist/constants/constants.js.map +1 -0
  150. package/dist/constants/decoders.d.ts +651 -0
  151. package/dist/constants/decoders.js +641 -0
  152. package/dist/constants/decoders.js.map +1 -0
  153. package/dist/constants/encoders.d.ts +314 -0
  154. package/dist/constants/encoders.js +309 -0
  155. package/dist/constants/encoders.js.map +1 -0
  156. package/dist/constants/filter-options.d.ts +12099 -0
  157. package/dist/constants/filter-options.js +7 -0
  158. package/dist/constants/filter-options.js.map +1 -0
  159. package/dist/constants/format-options.d.ts +4319 -0
  160. package/dist/constants/format-options.js +7 -0
  161. package/dist/constants/format-options.js.map +1 -0
  162. package/dist/constants/formats.d.ts +18 -0
  163. package/dist/constants/formats.js +7 -0
  164. package/dist/constants/formats.js.map +1 -0
  165. package/dist/constants/hardware.d.ts +26 -0
  166. package/dist/constants/hardware.js +27 -0
  167. package/dist/constants/hardware.js.map +1 -0
  168. package/dist/constants/index.d.ts +10 -0
  169. package/dist/constants/index.js +6 -0
  170. package/dist/constants/index.js.map +1 -0
  171. package/dist/constants/options.d.ts +6217 -0
  172. package/dist/constants/options.js +7 -0
  173. package/dist/constants/options.js.map +1 -0
  174. package/dist/ffmpeg/index.d.ts +101 -0
  175. package/dist/ffmpeg/index.js +161 -0
  176. package/dist/ffmpeg/index.js.map +1 -0
  177. package/dist/ffmpeg/version.d.ts +6 -0
  178. package/dist/ffmpeg/version.js +7 -0
  179. package/dist/ffmpeg/version.js.map +1 -0
  180. package/dist/index.d.ts +4 -0
  181. package/dist/index.js +9 -0
  182. package/dist/index.js.map +1 -0
  183. package/dist/lib/audio-fifo.d.ts +399 -0
  184. package/dist/lib/audio-fifo.js +431 -0
  185. package/dist/lib/audio-fifo.js.map +1 -0
  186. package/dist/lib/binding.d.ts +242 -0
  187. package/dist/lib/binding.js +102 -0
  188. package/dist/lib/binding.js.map +1 -0
  189. package/dist/lib/bitstream-filter-context.d.ts +381 -0
  190. package/dist/lib/bitstream-filter-context.js +443 -0
  191. package/dist/lib/bitstream-filter-context.js.map +1 -0
  192. package/dist/lib/bitstream-filter.d.ts +140 -0
  193. package/dist/lib/bitstream-filter.js +154 -0
  194. package/dist/lib/bitstream-filter.js.map +1 -0
  195. package/dist/lib/codec-context.d.ts +1069 -0
  196. package/dist/lib/codec-context.js +1336 -0
  197. package/dist/lib/codec-context.js.map +1 -0
  198. package/dist/lib/codec-parameters.d.ts +664 -0
  199. package/dist/lib/codec-parameters.js +813 -0
  200. package/dist/lib/codec-parameters.js.map +1 -0
  201. package/dist/lib/codec-parser.d.ts +201 -0
  202. package/dist/lib/codec-parser.js +213 -0
  203. package/dist/lib/codec-parser.js.map +1 -0
  204. package/dist/lib/codec.d.ts +617 -0
  205. package/dist/lib/codec.js +746 -0
  206. package/dist/lib/codec.js.map +1 -0
  207. package/dist/lib/device.d.ts +294 -0
  208. package/dist/lib/device.js +326 -0
  209. package/dist/lib/device.js.map +1 -0
  210. package/dist/lib/dictionary.d.ts +333 -0
  211. package/dist/lib/dictionary.js +372 -0
  212. package/dist/lib/dictionary.js.map +1 -0
  213. package/dist/lib/error.d.ts +311 -0
  214. package/dist/lib/error.js +395 -0
  215. package/dist/lib/error.js.map +1 -0
  216. package/dist/lib/fifo.d.ts +416 -0
  217. package/dist/lib/fifo.js +453 -0
  218. package/dist/lib/fifo.js.map +1 -0
  219. package/dist/lib/filter-context.d.ts +714 -0
  220. package/dist/lib/filter-context.js +783 -0
  221. package/dist/lib/filter-context.js.map +1 -0
  222. package/dist/lib/filter-graph-segment.d.ts +161 -0
  223. package/dist/lib/filter-graph-segment.js +172 -0
  224. package/dist/lib/filter-graph-segment.js.map +1 -0
  225. package/dist/lib/filter-graph.d.ts +641 -0
  226. package/dist/lib/filter-graph.js +704 -0
  227. package/dist/lib/filter-graph.js.map +1 -0
  228. package/dist/lib/filter-inout.d.ts +202 -0
  229. package/dist/lib/filter-inout.js +261 -0
  230. package/dist/lib/filter-inout.js.map +1 -0
  231. package/dist/lib/filter.d.ts +243 -0
  232. package/dist/lib/filter.js +272 -0
  233. package/dist/lib/filter.js.map +1 -0
  234. package/dist/lib/format-context.d.ts +1335 -0
  235. package/dist/lib/format-context.js +1455 -0
  236. package/dist/lib/format-context.js.map +1 -0
  237. package/dist/lib/frame-utils.d.ts +116 -0
  238. package/dist/lib/frame-utils.js +98 -0
  239. package/dist/lib/frame-utils.js.map +1 -0
  240. package/dist/lib/frame.d.ts +1315 -0
  241. package/dist/lib/frame.js +1535 -0
  242. package/dist/lib/frame.js.map +1 -0
  243. package/dist/lib/hardware-device-context.d.ts +362 -0
  244. package/dist/lib/hardware-device-context.js +383 -0
  245. package/dist/lib/hardware-device-context.js.map +1 -0
  246. package/dist/lib/hardware-frames-context.d.ts +418 -0
  247. package/dist/lib/hardware-frames-context.js +471 -0
  248. package/dist/lib/hardware-frames-context.js.map +1 -0
  249. package/dist/lib/index.d.ts +35 -0
  250. package/dist/lib/index.js +60 -0
  251. package/dist/lib/index.js.map +1 -0
  252. package/dist/lib/input-format.d.ts +249 -0
  253. package/dist/lib/input-format.js +306 -0
  254. package/dist/lib/input-format.js.map +1 -0
  255. package/dist/lib/io-context.d.ts +698 -0
  256. package/dist/lib/io-context.js +771 -0
  257. package/dist/lib/io-context.js.map +1 -0
  258. package/dist/lib/log.d.ts +174 -0
  259. package/dist/lib/log.js +184 -0
  260. package/dist/lib/log.js.map +1 -0
  261. package/dist/lib/native-types.d.ts +1024 -0
  262. package/dist/lib/native-types.js +2 -0
  263. package/dist/lib/native-types.js.map +1 -0
  264. package/dist/lib/option.d.ts +953 -0
  265. package/dist/lib/option.js +1607 -0
  266. package/dist/lib/option.js.map +1 -0
  267. package/dist/lib/output-format.d.ts +222 -0
  268. package/dist/lib/output-format.js +259 -0
  269. package/dist/lib/output-format.js.map +1 -0
  270. package/dist/lib/packet.d.ts +521 -0
  271. package/dist/lib/packet.js +613 -0
  272. package/dist/lib/packet.js.map +1 -0
  273. package/dist/lib/rational.d.ts +251 -0
  274. package/dist/lib/rational.js +278 -0
  275. package/dist/lib/rational.js.map +1 -0
  276. package/dist/lib/software-resample-context.d.ts +552 -0
  277. package/dist/lib/software-resample-context.js +592 -0
  278. package/dist/lib/software-resample-context.js.map +1 -0
  279. package/dist/lib/software-scale-context.d.ts +344 -0
  280. package/dist/lib/software-scale-context.js +366 -0
  281. package/dist/lib/software-scale-context.js.map +1 -0
  282. package/dist/lib/stream.d.ts +383 -0
  283. package/dist/lib/stream.js +524 -0
  284. package/dist/lib/stream.js.map +1 -0
  285. package/dist/lib/sync-queue.d.ts +179 -0
  286. package/dist/lib/sync-queue.js +197 -0
  287. package/dist/lib/sync-queue.js.map +1 -0
  288. package/dist/lib/types.d.ts +34 -0
  289. package/dist/lib/types.js +2 -0
  290. package/dist/lib/types.js.map +1 -0
  291. package/dist/lib/utilities.d.ts +1198 -0
  292. package/dist/lib/utilities.js +1302 -0
  293. package/dist/lib/utilities.js.map +1 -0
  294. package/dist/utils/electron.d.ts +49 -0
  295. package/dist/utils/electron.js +63 -0
  296. package/dist/utils/electron.js.map +1 -0
  297. package/dist/utils/index.d.ts +6 -0
  298. package/dist/utils/index.js +7 -0
  299. package/dist/utils/index.js.map +1 -0
  300. package/dist/utils/libc.d.ts +11 -0
  301. package/dist/utils/libc.js +35 -0
  302. package/dist/utils/libc.js.map +1 -0
  303. package/dist/utils/variant.d.ts +26 -0
  304. package/dist/utils/variant.js +31 -0
  305. package/dist/utils/variant.js.map +1 -0
  306. package/dist/webrtc/index.d.ts +4 -0
  307. package/dist/webrtc/index.js +7 -0
  308. package/dist/webrtc/index.js.map +1 -0
  309. package/install/check.js +124 -0
  310. package/install/ffmpeg.js +66 -0
  311. package/package.json +167 -4
@@ -0,0 +1,2344 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { createSocket } from 'dgram';
54
+ import { closeSync, openSync, readSync } from 'fs';
55
+ import { open } from 'fs/promises';
56
+ import { Readable } from 'node:stream';
57
+ import { resolve } from 'path';
58
+ import { AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_NEAR_INF, AV_ROUND_PASS_MINMAX, AV_TIME_BASE, AV_TIME_BASE_Q, AVERROR_EOF, AVERROR_EXIT, AVFLAG_NONE, AVFMT_FLAG_CUSTOM_IO, AVFMT_FLAG_NONBLOCK, AVFMT_TS_DISCONT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, } from '../constants/constants.js';
59
+ import { Dictionary } from '../lib/dictionary.js';
60
+ import { AVERROR_EAGAIN, FFmpegError } from '../lib/error.js';
61
+ import { FormatContext } from '../lib/format-context.js';
62
+ import { InputFormat } from '../lib/input-format.js';
63
+ import { IOContext } from '../lib/io-context.js';
64
+ import { Packet } from '../lib/packet.js';
65
+ import { Rational } from '../lib/rational.js';
66
+ import { avGetPixFmtName, avGetSampleFmtName, avRescaleQ, avRescaleQRnd, dtsPredict as nativeDtsPredict } from '../lib/utilities.js';
67
+ import { DELTA_THRESHOLD, DTS_ERROR_THRESHOLD, IO_BUFFER_SIZE, MAX_INPUT_QUEUE_SIZE } from './constants.js';
68
+ import { IOStream } from './io-stream.js';
69
+ import { StreamingUtils } from './utilities/streaming.js';
70
+ // Scratch buffer for Atomics.wait(), created on first use: Electron renderers
71
+ // without cross-origin isolation have no SharedArrayBuffer, so creating it at
72
+ // import time would make the whole module fail to load there.
73
+ let syncSleepSignal;
74
+ /**
75
+ * Block the calling thread without spinning the CPU.
76
+ *
77
+ * Gives packetsSync a blocking sleep for EAGAIN retries (mirrors FFmpeg CLI's av_usleep()).
78
+ *
79
+ * @param ms - Sleep duration in milliseconds
80
+ *
81
+ * @internal
82
+ */
83
+ function sleepSync(ms) {
84
+ syncSleepSignal ??= new Int32Array(new SharedArrayBuffer(4));
85
+ Atomics.wait(syncSleepSignal, 0, 0, ms);
86
+ }
87
+ /**
88
+ * Keep a timer from holding the process open.
89
+ *
90
+ * Electron renderers use browser timers, where setTimeout returns a number without unref().
91
+ *
92
+ * @param timer - Timer handle returned by setTimeout
93
+ *
94
+ * @internal
95
+ */
96
+ function unrefTimer(timer) {
97
+ if (typeof timer === 'object') {
98
+ timer.unref();
99
+ }
100
+ }
101
+ /**
102
+ * High-level demuxer for reading and demuxing media files.
103
+ *
104
+ * Provides simplified access to media streams, packets, and metadata.
105
+ * Handles file opening, format detection, and stream information extraction.
106
+ * Supports files, URLs, buffers, and raw data input with automatic cleanup.
107
+ * Essential component for media processing pipelines and transcoding.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * import { Demuxer } from 'node-av/api';
112
+ *
113
+ * // Open media file
114
+ * await using input = await Demuxer.open('video.mp4');
115
+ * console.log(`Format: ${input.formatName}`);
116
+ * console.log(`Duration: ${input.duration}s`);
117
+ *
118
+ * // Process packets
119
+ * for await (const packet of input.packets()) {
120
+ * console.log(`Packet from stream ${packet.streamIndex}`);
121
+ * packet.free();
122
+ * }
123
+ * ```
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * // From buffer
128
+ * const buffer = await fs.readFile('video.mp4');
129
+ * await using input = await Demuxer.open(buffer);
130
+ *
131
+ * // Access streams
132
+ * const videoStream = input.video();
133
+ * const audioStream = input.audio();
134
+ * ```
135
+ *
136
+ * @see {@link Muxer} For writing media files
137
+ * @see {@link Decoder} For decoding packets to frames
138
+ * @see {@link FormatContext} For low-level API
139
+ */
140
+ export class Demuxer {
141
+ formatContext;
142
+ _streams = [];
143
+ ioContext;
144
+ externalIO = false; // ioContext was supplied by the caller - may own a protocol handle from open2()
145
+ isClosed = false;
146
+ options;
147
+ // Timestamp processing state (per-stream)
148
+ streamStates = new Map();
149
+ // Timestamp discontinuity tracking (global)
150
+ tsOffsetDiscont = 0n;
151
+ lastTs = AV_NOPTS_VALUE;
152
+ // Demux manager for handling multiple parallel packet generators
153
+ activeGenerators = 0;
154
+ demuxThread = null;
155
+ packetQueues = new Map(); // streamIndex or 'all' -> queue
156
+ packetQueueConsumers = new Map(); // active consumer count per queue key
157
+ queueResolvers = new Map(); // Promise resolvers for waiting consumers (multiple per key)
158
+ drainResolver = null; // Promise resolver for the demux thread parked on a full queue
159
+ demuxThreadActive = false;
160
+ demuxEof = false;
161
+ lastError = null; // read error that ended the demux loop (null = clean EOF)
162
+ signal;
163
+ signalCleanup;
164
+ /**
165
+ * @param formatContext - Opened format context
166
+ *
167
+ * @param options - Media input options
168
+ *
169
+ * @param ioContext - Optional IO context for custom I/O (e.g., from Buffer)
170
+ *
171
+ * @param externalIO - Whether ioContext was supplied by the caller instead of built here
172
+ *
173
+ * @internal
174
+ */
175
+ constructor(formatContext, options, ioContext, externalIO = false) {
176
+ this.formatContext = formatContext;
177
+ this.ioContext = ioContext;
178
+ this.externalIO = externalIO;
179
+ this._streams = formatContext.streams ?? [];
180
+ this.options = options;
181
+ }
182
+ /**
183
+ * Probe media format without fully opening the file.
184
+ *
185
+ * Detects format by analyzing file headers and content.
186
+ * Useful for format validation before processing.
187
+ *
188
+ * Direct mapping to av_probe_input_format().
189
+ *
190
+ * @param input - File path or buffer to probe
191
+ *
192
+ * @returns Format information or null if unrecognized
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const info = await Demuxer.probeFormat('video.mp4');
197
+ * if (info) {
198
+ * console.log(`Format: ${info.format}`);
199
+ * console.log(`Confidence: ${info.confidence}%`);
200
+ * }
201
+ * ```
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // Probe from buffer
206
+ * const buffer = await fs.readFile('video.webm');
207
+ * const info = await Demuxer.probeFormat(buffer);
208
+ * console.log(`MIME type: ${info?.mimeType}`);
209
+ * ```
210
+ *
211
+ * @see {@link InputFormat.probe} For low-level probing
212
+ */
213
+ static async probeFormat(input) {
214
+ try {
215
+ if (Buffer.isBuffer(input)) {
216
+ // Probe from buffer
217
+ const format = InputFormat.probe(input);
218
+ if (!format) {
219
+ return null;
220
+ }
221
+ return {
222
+ format: format.name ?? 'unknown',
223
+ longName: format.longName ?? undefined,
224
+ extensions: format.extensions ?? undefined,
225
+ mimeType: format.mimeType ?? undefined,
226
+ confidence: 100, // Direct probe always has high confidence
227
+ };
228
+ }
229
+ else {
230
+ // For files, read first part and probe
231
+ let fileHandle;
232
+ try {
233
+ fileHandle = await open(input, 'r');
234
+ // Read first 64KB for probing
235
+ const buffer = Buffer.alloc(65536);
236
+ const { bytesRead } = await fileHandle.read(buffer, 0, 65536, 0);
237
+ const probeBuffer = buffer.subarray(0, bytesRead);
238
+ const format = InputFormat.probe(probeBuffer, input);
239
+ if (!format) {
240
+ return null;
241
+ }
242
+ return {
243
+ format: format.name ?? 'unknown',
244
+ longName: format.longName ?? undefined,
245
+ extensions: format.extensions ?? undefined,
246
+ mimeType: format.mimeType ?? undefined,
247
+ confidence: 90, // File-based probe with filename hint
248
+ };
249
+ }
250
+ catch {
251
+ // If file reading fails, return null
252
+ return null;
253
+ }
254
+ finally {
255
+ await fileHandle?.close();
256
+ }
257
+ }
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+ /**
264
+ * Probe media format without fully opening the file synchronously.
265
+ * Synchronous version of probeFormat.
266
+ *
267
+ * Detects format by analyzing file headers and content.
268
+ * Useful for format validation before processing.
269
+ *
270
+ * Direct mapping to av_probe_input_format().
271
+ *
272
+ * @param input - File path or buffer to probe
273
+ *
274
+ * @returns Format information or null if unrecognized
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * const info = Demuxer.probeFormatSync('video.mp4');
279
+ * if (info) {
280
+ * console.log(`Format: ${info.format}`);
281
+ * console.log(`Confidence: ${info.confidence}%`);
282
+ * }
283
+ * ```
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * // Probe from buffer
288
+ * const buffer = fs.readFileSync('video.webm');
289
+ * const info = Demuxer.probeFormatSync(buffer);
290
+ * console.log(`MIME type: ${info?.mimeType}`);
291
+ * ```
292
+ *
293
+ * @see {@link probeFormat} For async version
294
+ */
295
+ static probeFormatSync(input) {
296
+ try {
297
+ if (Buffer.isBuffer(input)) {
298
+ // Probe from buffer
299
+ const format = InputFormat.probe(input);
300
+ if (!format) {
301
+ return null;
302
+ }
303
+ return {
304
+ format: format.name ?? 'unknown',
305
+ longName: format.longName ?? undefined,
306
+ extensions: format.extensions ?? undefined,
307
+ mimeType: format.mimeType ?? undefined,
308
+ confidence: 100, // Direct probe always has high confidence
309
+ };
310
+ }
311
+ else {
312
+ // For files, read first part and probe
313
+ let fd;
314
+ try {
315
+ fd = openSync(input, 'r');
316
+ // Read first 64KB for probing
317
+ const buffer = Buffer.alloc(65536);
318
+ const bytesRead = readSync(fd, buffer, 0, 65536, 0);
319
+ const probeBuffer = buffer.subarray(0, bytesRead);
320
+ const format = InputFormat.probe(probeBuffer, input);
321
+ if (!format) {
322
+ return null;
323
+ }
324
+ return {
325
+ format: format.name ?? 'unknown',
326
+ longName: format.longName ?? undefined,
327
+ extensions: format.extensions ?? undefined,
328
+ mimeType: format.mimeType ?? undefined,
329
+ confidence: 90, // File-based probe with filename hint
330
+ };
331
+ }
332
+ catch {
333
+ // If file reading fails, return null
334
+ return null;
335
+ }
336
+ finally {
337
+ if (fd !== undefined)
338
+ closeSync(fd);
339
+ }
340
+ }
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ static async open(input, options = {}) {
347
+ // Check if input is raw data
348
+ if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
349
+ // Build options for raw data
350
+ const rawOptions = {
351
+ bufferSize: options.bufferSize,
352
+ format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
353
+ options: {
354
+ ...options.options,
355
+ },
356
+ };
357
+ if (input.type === 'video') {
358
+ rawOptions.options = {
359
+ ...rawOptions.options,
360
+ video_size: `${input.width}x${input.height}`,
361
+ pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
362
+ framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
363
+ };
364
+ }
365
+ else {
366
+ rawOptions.options = {
367
+ ...rawOptions.options,
368
+ sample_rate: input.sampleRate,
369
+ channels: input.channels,
370
+ sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
371
+ };
372
+ }
373
+ input = input.input;
374
+ options = rawOptions;
375
+ }
376
+ // Original implementation for non-raw data
377
+ const formatContext = new FormatContext();
378
+ let ioContext;
379
+ let externalIO = false; // caller-supplied context - released via dispose, not freeContext
380
+ let optionsDict = null;
381
+ let inputFormat = null;
382
+ // Abort during the open phase: avformat_open_input()/find_stream_info()
383
+ // block for seconds on an unresponsive network source, so the signal must
384
+ // fire the native interrupt callback to unblock them.
385
+ const onOpenAbort = () => formatContext.interrupt();
386
+ if (options.signal) {
387
+ options.signal.throwIfAborted();
388
+ options.signal.addEventListener('abort', onOpenAbort, { once: true });
389
+ }
390
+ try {
391
+ // Create options dictionary if options are provided
392
+ if (options.options) {
393
+ optionsDict = Dictionary.fromObject(options.options);
394
+ }
395
+ // Find input format if specified
396
+ if (options.format) {
397
+ inputFormat = InputFormat.findInputFormat(options.format);
398
+ if (!inputFormat) {
399
+ throw new Error(`Input format '${options.format}' not found`);
400
+ }
401
+ }
402
+ if (typeof input === 'string') {
403
+ // File path or URL - resolve relative paths to absolute
404
+ // Skip path resolution for device inputs
405
+ // Check if it's a URL (starts with protocol://) or a file path
406
+ const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
407
+ const resolvedInput = isUrl || inputFormat ? input : resolve(input);
408
+ const ret = await formatContext.openInput(resolvedInput, inputFormat, optionsDict);
409
+ FFmpegError.throwIfError(ret, 'Failed to open input');
410
+ formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
411
+ }
412
+ else if (Buffer.isBuffer(input)) {
413
+ // Validate buffer is not empty
414
+ if (input.length === 0) {
415
+ throw new Error('Cannot open media from empty buffer');
416
+ }
417
+ // From buffer - allocate context first for custom I/O
418
+ formatContext.allocContext();
419
+ ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
420
+ formatContext.pb = ioContext;
421
+ const ret = await formatContext.openInput('', inputFormat, optionsDict);
422
+ FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
423
+ }
424
+ else if (input instanceof IOContext) {
425
+ if (!options.format) {
426
+ throw new Error('Format must be specified for native IOContext input');
427
+ }
428
+ formatContext.allocContext();
429
+ ioContext = input;
430
+ externalIO = true;
431
+ formatContext.pb = ioContext;
432
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
433
+ const ret = await formatContext.openInput('', inputFormat, optionsDict);
434
+ FFmpegError.throwIfError(ret, 'Failed to open input from native IOContext');
435
+ }
436
+ else if (input instanceof Readable) {
437
+ // Readable stream - format is required
438
+ if (!options.format) {
439
+ throw new Error('Format must be specified for Readable stream input');
440
+ }
441
+ formatContext.allocContext();
442
+ ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
443
+ formatContext.pb = ioContext;
444
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
445
+ const ret = await formatContext.openInput('', inputFormat, optionsDict);
446
+ FFmpegError.throwIfError(ret, 'Failed to open input from Readable stream');
447
+ }
448
+ else if (typeof input === 'object' && 'read' in input) {
449
+ // Custom I/O with callbacks - format is required
450
+ if (!options.format) {
451
+ throw new Error('Format must be specified for custom I/O');
452
+ }
453
+ // Allocate context first for custom I/O
454
+ formatContext.allocContext();
455
+ // Setup custom I/O with callbacks
456
+ ioContext = new IOContext();
457
+ ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 0, input.read, null, input.seek);
458
+ formatContext.pb = ioContext;
459
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
460
+ const ret = await formatContext.openInput('', inputFormat, optionsDict);
461
+ FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
462
+ }
463
+ else {
464
+ throw new TypeError('Invalid input type. Expected file path, URL, Buffer, IOContext, Readable, or IOInputCallbacks');
465
+ }
466
+ // Find stream information
467
+ if (!options.skipStreamInfo) {
468
+ const ret = await formatContext.findStreamInfo(null);
469
+ FFmpegError.throwIfError(ret, 'Failed to find stream info');
470
+ // Try to parse extradata for video streams with missing dimensions
471
+ for (const stream of formatContext.streams ?? []) {
472
+ if (stream.codecpar.codecType === AVMEDIA_TYPE_VIDEO) {
473
+ const dimensionsMissing = stream.codecpar.width === 0 || stream.codecpar.height === 0;
474
+ const invalidFormat = stream.codecpar.format === AV_PIX_FMT_NONE;
475
+ const invalidRate = stream.codecpar.frameRate.num === 0 || stream.codecpar.frameRate.den === 0;
476
+ const needsParsing = dimensionsMissing || invalidFormat || invalidRate;
477
+ if (needsParsing && stream.codecpar.extradataSize > 0) {
478
+ stream.codecpar.parseExtradata();
479
+ }
480
+ }
481
+ }
482
+ }
483
+ options.configure?.(formatContext);
484
+ // Determine buffer size
485
+ let bufferSize = options.bufferSize ?? IO_BUFFER_SIZE;
486
+ if (!ioContext && formatContext.iformat && formatContext.pb) {
487
+ // Check if this is a streaming input (like RTSP, HTTP, etc.)
488
+ const isStreaming = formatContext.pb.seekable === 0;
489
+ if (isStreaming) {
490
+ bufferSize *= 2; // double buffer size for streaming inputs
491
+ }
492
+ }
493
+ // Apply defaults to options
494
+ const fullOptions = {
495
+ bufferSize,
496
+ format: options.format ?? '',
497
+ skipStreamInfo: options.skipStreamInfo ?? false,
498
+ startWithKeyframe: options.startWithKeyframe ?? false,
499
+ dtsDeltaThreshold: options.dtsDeltaThreshold ?? DELTA_THRESHOLD,
500
+ dtsErrorThreshold: options.dtsErrorThreshold ?? DTS_ERROR_THRESHOLD,
501
+ copyTs: options.copyTs ?? false,
502
+ options: options.options ?? {},
503
+ };
504
+ const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
505
+ if (options.signal) {
506
+ const signal = options.signal;
507
+ signal.removeEventListener('abort', onOpenAbort);
508
+ demuxer.signal = signal;
509
+ // An abort that raced the successful open must not hand out a usable
510
+ // demuxer - tear it down and surface the abort instead.
511
+ if (signal.aborted) {
512
+ await demuxer.close();
513
+ signal.throwIfAborted();
514
+ }
515
+ // Keep abort -> interrupt wired for the demuxer's lifetime so a
516
+ // blocked av_read_frame() unwinds too; close() removes the listener.
517
+ const onAbort = () => {
518
+ demuxer.demuxThreadActive = false;
519
+ demuxer.interrupt();
520
+ };
521
+ signal.addEventListener('abort', onAbort, { once: true });
522
+ demuxer.signalCleanup = () => signal.removeEventListener('abort', onAbort);
523
+ }
524
+ return demuxer;
525
+ }
526
+ catch (error) {
527
+ options.signal?.removeEventListener('abort', onOpenAbort);
528
+ // Clean up only on error
529
+ if (ioContext) {
530
+ // Clear the pb reference first
531
+ formatContext.pb = null;
532
+ // Release the IOContext (for both custom I/O and buffer-based I/O).
533
+ // A caller-supplied context may own a protocol handle from open2(),
534
+ // which only dispose closes - see close().
535
+ if (externalIO) {
536
+ await ioContext[Symbol.asyncDispose]();
537
+ }
538
+ else {
539
+ ioContext.freeContext();
540
+ }
541
+ }
542
+ // Clean up FormatContext
543
+ await formatContext.closeInput();
544
+ throw error;
545
+ }
546
+ finally {
547
+ // Clean up options dictionary
548
+ if (optionsDict) {
549
+ optionsDict.free();
550
+ }
551
+ }
552
+ }
553
+ static openSync(input, options = {}) {
554
+ // Check if input is raw data
555
+ if (typeof input === 'object' && 'type' in input && ('width' in input || 'sampleRate' in input)) {
556
+ // Build options for raw data
557
+ const rawOptions = {
558
+ bufferSize: options.bufferSize,
559
+ format: options.format ?? (input.type === 'video' ? 'rawvideo' : 's16le'),
560
+ options: {
561
+ ...options.options,
562
+ },
563
+ };
564
+ if (input.type === 'video') {
565
+ rawOptions.options = {
566
+ ...rawOptions.options,
567
+ video_size: `${input.width}x${input.height}`,
568
+ pixel_format: avGetPixFmtName(input.pixelFormat) ?? 'yuv420p',
569
+ framerate: new Rational(input.frameRate.num, input.frameRate.den).toString(),
570
+ };
571
+ }
572
+ else {
573
+ rawOptions.options = {
574
+ ...rawOptions.options,
575
+ sample_rate: input.sampleRate,
576
+ channels: input.channels,
577
+ sample_fmt: avGetSampleFmtName(input.sampleFormat) ?? 's16le',
578
+ };
579
+ }
580
+ input = input.input;
581
+ options = rawOptions;
582
+ }
583
+ // Original implementation for non-raw data
584
+ const formatContext = new FormatContext();
585
+ let ioContext;
586
+ let externalIO = false; // caller-supplied context - released via dispose, not freeContext
587
+ let optionsDict = null;
588
+ let inputFormat = null;
589
+ try {
590
+ // Create options dictionary if options are provided
591
+ if (options.options) {
592
+ optionsDict = Dictionary.fromObject(options.options);
593
+ }
594
+ // Find input format if specified
595
+ if (options.format) {
596
+ inputFormat = InputFormat.findInputFormat(options.format);
597
+ if (!inputFormat) {
598
+ throw new Error(`Input format '${options.format}' not found`);
599
+ }
600
+ }
601
+ if (typeof input === 'string') {
602
+ // File path or URL - resolve relative paths to absolute
603
+ // Skip path resolution for device inputs
604
+ // Check if it's a URL (starts with protocol://) or a file path
605
+ const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input);
606
+ const resolvedInput = isUrl || inputFormat ? input : resolve(input);
607
+ const ret = formatContext.openInputSync(resolvedInput, inputFormat, optionsDict);
608
+ FFmpegError.throwIfError(ret, 'Failed to open input');
609
+ formatContext.setFlags(AVFMT_FLAG_NONBLOCK);
610
+ }
611
+ else if (Buffer.isBuffer(input)) {
612
+ // Validate buffer is not empty
613
+ if (input.length === 0) {
614
+ throw new Error('Cannot open media from empty buffer');
615
+ }
616
+ // From buffer - allocate context first for custom I/O
617
+ formatContext.allocContext();
618
+ ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
619
+ formatContext.pb = ioContext;
620
+ const ret = formatContext.openInputSync('', inputFormat, optionsDict);
621
+ FFmpegError.throwIfError(ret, 'Failed to open input from buffer');
622
+ }
623
+ else if (input instanceof IOContext) {
624
+ if (!options.format) {
625
+ throw new Error('Format must be specified for native IOContext input');
626
+ }
627
+ formatContext.allocContext();
628
+ ioContext = input;
629
+ externalIO = true;
630
+ formatContext.pb = ioContext;
631
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
632
+ const ret = formatContext.openInputSync('', inputFormat, optionsDict);
633
+ FFmpegError.throwIfError(ret, 'Failed to open input from native IOContext');
634
+ }
635
+ else if (input instanceof Readable) {
636
+ // Readable stream - format is required
637
+ if (!options.format) {
638
+ throw new Error('Format must be specified for Readable stream input');
639
+ }
640
+ formatContext.allocContext();
641
+ ioContext = IOStream.create(input, { bufferSize: options.bufferSize });
642
+ formatContext.pb = ioContext;
643
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
644
+ const ret = formatContext.openInputSync('', inputFormat, optionsDict);
645
+ FFmpegError.throwIfError(ret, 'Failed to open input from Readable stream');
646
+ }
647
+ else if (typeof input === 'object' && 'read' in input) {
648
+ // Custom I/O with callbacks - format is required
649
+ if (!options.format) {
650
+ throw new Error('Format must be specified for custom I/O');
651
+ }
652
+ // Allocate context first for custom I/O
653
+ formatContext.allocContext();
654
+ // Setup custom I/O with callbacks
655
+ ioContext = new IOContext();
656
+ ioContext.allocContextWithCallbacks(options.bufferSize ?? IO_BUFFER_SIZE, 0, input.read, null, input.seek);
657
+ formatContext.pb = ioContext;
658
+ formatContext.setFlags(AVFMT_FLAG_CUSTOM_IO);
659
+ const ret = formatContext.openInputSync('', inputFormat, optionsDict);
660
+ FFmpegError.throwIfError(ret, 'Failed to open input from custom I/O');
661
+ }
662
+ else {
663
+ throw new TypeError('Invalid input type. Expected file path, URL, Buffer, IOContext, Readable, or IOInputCallbacks');
664
+ }
665
+ // Find stream information
666
+ if (!options.skipStreamInfo) {
667
+ const ret = formatContext.findStreamInfoSync(null);
668
+ FFmpegError.throwIfError(ret, 'Failed to find stream info');
669
+ }
670
+ options.configure?.(formatContext);
671
+ // Determine buffer size
672
+ let bufferSize = options.bufferSize ?? IO_BUFFER_SIZE;
673
+ if (!ioContext && formatContext.iformat && formatContext.pb) {
674
+ // Check if this is a streaming input (like RTSP, HTTP, etc.)
675
+ const isStreaming = formatContext.pb.seekable === 0;
676
+ if (isStreaming) {
677
+ bufferSize *= 2; // double buffer size for streaming inputs
678
+ }
679
+ }
680
+ // Apply defaults to options
681
+ const fullOptions = {
682
+ bufferSize,
683
+ format: options.format ?? '',
684
+ skipStreamInfo: options.skipStreamInfo ?? false,
685
+ startWithKeyframe: options.startWithKeyframe ?? false,
686
+ dtsDeltaThreshold: options.dtsDeltaThreshold ?? DELTA_THRESHOLD,
687
+ dtsErrorThreshold: options.dtsErrorThreshold ?? DTS_ERROR_THRESHOLD,
688
+ copyTs: options.copyTs ?? false,
689
+ options: options.options ?? {},
690
+ };
691
+ const demuxer = new Demuxer(formatContext, fullOptions, ioContext, externalIO);
692
+ if (options.signal) {
693
+ options.signal.throwIfAborted();
694
+ demuxer.signal = options.signal;
695
+ }
696
+ return demuxer;
697
+ }
698
+ catch (error) {
699
+ // Clean up only on error
700
+ if (ioContext) {
701
+ // Clear the pb reference first
702
+ formatContext.pb = null;
703
+ // Release the IOContext (see open() for why dispose)
704
+ if (externalIO) {
705
+ ioContext[Symbol.dispose]();
706
+ }
707
+ else {
708
+ ioContext.freeContext();
709
+ }
710
+ }
711
+ // Clean up FormatContext
712
+ formatContext.closeInputSync();
713
+ throw error;
714
+ }
715
+ finally {
716
+ // Clean up options dictionary
717
+ if (optionsDict) {
718
+ optionsDict.free();
719
+ }
720
+ }
721
+ }
722
+ /**
723
+ * Open RTP/SRTP input stream via localhost UDP.
724
+ *
725
+ * Creates a Demuxer from SDP string received via UDP socket.
726
+ * Opens UDP socket and configures FFmpeg to receive and parse RTP packets.
727
+ *
728
+ * @param sdpContent - SDP content string describing the RTP stream
729
+ *
730
+ * @throws {Error} If SDP parsing or socket setup fails
731
+ *
732
+ * @throws {FFmpegError} If FFmpeg operations fail
733
+ *
734
+ * @returns Promise with Demuxer, sendPacket function and cleanup
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * import { Demuxer, StreamingUtils } from 'node-av/api';
739
+ * import { AV_CODEC_ID_OPUS } from 'node-av/constants';
740
+ *
741
+ * // Generate SDP for SRTP encrypted Opus
742
+ * const sdp = StreamingUtils.createRTPInputSDP([{
743
+ * port: 5004,
744
+ * codecId: AV_CODEC_ID_OPUS,
745
+ * payloadType: 111,
746
+ * clockRate: 16000,
747
+ * channels: 1,
748
+ * srtp: { key: srtpKey, salt: srtpSalt }
749
+ * }]);
750
+ *
751
+ * // Open RTP input
752
+ * const { input, sendPacket, close } = await Demuxer.openSDP(sdp);
753
+ *
754
+ * // Route encrypted RTP packets from network
755
+ * socket.on('message', (msg) => sendPacket(msg));
756
+ *
757
+ * // Decode audio
758
+ * const decoder = await Decoder.create(input.audio()!);
759
+ * for await (const packet of input.packets()) {
760
+ * const frame = await decoder.decode(packet);
761
+ * // Process frame...
762
+ * }
763
+ *
764
+ * // Cleanup
765
+ * await close();
766
+ * ```
767
+ *
768
+ * @see {@link StreamingUtils.createInputSDP} to generate SDP content.
769
+ */
770
+ static async openSDP(sdpContent) {
771
+ // Extract all ports from SDP (supports multi-stream: video + audio)
772
+ const ports = StreamingUtils.extractPortsFromSDP(sdpContent);
773
+ if (ports.length === 0) {
774
+ throw new Error('Failed to extract any ports from SDP content');
775
+ }
776
+ // Convert SDP to buffer for custom I/O
777
+ const sdpBuffer = Buffer.from(sdpContent);
778
+ let position = 0;
779
+ // Create custom I/O callbacks for SDP content
780
+ const callbacks = {
781
+ read: (size) => {
782
+ if (position >= sdpBuffer.length) {
783
+ return null; // EOF
784
+ }
785
+ const chunk = sdpBuffer.subarray(position, Math.min(position + size, sdpBuffer.length));
786
+ position += chunk.length;
787
+ return chunk;
788
+ },
789
+ seek: (offset, whence) => {
790
+ const offsetNum = Number(offset);
791
+ if (whence === AVSEEK_SET) {
792
+ position = offsetNum;
793
+ }
794
+ else if (whence === AVSEEK_CUR) {
795
+ position += offsetNum;
796
+ }
797
+ else if (whence === AVSEEK_END) {
798
+ position = sdpBuffer.length + offsetNum;
799
+ }
800
+ return position;
801
+ },
802
+ };
803
+ // Create UDP socket for sending packets to FFmpeg
804
+ const udpSocket = createSocket('udp4');
805
+ try {
806
+ // Open Demuxer with SDP format using custom I/O
807
+ const input = await Demuxer.open(callbacks, {
808
+ format: 'sdp',
809
+ skipStreamInfo: true,
810
+ options: {
811
+ protocol_whitelist: 'pipe,udp,rtp,file,crypto',
812
+ listen_timeout: -1,
813
+ },
814
+ });
815
+ const sendPacket = (rtpPacket, streamIndex = 0) => {
816
+ const port = ports[streamIndex];
817
+ if (!port) {
818
+ throw new Error(`No port found for stream index ${streamIndex}. Available streams: ${ports.length}`);
819
+ }
820
+ const data = Buffer.isBuffer(rtpPacket) ? rtpPacket : rtpPacket.serialize();
821
+ udpSocket.send(data, port, '127.0.0.1');
822
+ };
823
+ const close = async () => {
824
+ await input.close();
825
+ udpSocket.close();
826
+ };
827
+ const closeSync = () => {
828
+ input.closeSync();
829
+ udpSocket.close();
830
+ };
831
+ return { input, sendPacket, close, closeSync };
832
+ }
833
+ catch (error) {
834
+ // Cleanup on error
835
+ udpSocket.close();
836
+ throw error;
837
+ }
838
+ }
839
+ /**
840
+ * Open RTP/SRTP input stream via localhost UDP synchronously.
841
+ * Synchronous version of openSDP.
842
+ *
843
+ * Creates a Demuxer from SDP string received via UDP socket.
844
+ * Opens UDP socket and configures FFmpeg to receive and parse RTP packets.
845
+ *
846
+ * @param sdpContent - SDP content string describing the RTP stream
847
+ *
848
+ * @throws {Error} If SDP parsing or socket setup fails
849
+ *
850
+ * @throws {FFmpegError} If FFmpeg operations fail
851
+ *
852
+ * @returns Object with Demuxer, sendPacket function and cleanup
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * import { Demuxer, StreamingUtils } from 'node-av/api';
857
+ * import { AV_CODEC_ID_OPUS } from 'node-av/constants';
858
+ *
859
+ * // Generate SDP for SRTP encrypted Opus
860
+ * const sdp = StreamingUtils.createRTPInputSDP([{
861
+ * port: 5004,
862
+ * codecId: AV_CODEC_ID_OPUS,
863
+ * payloadType: 111,
864
+ * clockRate: 16000,
865
+ * channels: 1,
866
+ * srtp: { key: srtpKey, salt: srtpSalt }
867
+ * }]);
868
+ *
869
+ * // Open RTP input
870
+ * const { input, sendPacket, closeSync } = Demuxer.openSDPSync(sdp);
871
+ *
872
+ * // Route encrypted RTP packets from network
873
+ * socket.on('message', (msg) => sendPacket(msg));
874
+ *
875
+ * // Decode audio
876
+ * const decoder = await Decoder.create(input.audio()!);
877
+ * for await (const packet of input.packets()) {
878
+ * const frame = await decoder.decode(packet);
879
+ * // Process frame...
880
+ * }
881
+ *
882
+ * // Cleanup synchronously
883
+ * closeSync();
884
+ * ```
885
+ *
886
+ * @see {@link StreamingUtils.createInputSDP} to generate SDP content.
887
+ * @see {@link openSDP} For async version
888
+ */
889
+ static openSDPSync(sdpContent) {
890
+ // Extract all ports from SDP (supports multi-stream: video + audio)
891
+ const ports = StreamingUtils.extractPortsFromSDP(sdpContent);
892
+ if (ports.length === 0) {
893
+ throw new Error('Failed to extract any ports from SDP content');
894
+ }
895
+ // Convert SDP to buffer for custom I/O
896
+ const sdpBuffer = Buffer.from(sdpContent);
897
+ let position = 0;
898
+ // Create custom I/O callbacks for SDP content
899
+ const callbacks = {
900
+ read: (size) => {
901
+ if (position >= sdpBuffer.length) {
902
+ return null; // EOF
903
+ }
904
+ const chunk = sdpBuffer.subarray(position, Math.min(position + size, sdpBuffer.length));
905
+ position += chunk.length;
906
+ return chunk;
907
+ },
908
+ seek: (offset, whence) => {
909
+ const offsetNum = Number(offset);
910
+ if (whence === AVSEEK_SET) {
911
+ position = offsetNum;
912
+ }
913
+ else if (whence === AVSEEK_CUR) {
914
+ position += offsetNum;
915
+ }
916
+ else if (whence === AVSEEK_END) {
917
+ position = sdpBuffer.length + offsetNum;
918
+ }
919
+ return position;
920
+ },
921
+ };
922
+ // Create UDP socket for sending packets to FFmpeg
923
+ const udpSocket = createSocket('udp4');
924
+ try {
925
+ // Open Demuxer with SDP format using custom I/O
926
+ const input = Demuxer.openSync(callbacks, {
927
+ format: 'sdp',
928
+ skipStreamInfo: true,
929
+ options: {
930
+ protocol_whitelist: 'pipe,udp,rtp,file,crypto',
931
+ listen_timeout: -1,
932
+ },
933
+ });
934
+ const sendPacket = (rtpPacket, streamIndex = 0) => {
935
+ const port = ports[streamIndex];
936
+ if (!port) {
937
+ throw new Error(`No port found for stream index ${streamIndex}. Available streams: ${ports.length}`);
938
+ }
939
+ const data = Buffer.isBuffer(rtpPacket) ? rtpPacket : rtpPacket.serialize();
940
+ udpSocket.send(data, port, '127.0.0.1');
941
+ };
942
+ const close = async () => {
943
+ await input.close();
944
+ udpSocket.close();
945
+ };
946
+ const closeSync = () => {
947
+ input.closeSync();
948
+ udpSocket.close();
949
+ };
950
+ return { input, sendPacket, close, closeSync };
951
+ }
952
+ catch (error) {
953
+ // Cleanup on error
954
+ udpSocket.close();
955
+ throw error;
956
+ }
957
+ }
958
+ /**
959
+ * Check if input is open.
960
+ *
961
+ * @example
962
+ * ```typescript
963
+ * if (!input.isInputOpen) {
964
+ * console.log('Input is not open');
965
+ * }
966
+ * ```
967
+ */
968
+ get isInputOpen() {
969
+ return !this.isClosed;
970
+ }
971
+ /**
972
+ * Get all streams in the media.
973
+ *
974
+ * @example
975
+ * ```typescript
976
+ * for (const stream of input.streams) {
977
+ * console.log(`Stream ${stream.index}: ${stream.codecpar.codecType}`);
978
+ * }
979
+ * ```
980
+ */
981
+ get streams() {
982
+ return this._streams;
983
+ }
984
+ /**
985
+ * Get media duration in seconds.
986
+ *
987
+ * Returns 0 if duration is unknown or not available or input is closed.
988
+ *
989
+ * @example
990
+ * ```typescript
991
+ * console.log(`Duration: ${input.duration} seconds`);
992
+ * ```
993
+ */
994
+ get duration() {
995
+ if (this.isClosed) {
996
+ return 0;
997
+ }
998
+ const duration = this.formatContext.duration;
999
+ if (!duration || duration <= 0) {
1000
+ return 0;
1001
+ }
1002
+ // Convert from AV_TIME_BASE (microseconds) to seconds
1003
+ return Number(duration) / 1000000;
1004
+ }
1005
+ /**
1006
+ * Get media start time in seconds.
1007
+ *
1008
+ * For device inputs (e.g., avfoundation), this reflects the system uptime
1009
+ * at capture start. Pass this value to `Muxer.open()` via the `startTime`
1010
+ * option to produce correct output timestamps.
1011
+ *
1012
+ * Returns 0 if start time is unknown or not available or input is closed.
1013
+ *
1014
+ * @example
1015
+ * ```typescript
1016
+ * await using input = await Demuxer.open(source);
1017
+ * await using output = await Muxer.open('output.mp4', { startTime: input.startTime });
1018
+ * ```
1019
+ */
1020
+ get startTime() {
1021
+ if (this.isClosed) {
1022
+ return 0;
1023
+ }
1024
+ const startTime = this.formatContext.startTime;
1025
+ if (!startTime || startTime <= 0n) {
1026
+ return 0;
1027
+ }
1028
+ // Convert from AV_TIME_BASE (microseconds) to seconds
1029
+ return Number(startTime) / 1000000;
1030
+ }
1031
+ /**
1032
+ * Get media bitrate in kilobits per second.
1033
+ *
1034
+ * Returns 0 if bitrate is unknown or not available or input is closed.
1035
+ *
1036
+ * @example
1037
+ * ```typescript
1038
+ * console.log(`Bitrate: ${input.bitRate} kbps`);
1039
+ * ```
1040
+ */
1041
+ get bitRate() {
1042
+ if (this.isClosed) {
1043
+ return 0;
1044
+ }
1045
+ const bitrate = this.formatContext.bitRate;
1046
+ if (!bitrate || bitrate <= 0) {
1047
+ return 0;
1048
+ }
1049
+ // Convert from bits per second to kilobits per second
1050
+ return Number(bitrate) / 1000;
1051
+ }
1052
+ /**
1053
+ * Get media metadata.
1054
+ *
1055
+ * Returns all metadata tags as key-value pairs.
1056
+ *
1057
+ * @example
1058
+ * ```typescript
1059
+ * const metadata = input.metadata;
1060
+ * console.log(`Title: ${metadata.title}`);
1061
+ * console.log(`Artist: ${metadata.artist}`);
1062
+ * ```
1063
+ */
1064
+ get metadata() {
1065
+ if (this.isClosed) {
1066
+ return {};
1067
+ }
1068
+ return this.formatContext.metadata?.getAll() ?? {};
1069
+ }
1070
+ /**
1071
+ * Get format name.
1072
+ *
1073
+ * Returns 'unknown' if input is closed or format is not available.
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * console.log(`Format: ${input.formatName}`); // "mov,mp4,m4a,3gp,3g2,mj2"
1078
+ * ```
1079
+ */
1080
+ get formatName() {
1081
+ if (this.isClosed) {
1082
+ return 'unknown';
1083
+ }
1084
+ return this.formatContext.iformat?.name ?? 'unknown';
1085
+ }
1086
+ /**
1087
+ * Get format long name.
1088
+ *
1089
+ * Returns 'Unknown Format' if input is closed or format is not available.
1090
+ *
1091
+ * @example
1092
+ * ```typescript
1093
+ * console.log(`Format: ${input.formatLongName}`); // "QuickTime / MOV"
1094
+ * ```
1095
+ */
1096
+ get formatLongName() {
1097
+ if (this.isClosed) {
1098
+ return 'Unknown Format';
1099
+ }
1100
+ return this.formatContext.iformat?.longName ?? 'Unknown Format';
1101
+ }
1102
+ /**
1103
+ * Get MIME type of the input format.
1104
+ *
1105
+ * Returns null if input is closed or format is not available.
1106
+ *
1107
+ * @example
1108
+ * ```typescript
1109
+ * console.log(`MIME Type: ${input.mimeType}`); // "video/mp4"
1110
+ * ```
1111
+ */
1112
+ get mimeType() {
1113
+ if (this.isClosed) {
1114
+ return null;
1115
+ }
1116
+ return this.formatContext.iformat?.mimeType ?? null;
1117
+ }
1118
+ /**
1119
+ * Get input stream by index.
1120
+ *
1121
+ * Returns the stream at the specified index.
1122
+ *
1123
+ * @param index - Stream index
1124
+ *
1125
+ * @returns Stream or undefined if index is invalid
1126
+ *
1127
+ * @example
1128
+ * ```typescript
1129
+ * const input = await Demuxer.open('input.mp4');
1130
+ *
1131
+ * // Get the input stream to inspect codec parameters
1132
+ * const stream = input.getStream(1); // Get stream at index 1
1133
+ * if (stream) {
1134
+ * console.log(`Input codec: ${stream.codecpar.codecId}`);
1135
+ * }
1136
+ * ```
1137
+ *
1138
+ * @see {@link video} For getting video streams
1139
+ * @see {@link audio} For getting audio streams
1140
+ */
1141
+ getStream(index) {
1142
+ const streams = this.formatContext.streams;
1143
+ if (!streams || index < 0 || index >= streams.length) {
1144
+ return undefined;
1145
+ }
1146
+ return streams[index];
1147
+ }
1148
+ /**
1149
+ * Get video stream by index.
1150
+ *
1151
+ * Returns the nth video stream (0-based index).
1152
+ * Returns undefined if stream doesn't exist.
1153
+ *
1154
+ * @param index - Video stream index (default: 0)
1155
+ *
1156
+ * @returns Video stream or undefined
1157
+ *
1158
+ * @example
1159
+ * ```typescript
1160
+ * const videoStream = input.video();
1161
+ * if (videoStream) {
1162
+ * console.log(`Video: ${videoStream.codecpar.width}x${videoStream.codecpar.height}`);
1163
+ * }
1164
+ * ```
1165
+ *
1166
+ * @example
1167
+ * ```typescript
1168
+ * // Get second video stream
1169
+ * const secondVideo = input.video(1);
1170
+ * ```
1171
+ *
1172
+ * @see {@link audio} For audio streams
1173
+ * @see {@link findBestStream} For automatic selection
1174
+ */
1175
+ video(index = 0) {
1176
+ const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_VIDEO);
1177
+ return streams[index];
1178
+ }
1179
+ /**
1180
+ * Get audio stream by index.
1181
+ *
1182
+ * Returns the nth audio stream (0-based index).
1183
+ * Returns undefined if stream doesn't exist.
1184
+ *
1185
+ * @param index - Audio stream index (default: 0)
1186
+ *
1187
+ * @returns Audio stream or undefined
1188
+ *
1189
+ * @example
1190
+ * ```typescript
1191
+ * const audioStream = input.audio();
1192
+ * if (audioStream) {
1193
+ * console.log(`Audio: ${audioStream.codecpar.sampleRate}Hz`);
1194
+ * }
1195
+ * ```
1196
+ *
1197
+ * @example
1198
+ * ```typescript
1199
+ * // Get second audio stream
1200
+ * const secondAudio = input.audio(1);
1201
+ * ```
1202
+ *
1203
+ * @see {@link video} For video streams
1204
+ * @see {@link findBestStream} For automatic selection
1205
+ */
1206
+ audio(index = 0) {
1207
+ const streams = this._streams.filter((s) => s.codecpar.codecType === AVMEDIA_TYPE_AUDIO);
1208
+ return streams[index];
1209
+ }
1210
+ /**
1211
+ * Get input format details.
1212
+ *
1213
+ * Returns null if input is closed or format is not available.
1214
+ *
1215
+ * @returns Input format or null
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * const inputFormat = input.inputFormat;
1220
+ * if (inputFormat) {
1221
+ * console.log(`Input Format: ${inputFormat.name}`);
1222
+ * }
1223
+ * ```
1224
+ */
1225
+ inputFormat() {
1226
+ return this.formatContext.iformat;
1227
+ }
1228
+ /**
1229
+ * Find the best stream of a given type.
1230
+ *
1231
+ * Uses FFmpeg's stream selection algorithm.
1232
+ * Considers codec support, default flags, and quality.
1233
+ *
1234
+ * Direct mapping to av_find_best_stream().
1235
+ *
1236
+ * @param type - Media type to find
1237
+ *
1238
+ * @returns Best stream or undefined if not found or input is closed
1239
+ *
1240
+ * @example
1241
+ * ```typescript
1242
+ * import { AVMEDIA_TYPE_VIDEO } from 'node-av/constants';
1243
+ *
1244
+ * const bestVideo = input.findBestStream(AVMEDIA_TYPE_VIDEO);
1245
+ * if (bestVideo) {
1246
+ * const decoder = await Decoder.create(bestVideo);
1247
+ * }
1248
+ * ```
1249
+ *
1250
+ * @see {@link video} For direct video stream access
1251
+ * @see {@link audio} For direct audio stream access
1252
+ */
1253
+ findBestStream(type) {
1254
+ if (this.isClosed) {
1255
+ return undefined;
1256
+ }
1257
+ const bestStreamIndex = this.formatContext.findBestStream(type);
1258
+ return this._streams.find((s) => s.index === bestStreamIndex);
1259
+ }
1260
+ /**
1261
+ * Read packets from media as async generator.
1262
+ *
1263
+ * Yields demuxed packets for processing.
1264
+ * Automatically handles packet memory management.
1265
+ * Optionally filters packets by stream index.
1266
+ *
1267
+ * **Supports parallel generators**: Multiple `packets()` iterators can run concurrently.
1268
+ * When multiple generators are active, an internal demux thread automatically handles
1269
+ * packet distribution to avoid race conditions.
1270
+ *
1271
+ * Direct mapping to av_read_frame().
1272
+ *
1273
+ * @param index - Optional stream index to filter
1274
+ *
1275
+ * @yields {Packet} Demuxed packets (must be freed by caller)
1276
+ *
1277
+ * @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
1278
+ * or a dropped network source) - only a real end-of-file ends the stream cleanly
1279
+ *
1280
+ * @throws {Error} If packet cloning fails
1281
+ *
1282
+ * @example
1283
+ * ```typescript
1284
+ * // Read all packets
1285
+ * for await (const packet of input.packets()) {
1286
+ * console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
1287
+ * packet.free();
1288
+ * }
1289
+ * ```
1290
+ *
1291
+ * @example
1292
+ * ```typescript
1293
+ * // Read only video packets
1294
+ * const videoStream = input.video();
1295
+ * for await (const packet of input.packets(videoStream.index)) {
1296
+ * // Process video packet
1297
+ * packet.free();
1298
+ * }
1299
+ * ```
1300
+ *
1301
+ * @example
1302
+ * ```typescript
1303
+ * // Parallel processing of video and audio streams
1304
+ * const videoGen = input.packets(videoStream.index);
1305
+ * const audioGen = input.packets(audioStream.index);
1306
+ *
1307
+ * await Promise.all([
1308
+ * (async () => {
1309
+ * for await (const packet of videoGen) {
1310
+ * // Process video
1311
+ * packet.free();
1312
+ * }
1313
+ * })(),
1314
+ * (async () => {
1315
+ * for await (const packet of audioGen) {
1316
+ * // Process audio
1317
+ * packet.free();
1318
+ * }
1319
+ * })()
1320
+ * ]);
1321
+ * ```
1322
+ *
1323
+ * @see {@link Decoder.frames} For decoding packets
1324
+ */
1325
+ async *packets(index) {
1326
+ // Register this generator
1327
+ this.activeGenerators++;
1328
+ const queueKey = index ?? 'all';
1329
+ // Initialize queue for this generator and count it as a consumer of the key.
1330
+ if (!this.packetQueues.has(queueKey)) {
1331
+ this.packetQueues.set(queueKey, []);
1332
+ }
1333
+ this.packetQueueConsumers.set(queueKey, (this.packetQueueConsumers.get(queueKey) ?? 0) + 1);
1334
+ // Always start demux thread (handles single and multiple generators)
1335
+ this.startDemuxThread();
1336
+ try {
1337
+ let hasSeenKeyframe = !this.options.startWithKeyframe;
1338
+ // Read from queue (demux thread is handling av_read_frame)
1339
+ const queue = this.packetQueues.get(queueKey);
1340
+ while (!this.isClosed) {
1341
+ if (this.signal?.aborted) {
1342
+ break;
1343
+ }
1344
+ // Try to get packet from queue
1345
+ let packet = queue.shift();
1346
+ if (packet) {
1347
+ this.wakeDemuxThread();
1348
+ }
1349
+ // If queue is empty, wait for next packet
1350
+ if (!packet) {
1351
+ // Queue drained - check for end of stream. Buffered packets always
1352
+ // drain before EOF/error is honored, so successfully read data is
1353
+ // never dropped.
1354
+ if (this.demuxEof) {
1355
+ // The read loop ended with a real error (not EOF): propagate it
1356
+ // instead of ending the stream silently - a dead live source must
1357
+ // be distinguishable from a finished file.
1358
+ if (this.lastError) {
1359
+ throw this.lastError;
1360
+ }
1361
+ break; // End of stream
1362
+ }
1363
+ // Create promise and register resolver. Multiple consumers may wait
1364
+ // on the same queue key concurrently, so resolvers are collected per
1365
+ // key - a single slot would be overwritten by the second consumer,
1366
+ // parking the first one forever (whole-demuxer deadlock).
1367
+ const { promise, resolve } = Promise.withResolvers();
1368
+ const resolvers = this.queueResolvers.get(queueKey);
1369
+ if (resolvers) {
1370
+ resolvers.push(resolve);
1371
+ }
1372
+ else {
1373
+ this.queueResolvers.set(queueKey, [resolve]);
1374
+ }
1375
+ // Wait for demux thread to add packet
1376
+ await promise;
1377
+ // Check for abort after wakeup
1378
+ if (this.signal?.aborted) {
1379
+ break;
1380
+ }
1381
+ packet = queue.shift();
1382
+ if (!packet) {
1383
+ // Woken without a packet for us - either EOF/error or another
1384
+ // consumer grabbed it. Loop back and re-evaluate.
1385
+ if (this.demuxEof) {
1386
+ if (this.lastError) {
1387
+ throw this.lastError;
1388
+ }
1389
+ break;
1390
+ }
1391
+ continue;
1392
+ }
1393
+ this.wakeDemuxThread();
1394
+ }
1395
+ // Apply keyframe filtering if needed
1396
+ if (!hasSeenKeyframe) {
1397
+ const stream = this._streams[packet.streamIndex];
1398
+ const isVideoStream = stream?.codecpar.codecType === AVMEDIA_TYPE_VIDEO;
1399
+ if (isVideoStream && packet.isKeyframe) {
1400
+ hasSeenKeyframe = true;
1401
+ }
1402
+ else if (isVideoStream && !packet.isKeyframe) {
1403
+ packet.free();
1404
+ continue;
1405
+ }
1406
+ }
1407
+ yield packet;
1408
+ }
1409
+ }
1410
+ finally {
1411
+ // Unregister this generator
1412
+ this.activeGenerators--;
1413
+ // Drop this consumer from its queue key. When the last consumer of a key
1414
+ // leaves, remove the queue so the demux thread stops treating it as a
1415
+ // backpressure target - otherwise a full queue whose consumer has already
1416
+ // stopped would block the read loop forever and starve the other streams
1417
+ // (teardown deadlock with multi-stream pipelines). Free any buffered packets.
1418
+ const remaining = (this.packetQueueConsumers.get(queueKey) ?? 1) - 1;
1419
+ if (remaining <= 0) {
1420
+ this.packetQueueConsumers.delete(queueKey);
1421
+ const abandoned = this.packetQueues.get(queueKey);
1422
+ this.packetQueues.delete(queueKey);
1423
+ if (abandoned) {
1424
+ for (const p of abandoned) {
1425
+ p.free();
1426
+ }
1427
+ }
1428
+ // The demux thread may be waiting on this queue, so wake it to check the remaining ones.
1429
+ this.wakeDemuxThread();
1430
+ }
1431
+ else {
1432
+ this.packetQueueConsumers.set(queueKey, remaining);
1433
+ }
1434
+ // Stop demux thread if no more generators
1435
+ if (this.activeGenerators === 0) {
1436
+ await this.stopDemuxThread();
1437
+ }
1438
+ }
1439
+ // Only reached when the loop ended naturally (EOF, abort, close). A consumer
1440
+ // break resumes inside the finally above and completes the generator there -
1441
+ // yielding from the finally would suspend forever inside iterator.return(),
1442
+ // violating the iterator protocol.
1443
+ this.signal?.throwIfAborted();
1444
+ yield null; // Signal EOF
1445
+ }
1446
+ /**
1447
+ * Read packets from media as generator synchronously.
1448
+ * Synchronous version of packets.
1449
+ *
1450
+ * Yields demuxed packets for processing.
1451
+ * Automatically handles packet memory management.
1452
+ * Optionally filters packets by stream index.
1453
+ *
1454
+ * Direct mapping to av_read_frame().
1455
+ *
1456
+ * @param index - Optional stream index to filter
1457
+ *
1458
+ * @yields {Packet} Demuxed packets (must be freed by caller)
1459
+ *
1460
+ * @throws {FFmpegError} If reading fails with an error other than EOF (e.g. I/O failure
1461
+ * or a dropped network source) - only a real end-of-file ends the stream cleanly
1462
+ *
1463
+ * @throws {Error} If packet cloning fails
1464
+ *
1465
+ * @example
1466
+ * ```typescript
1467
+ * // Read all packets
1468
+ * for (const packet of input.packetsSync()) {
1469
+ * console.log(`Packet: stream=${packet.streamIndex}, pts=${packet.pts}`);
1470
+ * packet.free();
1471
+ * }
1472
+ * ```
1473
+ *
1474
+ * @example
1475
+ * ```typescript
1476
+ * // Read only video packets
1477
+ * const videoStream = input.video();
1478
+ * for (const packet of input.packetsSync(videoStream.index)) {
1479
+ * // Process video packet
1480
+ * packet.free();
1481
+ * }
1482
+ * ```
1483
+ *
1484
+ * @see {@link packets} For async version
1485
+ */
1486
+ *packetsSync(index) {
1487
+ const env_1 = { stack: [], error: void 0, hasError: false };
1488
+ try {
1489
+ const packet = __addDisposableResource(env_1, new Packet(), false);
1490
+ packet.alloc();
1491
+ let hasSeenKeyframe = !this.options.startWithKeyframe;
1492
+ while (!this.isClosed) {
1493
+ const ret = this.formatContext.readFrameSync(packet);
1494
+ if (ret < 0) {
1495
+ // No data available yet (live device capture) - blocking retry
1496
+ if (ret === AVERROR_EAGAIN) {
1497
+ sleepSync(10);
1498
+ continue;
1499
+ }
1500
+ // Real read errors propagate; end-of-file and a deliberate
1501
+ // interrupt() (AVERROR_EXIT) end cleanly
1502
+ if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
1503
+ throw new FFmpegError(ret);
1504
+ }
1505
+ break;
1506
+ }
1507
+ // Get stream for timestamp processing
1508
+ const stream = this._streams[packet.streamIndex];
1509
+ if (stream) {
1510
+ // Set packet timebase to stream timebase
1511
+ // This must be done BEFORE any timestamp processing
1512
+ packet.timeBase = stream.timeBase;
1513
+ // Apply timestamp processing
1514
+ // 1. PTS wrap-around correction
1515
+ this.ptsWrapAroundCorrection(packet, stream);
1516
+ // 2. Timestamp discontinuity processing
1517
+ this.timestampDiscontinuityProcess(packet, stream);
1518
+ // 3. DTS prediction/update
1519
+ this.dtsPredict(packet, stream);
1520
+ }
1521
+ if (index === undefined || packet.streamIndex === index) {
1522
+ // If startWithKeyframe is enabled, skip packets until we see a keyframe
1523
+ // Only apply to video streams - audio packets should always pass through
1524
+ if (!hasSeenKeyframe) {
1525
+ const stream = this._streams[packet.streamIndex];
1526
+ const isVideoStream = stream?.codecpar.codecType === AVMEDIA_TYPE_VIDEO;
1527
+ if (isVideoStream && packet.isKeyframe) {
1528
+ hasSeenKeyframe = true;
1529
+ }
1530
+ else if (isVideoStream && !packet.isKeyframe) {
1531
+ // Skip video P-frames until first keyframe
1532
+ packet.unref();
1533
+ continue;
1534
+ }
1535
+ // Non-video streams (audio, etc.) always pass through
1536
+ }
1537
+ // Clone the packet for the user
1538
+ // This creates a new Packet object that shares the same data buffer
1539
+ // through reference counting. The data won't be freed until both
1540
+ // the original and the clone are unreferenced.
1541
+ const cloned = packet.clone();
1542
+ if (!cloned) {
1543
+ throw new Error('Failed to clone packet (out of memory)');
1544
+ }
1545
+ yield cloned;
1546
+ }
1547
+ // Unreference the original packet's data buffer
1548
+ // This allows us to reuse the packet object for the next readFrame()
1549
+ // The data itself is still alive because the clone has a reference
1550
+ packet.unref();
1551
+ }
1552
+ // Signal EOF
1553
+ yield null;
1554
+ }
1555
+ catch (e_1) {
1556
+ env_1.error = e_1;
1557
+ env_1.hasError = true;
1558
+ }
1559
+ finally {
1560
+ __disposeResources(env_1);
1561
+ }
1562
+ }
1563
+ /**
1564
+ * Seek to timestamp in media.
1565
+ *
1566
+ * Seeks to the specified position in seconds.
1567
+ * Can seek in specific stream or globally.
1568
+ *
1569
+ * Direct mapping to av_seek_frame().
1570
+ *
1571
+ * @param timestamp - Target position in seconds
1572
+ *
1573
+ * @param streamIndex - Stream index or -1 for global (default: -1)
1574
+ *
1575
+ * @param flags - Seek flags (default: AVFLAG_NONE)
1576
+ *
1577
+ * @returns 0 on success, negative on error
1578
+ *
1579
+ * @throws {Error} If input is closed
1580
+ *
1581
+ * @example
1582
+ * ```typescript
1583
+ * // Seek to 30 seconds
1584
+ * const ret = await input.seek(30);
1585
+ * FFmpegError.throwIfError(ret, 'seek failed');
1586
+ * ```
1587
+ *
1588
+ * @example
1589
+ * ```typescript
1590
+ * import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
1591
+ *
1592
+ * // Seek to keyframe before 60 seconds
1593
+ * await input.seek(60, -1, AVSEEK_FLAG_BACKWARD);
1594
+ * ```
1595
+ *
1596
+ * @see {@link AVSeekFlag} For seek flags
1597
+ */
1598
+ async seek(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
1599
+ this.signal?.throwIfAborted();
1600
+ if (this.isClosed) {
1601
+ throw new Error('Cannot seek on closed input');
1602
+ }
1603
+ // Convert seconds to AV_TIME_BASE
1604
+ const ts = BigInt(Math.floor(timestamp * 1000000));
1605
+ return this.formatContext.seekFrame(streamIndex, ts, flags);
1606
+ }
1607
+ /**
1608
+ * Seek to timestamp in media synchronously.
1609
+ * Synchronous version of seek.
1610
+ *
1611
+ * Seeks to the specified position in seconds.
1612
+ * Can seek in specific stream or globally.
1613
+ *
1614
+ * Direct mapping to av_seek_frame().
1615
+ *
1616
+ * @param timestamp - Target position in seconds
1617
+ *
1618
+ * @param streamIndex - Stream index or -1 for global (default: -1)
1619
+ *
1620
+ * @param flags - Seek flags (default: AVFLAG_NONE)
1621
+ *
1622
+ * @returns 0 on success, negative on error
1623
+ *
1624
+ * @throws {Error} If input is closed
1625
+ *
1626
+ * @example
1627
+ * ```typescript
1628
+ * // Seek to 30 seconds
1629
+ * const ret = input.seekSync(30);
1630
+ * FFmpegError.throwIfError(ret, 'seek failed');
1631
+ * ```
1632
+ *
1633
+ * @example
1634
+ * ```typescript
1635
+ * import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants';
1636
+ *
1637
+ * // Seek to keyframe before 60 seconds
1638
+ * input.seekSync(60, -1, AVSEEK_FLAG_BACKWARD);
1639
+ * ```
1640
+ *
1641
+ * @see {@link seek} For async version
1642
+ */
1643
+ seekSync(timestamp, streamIndex = -1, flags = AVFLAG_NONE) {
1644
+ if (this.isClosed) {
1645
+ throw new Error('Cannot seek on closed input');
1646
+ }
1647
+ // Convert seconds to AV_TIME_BASE
1648
+ const ts = BigInt(Math.floor(timestamp * 1000000));
1649
+ return this.formatContext.seekFrameSync(streamIndex, ts, flags);
1650
+ }
1651
+ /**
1652
+ * Wake the demux thread if it is parked waiting for a full queue to drain.
1653
+ *
1654
+ * @internal
1655
+ */
1656
+ wakeDemuxThread() {
1657
+ const resolve = this.drainResolver;
1658
+ if (resolve) {
1659
+ this.drainResolver = null;
1660
+ resolve();
1661
+ }
1662
+ }
1663
+ /**
1664
+ * Interrupt a blocking read without closing the demuxer.
1665
+ *
1666
+ * Aborts any in-progress `av_read_frame()` (e.g. on a quiet RTSP/network source
1667
+ * or a stalled Readable/custom I/O source that is waiting for data) and signals
1668
+ * end-of-stream to packet consumers, so an active pipeline can drain and finish.
1669
+ * The demuxer is not freed and its streams stay valid - call {@link close}
1670
+ * afterwards to release resources.
1671
+ *
1672
+ * Mainly used by pipeline teardown: a blocking read only unblocks on close, but
1673
+ * closing while the pipeline is still draining would free the context too early,
1674
+ * so the read is interrupted first and the input closed once processing settles.
1675
+ *
1676
+ * @example
1677
+ * ```typescript
1678
+ * control.stop();
1679
+ * input.interrupt(); // unblock a stalled read so completion can resolve
1680
+ * await control.completion;
1681
+ * await input.close();
1682
+ * ```
1683
+ *
1684
+ * @see {@link close} To close and free the demuxer
1685
+ */
1686
+ interrupt() {
1687
+ if (this.isClosed) {
1688
+ return;
1689
+ }
1690
+ // Abort the native blocking read so readFrame() returns immediately.
1691
+ this.formatContext.interrupt();
1692
+ // Signal EOF to consumers and wake any generator parked on an empty queue.
1693
+ this.demuxEof = true;
1694
+ this.wakeDemuxThread();
1695
+ for (const resolvers of this.queueResolvers.values()) {
1696
+ for (const resolve of resolvers) {
1697
+ resolve();
1698
+ }
1699
+ }
1700
+ this.queueResolvers.clear();
1701
+ }
1702
+ /**
1703
+ * Start the internal demux thread for handling multiple parallel packet generators.
1704
+ * This thread reads packets from the format context and distributes them to queues.
1705
+ *
1706
+ * @internal
1707
+ */
1708
+ startDemuxThread() {
1709
+ if (this.demuxThreadActive || this.demuxThread) {
1710
+ return; // Already running
1711
+ }
1712
+ this.demuxThreadActive = true;
1713
+ if (this.signal && !this.signalCleanup) {
1714
+ const handler = () => {
1715
+ this.demuxThreadActive = false;
1716
+ // Abort the native blocking read too - setting the flag alone leaves the
1717
+ // demux thread parked in av_read_frame() until data happens to arrive.
1718
+ // interrupt() also signals EOF and wakes the resolvers below.
1719
+ this.interrupt();
1720
+ };
1721
+ this.signal.addEventListener('abort', handler, { once: true });
1722
+ this.signalCleanup = () => this.signal?.removeEventListener('abort', handler);
1723
+ }
1724
+ this.demuxThread = (async () => {
1725
+ const env_2 = { stack: [], error: void 0, hasError: false };
1726
+ try {
1727
+ const packet = __addDisposableResource(env_2, new Packet(), false);
1728
+ packet.alloc();
1729
+ while (this.demuxThreadActive && !this.isClosed) {
1730
+ // Read next packet
1731
+ const ret = await this.formatContext.readFrame(packet);
1732
+ // IMPORTANT: Check isClosed again after await - the demuxer may have been
1733
+ // closed while we were waiting for readFrame(). If closed, the native
1734
+ // AVStreams have been freed and accessing them would cause use-after-free!
1735
+ if (this.isClosed) {
1736
+ break;
1737
+ }
1738
+ if (ret < 0) {
1739
+ // EAGAIN means no data available yet (common with live device capture)
1740
+ // Retry after a short delay instead of treating as EOF
1741
+ // Matches FFmpeg CLI behavior: av_usleep(10000) in ffmpeg_demux.c
1742
+ if (ret === AVERROR_EAGAIN) {
1743
+ await new Promise((resolve) => setTimeout(resolve, 10));
1744
+ continue;
1745
+ }
1746
+ // Anything but a genuine end-of-file is a real read error (I/O
1747
+ // failure, dropped network source, ...). Store it so consumers throw
1748
+ // instead of seeing a clean EOF - matching FFmpeg CLI, where a read
1749
+ // error ends processing with a failure, not a normal finish.
1750
+ // AVERROR_EXIT is exempt: it is how interrupt() aborts a blocking
1751
+ // read on purpose (pipeline stop/teardown), not a source failure.
1752
+ if (!FFmpegError.is(ret, AVERROR_EOF) && !FFmpegError.is(ret, AVERROR_EXIT)) {
1753
+ this.lastError = new FFmpegError(ret);
1754
+ }
1755
+ // End of stream (or error) - notify all waiting consumers
1756
+ this.demuxEof = true;
1757
+ for (const resolvers of this.queueResolvers.values()) {
1758
+ for (const resolve of resolvers) {
1759
+ resolve();
1760
+ }
1761
+ }
1762
+ this.queueResolvers.clear();
1763
+ break;
1764
+ }
1765
+ // Get stream for timestamp processing
1766
+ const stream = this._streams[packet.streamIndex];
1767
+ if (stream) {
1768
+ packet.timeBase = stream.timeBase;
1769
+ this.ptsWrapAroundCorrection(packet, stream);
1770
+ this.timestampDiscontinuityProcess(packet, stream);
1771
+ this.dtsPredict(packet, stream);
1772
+ }
1773
+ // Find which queues need this packet. Select by existence, not by
1774
+ // fullness: each consumer registers a queue for its stream and needs
1775
+ // EVERY packet of that stream. Only streams with no registered consumer
1776
+ // (e.g. a subtitle track absent from the pipeline) are skipped here.
1777
+ const allQueue = this.packetQueues.get('all');
1778
+ const streamQueue = this.packetQueues.get(packet.streamIndex);
1779
+ const targetQueues = [];
1780
+ if (allQueue) {
1781
+ targetQueues.push({ queue: allQueue, key: 'all', event: 'packet-all' });
1782
+ }
1783
+ // Only add stream queue if it's different from 'all' queue
1784
+ if (streamQueue && streamQueue !== allQueue) {
1785
+ targetQueues.push({ queue: streamQueue, key: packet.streamIndex, event: `packet-${packet.streamIndex}` });
1786
+ }
1787
+ if (targetQueues.length === 0) {
1788
+ // No consumer for this stream, skip it
1789
+ packet.unref();
1790
+ continue;
1791
+ }
1792
+ // Backpressure: if a target queue is full, wait until the consumer drains
1793
+ // it instead of dropping the packet. Dropping loses reference frames and
1794
+ // corrupts decoding (e.g. H.264 "missing reference picture") - the bug
1795
+ // that surfaced with multi-stream pipelines where one stream (video)
1796
+ // decodes slower than the other (audio). The read loop is paced by the
1797
+ // slowest consumer; faster consumers simply idle on an empty queue.
1798
+ //
1799
+ // Only wait on queues whose consumer is still live: when a generator stops
1800
+ // it removes its queue from packetQueues, so `get(key) !== target.queue`
1801
+ // means that consumer is gone - skip it instead of blocking forever, which
1802
+ // would starve the other streams and deadlock teardown.
1803
+ let abandoned = false;
1804
+ while (this.demuxThreadActive && !this.isClosed) {
1805
+ let queueFull = false;
1806
+ let anyLive = false;
1807
+ for (const target of targetQueues) {
1808
+ if (this.packetQueues.get(target.key) !== target.queue) {
1809
+ continue; // consumer gone - do not block on its queue
1810
+ }
1811
+ anyLive = true;
1812
+ if (target.queue.length >= MAX_INPUT_QUEUE_SIZE) {
1813
+ queueFull = true;
1814
+ break;
1815
+ }
1816
+ }
1817
+ if (!anyLive) {
1818
+ // Every target's consumer has stopped - drop this packet.
1819
+ abandoned = true;
1820
+ break;
1821
+ }
1822
+ if (!queueFull) {
1823
+ break;
1824
+ }
1825
+ // Wait until a consumer frees a slot or the demuxer shuts down. Polling here would keep a CPU core busy.
1826
+ const { promise: drained, resolve: onDrain } = Promise.withResolvers();
1827
+ this.drainResolver = onDrain;
1828
+ await drained;
1829
+ }
1830
+ if (abandoned || !this.demuxThreadActive || this.isClosed) {
1831
+ packet.unref();
1832
+ if (abandoned) {
1833
+ continue;
1834
+ }
1835
+ break;
1836
+ }
1837
+ // Distribute to every live target queue. Each gets its own clone, which
1838
+ // shares the data buffer via reference counting. Skip any consumer that
1839
+ // vanished during the backpressure wait so its clone is not leaked into
1840
+ // an orphaned queue.
1841
+ for (const target of targetQueues) {
1842
+ if (this.packetQueues.get(target.key) !== target.queue) {
1843
+ continue; // consumer gone
1844
+ }
1845
+ const clone = packet.clone();
1846
+ if (!clone) {
1847
+ throw new Error('Failed to clone packet in demux thread (out of memory)');
1848
+ }
1849
+ target.queue.push(clone);
1850
+ // Wake every consumer waiting on this key - only one gets the packet,
1851
+ // the others re-park, but a lost wakeup would deadlock them.
1852
+ const resolvers = this.queueResolvers.get(target.key);
1853
+ if (resolvers) {
1854
+ this.queueResolvers.delete(target.key);
1855
+ for (const resolve of resolvers) {
1856
+ resolve();
1857
+ }
1858
+ }
1859
+ }
1860
+ packet.unref();
1861
+ }
1862
+ this.demuxThreadActive = false;
1863
+ }
1864
+ catch (e_2) {
1865
+ env_2.error = e_2;
1866
+ env_2.hasError = true;
1867
+ }
1868
+ finally {
1869
+ __disposeResources(env_2);
1870
+ }
1871
+ })();
1872
+ }
1873
+ /**
1874
+ * Stop the internal demux thread.
1875
+ *
1876
+ * @internal
1877
+ */
1878
+ async stopDemuxThread() {
1879
+ this.demuxThreadActive = false;
1880
+ this.demuxEof = true;
1881
+ this.wakeDemuxThread();
1882
+ // Wake up any waiting generators
1883
+ for (const resolvers of this.queueResolvers.values()) {
1884
+ for (const resolve of resolvers) {
1885
+ resolve();
1886
+ }
1887
+ }
1888
+ this.queueResolvers.clear();
1889
+ // Wait for demux thread with timeout to avoid hanging on blocked reads
1890
+ if (this.demuxThread) {
1891
+ const threadPromise = this.demuxThread;
1892
+ let timer;
1893
+ const timeoutPromise = new Promise((resolve) => {
1894
+ timer = setTimeout(resolve, 2000);
1895
+ unrefTimer(timer);
1896
+ });
1897
+ await Promise.race([threadPromise, timeoutPromise]);
1898
+ clearTimeout(timer);
1899
+ this.demuxThread = null;
1900
+ }
1901
+ // Clear all queues
1902
+ for (const queue of this.packetQueues.values()) {
1903
+ for (const packet of queue) {
1904
+ packet.free();
1905
+ }
1906
+ queue.length = 0;
1907
+ }
1908
+ this.packetQueues.clear();
1909
+ }
1910
+ /**
1911
+ * Get or create stream state for timestamp processing.
1912
+ *
1913
+ * @param streamIndex - Stream index
1914
+ *
1915
+ * @returns Stream state
1916
+ *
1917
+ * @internal
1918
+ */
1919
+ getStreamState(streamIndex) {
1920
+ let state = this.streamStates.get(streamIndex);
1921
+ if (!state) {
1922
+ state = {
1923
+ wrapCorrectionDone: false,
1924
+ sawFirstTs: false,
1925
+ firstDts: AV_NOPTS_VALUE,
1926
+ nextDts: AV_NOPTS_VALUE,
1927
+ dts: AV_NOPTS_VALUE,
1928
+ };
1929
+ this.streamStates.set(streamIndex, state);
1930
+ }
1931
+ return state;
1932
+ }
1933
+ /**
1934
+ * PTS Wrap-Around Correction.
1935
+ *
1936
+ * Based on FFmpeg's ts_fixup().
1937
+ *
1938
+ * Corrects timestamp wrap-around for streams with limited timestamp bits.
1939
+ * DVB streams typically use 31-bit timestamps that wrap around.
1940
+ * Without correction, timestamps become negative causing playback errors.
1941
+ *
1942
+ * Handles:
1943
+ * - Detects wrap-around based on pts_wrap_bits from stream
1944
+ * - Applies correction once per stream
1945
+ * - Corrects both PTS and DTS
1946
+ *
1947
+ * @param packet - Packet to correct
1948
+ *
1949
+ * @param stream - Stream metadata
1950
+ *
1951
+ * @internal
1952
+ */
1953
+ ptsWrapAroundCorrection(packet, stream) {
1954
+ const state = this.getStreamState(packet.streamIndex);
1955
+ // Already corrected or no wrap bits configured
1956
+ if (state.wrapCorrectionDone || stream.ptsWrapBits >= 64) {
1957
+ return;
1958
+ }
1959
+ const startTime = this.formatContext.startTime;
1960
+ if (startTime === AV_NOPTS_VALUE) {
1961
+ return;
1962
+ }
1963
+ const ptsWrapBits = stream.ptsWrapBits;
1964
+ // Rescale start_time to packet's timebase
1965
+ // Note: packet.timeBase was set to stream.timeBase in packets() generator
1966
+ const stime = avRescaleQ(startTime, AV_TIME_BASE_Q, packet.timeBase);
1967
+ const stime2 = stime + (1n << BigInt(ptsWrapBits));
1968
+ state.wrapCorrectionDone = true;
1969
+ const wrapThreshold = stime + (1n << BigInt(ptsWrapBits - 1));
1970
+ // Check DTS for wrap-around
1971
+ if (stime2 > stime && packet.dts !== AV_NOPTS_VALUE && packet.dts > wrapThreshold) {
1972
+ packet.dts -= 1n << BigInt(ptsWrapBits);
1973
+ state.wrapCorrectionDone = false; // May wrap again
1974
+ }
1975
+ // Check PTS for wrap-around
1976
+ if (stime2 > stime && packet.pts !== AV_NOPTS_VALUE && packet.pts > wrapThreshold) {
1977
+ packet.pts -= 1n << BigInt(ptsWrapBits);
1978
+ state.wrapCorrectionDone = false; // May wrap again
1979
+ }
1980
+ }
1981
+ /**
1982
+ * DTS Prediction and Update.
1983
+ *
1984
+ * Based on FFmpeg's ist_dts_update().
1985
+ *
1986
+ * Predicts next expected DTS for frame ordering validation and discontinuity detection.
1987
+ * Uses codec-specific logic:
1988
+ * - Audio: Based on sample_rate and frame_size
1989
+ * - Video: Based on framerate or duration
1990
+ *
1991
+ * Handles:
1992
+ * - First timestamp initialization
1993
+ * - Codec-specific duration calculation
1994
+ * - DTS sequence tracking
1995
+ *
1996
+ * @param packet - Packet to process
1997
+ *
1998
+ * @param stream - Stream metadata
1999
+ *
2000
+ * @internal
2001
+ */
2002
+ dtsPredict(packet, stream) {
2003
+ const state = this.getStreamState(packet.streamIndex);
2004
+ // Call native implementation with native objects
2005
+ const newState = nativeDtsPredict(packet, stream, {
2006
+ sawFirstTs: state.sawFirstTs,
2007
+ dts: state.dts,
2008
+ nextDts: state.nextDts,
2009
+ firstDts: state.firstDts,
2010
+ });
2011
+ // Update state with results
2012
+ state.sawFirstTs = newState.sawFirstTs;
2013
+ state.dts = newState.dts;
2014
+ state.nextDts = newState.nextDts;
2015
+ state.firstDts = newState.firstDts;
2016
+ }
2017
+ /**
2018
+ * Timestamp Discontinuity Detection.
2019
+ *
2020
+ * Based on FFmpeg's ts_discontinuity_detect().
2021
+ *
2022
+ * Detects and corrects timestamp discontinuities in streams.
2023
+ * Handles two cases:
2024
+ * - Discontinuous formats (MPEG-TS): Apply offset correction
2025
+ * - Continuous formats (MP4): Mark timestamps as invalid
2026
+ *
2027
+ * Handles:
2028
+ * - Format-specific discontinuity handling (AVFMT_TS_DISCONT flag)
2029
+ * - PTS wrap-around detection for streams with limited timestamp bits
2030
+ * - Intra-stream discontinuity detection
2031
+ * - Inter-stream discontinuity detection
2032
+ * - Offset accumulation and application
2033
+ * - copyTs mode with selective correction
2034
+ *
2035
+ * @param packet - Packet to check for discontinuities
2036
+ *
2037
+ * @param stream - Stream metadata
2038
+ *
2039
+ * @internal
2040
+ */
2041
+ timestampDiscontinuityDetect(packet, stream) {
2042
+ const state = this.getStreamState(packet.streamIndex);
2043
+ const inputFormat = this.formatContext.iformat;
2044
+ // Check if format declares timestamp discontinuities
2045
+ const fmtIsDiscont = !!(inputFormat && inputFormat.flags & AVFMT_TS_DISCONT);
2046
+ // Disable correction when copyTs is enabled
2047
+ let disableDiscontinuityCorrection = this.options.copyTs;
2048
+ // Rescale packet DTS to AV_TIME_BASE for comparison
2049
+ const pktDts = avRescaleQRnd(packet.dts, packet.timeBase, AV_TIME_BASE_Q, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
2050
+ // PTS wrap-around detection
2051
+ // Only applies when copyTs is enabled and stream has limited timestamp bits
2052
+ if (this.options.copyTs && state.nextDts !== AV_NOPTS_VALUE && fmtIsDiscont && stream.ptsWrapBits < 60) {
2053
+ // Calculate wrapped DTS by adding 2^pts_wrap_bits to packet DTS
2054
+ const wrapDts = avRescaleQRnd(packet.dts + (1n << BigInt(stream.ptsWrapBits)), packet.timeBase, AV_TIME_BASE_Q, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
2055
+ // If wrapped DTS is closer to predicted nextDts, enable correction
2056
+ const wrapDelta = wrapDts > state.nextDts ? wrapDts - state.nextDts : state.nextDts - wrapDts;
2057
+ const normalDelta = pktDts > state.nextDts ? pktDts - state.nextDts : state.nextDts - pktDts;
2058
+ if (wrapDelta < normalDelta / 10n) {
2059
+ disableDiscontinuityCorrection = false;
2060
+ }
2061
+ }
2062
+ // Intra-stream discontinuity detection
2063
+ if (state.nextDts !== AV_NOPTS_VALUE && !disableDiscontinuityCorrection) {
2064
+ const delta = pktDts - state.nextDts;
2065
+ if (fmtIsDiscont) {
2066
+ // Discontinuous format (e.g., MPEG-TS) - apply offset correction
2067
+ const threshold = BigInt(this.options.dtsDeltaThreshold) * BigInt(AV_TIME_BASE);
2068
+ if (delta > threshold || delta < -threshold || pktDts + BigInt(AV_TIME_BASE) / 10n < state.dts) {
2069
+ this.tsOffsetDiscont -= delta;
2070
+ // Apply correction to packet
2071
+ const deltaInPktTb = avRescaleQ(delta, AV_TIME_BASE_Q, packet.timeBase);
2072
+ packet.dts -= deltaInPktTb;
2073
+ if (packet.pts !== AV_NOPTS_VALUE) {
2074
+ packet.pts -= deltaInPktTb;
2075
+ }
2076
+ }
2077
+ }
2078
+ else {
2079
+ // Continuous format (e.g., MP4) - mark invalid timestamps
2080
+ const threshold = BigInt(this.options.dtsErrorThreshold) * BigInt(AV_TIME_BASE);
2081
+ // Check DTS
2082
+ if (delta > threshold || delta < -threshold) {
2083
+ packet.dts = AV_NOPTS_VALUE;
2084
+ }
2085
+ // Check PTS
2086
+ if (packet.pts !== AV_NOPTS_VALUE) {
2087
+ const pktPts = avRescaleQ(packet.pts, packet.timeBase, AV_TIME_BASE_Q);
2088
+ const ptsDelta = pktPts - state.nextDts;
2089
+ if (ptsDelta > threshold || ptsDelta < -threshold) {
2090
+ packet.pts = AV_NOPTS_VALUE;
2091
+ }
2092
+ }
2093
+ }
2094
+ }
2095
+ else if (state.nextDts === AV_NOPTS_VALUE && !this.options.copyTs && fmtIsDiscont && this.lastTs !== AV_NOPTS_VALUE) {
2096
+ // Inter-stream discontinuity detection
2097
+ const delta = pktDts - this.lastTs;
2098
+ const threshold = BigInt(this.options.dtsDeltaThreshold) * BigInt(AV_TIME_BASE);
2099
+ if (delta > threshold || delta < -threshold) {
2100
+ this.tsOffsetDiscont -= delta;
2101
+ // Apply correction to packet
2102
+ const deltaInPktTb = avRescaleQ(delta, AV_TIME_BASE_Q, packet.timeBase);
2103
+ packet.dts -= deltaInPktTb;
2104
+ if (packet.pts !== AV_NOPTS_VALUE) {
2105
+ packet.pts -= deltaInPktTb;
2106
+ }
2107
+ }
2108
+ }
2109
+ // Update last timestamp
2110
+ this.lastTs = avRescaleQ(packet.dts, packet.timeBase, AV_TIME_BASE_Q);
2111
+ }
2112
+ /**
2113
+ * Timestamp Discontinuity Processing - main entry point.
2114
+ *
2115
+ * Based on FFmpeg's ts_discontinuity_process().
2116
+ *
2117
+ * Applies accumulated discontinuity offset and detects new discontinuities.
2118
+ * Must be called for every packet before other timestamp processing.
2119
+ *
2120
+ * Handles:
2121
+ * - Applying previously-detected offset to all streams
2122
+ * - Detecting new discontinuities for audio/video streams
2123
+ *
2124
+ * @param packet - Packet to process
2125
+ *
2126
+ * @param stream - Stream metadata
2127
+ *
2128
+ * @internal
2129
+ */
2130
+ timestampDiscontinuityProcess(packet, stream) {
2131
+ // Apply previously-detected discontinuity offset
2132
+ // This applies to ALL streams, not just audio/video
2133
+ const offset = avRescaleQ(this.tsOffsetDiscont, AV_TIME_BASE_Q, packet.timeBase);
2134
+ if (packet.dts !== AV_NOPTS_VALUE) {
2135
+ packet.dts += offset;
2136
+ }
2137
+ if (packet.pts !== AV_NOPTS_VALUE) {
2138
+ packet.pts += offset;
2139
+ }
2140
+ // Detect new timestamp discontinuities for audio/video
2141
+ const par = stream.codecpar;
2142
+ if ((par.codecType === AVMEDIA_TYPE_VIDEO || par.codecType === AVMEDIA_TYPE_AUDIO) && packet.dts !== AV_NOPTS_VALUE) {
2143
+ this.timestampDiscontinuityDetect(packet, stream);
2144
+ }
2145
+ }
2146
+ /**
2147
+ * Close demuxer and free resources.
2148
+ *
2149
+ * Releases format context and I/O context.
2150
+ * A read waiting for a stalled Readable or custom I/O source is abandoned,
2151
+ * so closing never waits for the source to deliver.
2152
+ * Safe to call multiple times.
2153
+ * Automatically called by Symbol.asyncDispose.
2154
+ *
2155
+ * Direct mapping to avformat_close_input().
2156
+ *
2157
+ * @example
2158
+ * ```typescript
2159
+ * const input = await Demuxer.open('video.mp4');
2160
+ * try {
2161
+ * // Use input
2162
+ * } finally {
2163
+ * await input.close();
2164
+ * }
2165
+ * ```
2166
+ *
2167
+ * @see {@link Symbol.asyncDispose} For automatic cleanup
2168
+ */
2169
+ async close() {
2170
+ if (this.isClosed) {
2171
+ return;
2172
+ }
2173
+ this.isClosed = true;
2174
+ // Clean up abort signal listener
2175
+ this.signalCleanup?.();
2176
+ this.signalCleanup = undefined;
2177
+ // Signal demux thread to stop FIRST
2178
+ this.demuxThreadActive = false;
2179
+ // Set EOF flag so generators know to exit
2180
+ this.demuxEof = true;
2181
+ this.wakeDemuxThread();
2182
+ // Wake up all waiting generators BEFORE closing format context
2183
+ // This ensures generators can exit cleanly even if readFrame() is blocking
2184
+ for (const resolvers of this.queueResolvers.values()) {
2185
+ for (const resolve of resolvers) {
2186
+ resolve();
2187
+ }
2188
+ }
2189
+ this.queueResolvers.clear();
2190
+ // Close FormatContext - this may interrupt blocking readFrame(). A custom
2191
+ // pb is detached inside, after the reader thread stopped: nulling it from
2192
+ // here would pull it out from under a running av_read_frame()
2193
+ await this.formatContext.closeInput(!!this.ioContext);
2194
+ // Wait for demux thread with timeout to avoid hanging on blocked reads
2195
+ if (this.demuxThread) {
2196
+ const threadPromise = this.demuxThread;
2197
+ let timer;
2198
+ const timeoutPromise = new Promise((resolve) => {
2199
+ timer = setTimeout(resolve, 2000);
2200
+ unrefTimer(timer);
2201
+ });
2202
+ await Promise.race([threadPromise, timeoutPromise]);
2203
+ clearTimeout(timer);
2204
+ this.demuxThread = null;
2205
+ }
2206
+ // Clean up packet queues
2207
+ for (const queue of this.packetQueues.values()) {
2208
+ for (const packet of queue) {
2209
+ packet.free();
2210
+ }
2211
+ queue.length = 0;
2212
+ }
2213
+ this.packetQueues.clear();
2214
+ this.packetQueueConsumers.clear();
2215
+ // NOW we can safely release the IOContext. Contexts we built ourselves are
2216
+ // callback-backed and go through freeContext(), which IOStream hooks to
2217
+ // detach its stream listeners. A caller-supplied context may instead own a
2218
+ // protocol handle from open2(); dispose picks avio_closep() for those, as
2219
+ // freeing them alone leaks the handle and keeps the file locked on Windows.
2220
+ if (this.ioContext) {
2221
+ if (this.externalIO) {
2222
+ await this.ioContext[Symbol.asyncDispose]();
2223
+ }
2224
+ else {
2225
+ this.ioContext.freeContext();
2226
+ }
2227
+ this.ioContext = undefined;
2228
+ }
2229
+ }
2230
+ /**
2231
+ * Close demuxer and free resources synchronously.
2232
+ * Synchronous version of close.
2233
+ *
2234
+ * Releases format context and I/O context.
2235
+ * A read waiting for a stalled Readable or custom I/O source is abandoned.
2236
+ * Safe to call multiple times.
2237
+ * Automatically called by Symbol.dispose.
2238
+ *
2239
+ * Direct mapping to avformat_close_input().
2240
+ *
2241
+ * @example
2242
+ * ```typescript
2243
+ * const input = Demuxer.openSync('video.mp4');
2244
+ * try {
2245
+ * // Use input
2246
+ * } finally {
2247
+ * input.closeSync();
2248
+ * }
2249
+ * ```
2250
+ *
2251
+ * @see {@link close} For async version
2252
+ */
2253
+ closeSync() {
2254
+ if (this.isClosed) {
2255
+ return;
2256
+ }
2257
+ this.isClosed = true;
2258
+ // Clean up abort signal listener
2259
+ this.signalCleanup?.();
2260
+ this.signalCleanup = undefined;
2261
+ // Stop the demux thread and wake generators parked on an empty queue: they
2262
+ // resume once this returns and end, as in close()
2263
+ this.demuxThreadActive = false;
2264
+ this.demuxEof = true;
2265
+ this.wakeDemuxThread();
2266
+ for (const resolvers of this.queueResolvers.values()) {
2267
+ for (const resolve of resolvers) {
2268
+ resolve();
2269
+ }
2270
+ }
2271
+ this.queueResolvers.clear();
2272
+ // Close FormatContext; a custom pb is detached inside so it is not closed with the input
2273
+ this.formatContext.closeInputSync(!!this.ioContext);
2274
+ for (const queue of this.packetQueues.values()) {
2275
+ for (const packet of queue) {
2276
+ packet.free();
2277
+ }
2278
+ queue.length = 0;
2279
+ }
2280
+ this.packetQueues.clear();
2281
+ this.packetQueueConsumers.clear();
2282
+ // NOW we can safely release the IOContext (see close() for why dispose)
2283
+ if (this.ioContext) {
2284
+ if (this.externalIO) {
2285
+ this.ioContext[Symbol.dispose]();
2286
+ }
2287
+ else {
2288
+ this.ioContext.freeContext();
2289
+ }
2290
+ this.ioContext = undefined;
2291
+ }
2292
+ }
2293
+ /**
2294
+ * Get underlying format context.
2295
+ *
2296
+ * Returns the internal format context for advanced operations.
2297
+ *
2298
+ * @returns Format context
2299
+ *
2300
+ * @internal
2301
+ */
2302
+ getFormatContext() {
2303
+ return this.formatContext;
2304
+ }
2305
+ /**
2306
+ * Dispose of demuxer.
2307
+ *
2308
+ * Implements AsyncDisposable interface for automatic cleanup.
2309
+ * Equivalent to calling close().
2310
+ *
2311
+ * @example
2312
+ * ```typescript
2313
+ * {
2314
+ * await using input = await Demuxer.open('video.mp4');
2315
+ * // Process media...
2316
+ * } // Automatically closed
2317
+ * ```
2318
+ *
2319
+ * @see {@link close} For manual cleanup
2320
+ */
2321
+ async [Symbol.asyncDispose]() {
2322
+ await this.close();
2323
+ }
2324
+ /**
2325
+ * Dispose of demuxer synchronously.
2326
+ *
2327
+ * Implements Disposable interface for automatic cleanup.
2328
+ * Equivalent to calling closeSync().
2329
+ *
2330
+ * @example
2331
+ * ```typescript
2332
+ * {
2333
+ * using input = Demuxer.openSync('video.mp4');
2334
+ * // Process media...
2335
+ * } // Automatically closed
2336
+ * ```
2337
+ *
2338
+ * @see {@link closeSync} For manual cleanup
2339
+ */
2340
+ [Symbol.dispose]() {
2341
+ this.closeSync();
2342
+ }
2343
+ }
2344
+ //# sourceMappingURL=demuxer.js.map