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,2367 @@
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 { AV_CHANNEL_ORDER_UNSPEC, AV_CODEC_FLAG_COPY_OPAQUE, AV_FRAME_FLAG_CORRUPT, AV_HWFRAME_TRANSFER_DIRECTION_FROM, AV_LOG_WARNING, AV_NOPTS_VALUE, AV_PIX_FMT_NONE, AV_ROUND_UP, AVERROR_DECODER_NOT_FOUND, AVERROR_EAGAIN, AVERROR_EOF, AVERROR_INVALIDDATA, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, EOF, INT_MAX, SWS_BILINEAR, } from '../constants/constants.js';
54
+ import { CodecContext } from '../lib/codec-context.js';
55
+ import { Codec } from '../lib/codec.js';
56
+ import { Dictionary } from '../lib/dictionary.js';
57
+ import { FFmpegError } from '../lib/error.js';
58
+ import { Frame } from '../lib/frame.js';
59
+ import { Log } from '../lib/log.js';
60
+ import { Packet } from '../lib/packet.js';
61
+ import { Rational } from '../lib/rational.js';
62
+ import { SoftwareResampleContext } from '../lib/software-resample-context.js';
63
+ import { SoftwareScaleContext } from '../lib/software-scale-context.js';
64
+ import { avChannelLayoutDefault, avGcd, avGuessFrameRate, avInvQ, avMulQ, avRescaleDelta, avRescaleQ, avRescaleQRnd } from '../lib/utilities.js';
65
+ import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
66
+ import { AsyncQueue } from './utilities/async-queue.js';
67
+ import { applyContextOptions } from './utilities/context-options.js';
68
+ import { Scheduler } from './utilities/scheduler.js';
69
+ /**
70
+ * Packets a new hardware decoder has after its first keyframe, on top of its threads' delay, to produce a frame.
71
+ *
72
+ * @internal
73
+ */
74
+ const HW_DECODE_PROBE = 16;
75
+ /**
76
+ * Packets after the second silent keyframe in a row before a running hardware decoder counts as stuck.
77
+ *
78
+ * @internal
79
+ */
80
+ const HW_DECODE_DELAY = 8;
81
+ /**
82
+ * High-level decoder for audio and video streams.
83
+ *
84
+ * Provides a simplified interface for decoding media streams from packets to frames.
85
+ * Handles codec initialization, hardware acceleration setup, and frame management.
86
+ * Supports both synchronous packet-by-packet decoding and async iteration over frames.
87
+ * Essential component in media processing pipelines for converting compressed data to raw frames.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * import { Demuxer, Decoder } from 'node-av/api';
92
+ *
93
+ * // Open media and create decoder
94
+ * await using input = await Demuxer.open('video.mp4');
95
+ * using decoder = await Decoder.create(input.video());
96
+ *
97
+ * // Decode frames
98
+ * for await (const frame of decoder.frames(input.packets())) {
99
+ * console.log(`Decoded frame: ${frame.width}x${frame.height}`);
100
+ * frame.free();
101
+ * }
102
+ * ```
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * import { HardwareContext } from 'node-av/api';
107
+ * import { AV_HWDEVICE_TYPE_CUDA } from 'node-av/constants';
108
+ *
109
+ * // Setup hardware acceleration
110
+ * const hw = HardwareContext.create(AV_HWDEVICE_TYPE_CUDA);
111
+ * using decoder = await Decoder.create(stream, { hardware: hw });
112
+ *
113
+ * // Frames will be decoded on GPU
114
+ * for await (const frame of decoder.frames(packets)) {
115
+ * // frame.hwFramesCtx contains GPU memory reference
116
+ * }
117
+ * ```
118
+ *
119
+ * @see {@link Encoder} For encoding frames to packets
120
+ * @see {@link Demuxer} For reading media files
121
+ * @see {@link HardwareContext} For GPU acceleration
122
+ */
123
+ export class Decoder {
124
+ codecContext;
125
+ codec;
126
+ frame;
127
+ stream;
128
+ streamIndex;
129
+ streamFramerate;
130
+ guessedFramerate;
131
+ initialized = true;
132
+ isClosed = false;
133
+ options;
134
+ _decodeErrors = 0;
135
+ // Software fallback of a hardware decoder (hardwareFallback)
136
+ hardwareWatch;
137
+ replayedFrames = [];
138
+ // Frame tracking for PTS/duration estimation
139
+ lastFramePts = AV_NOPTS_VALUE;
140
+ lastFrameDurationEst = 0n;
141
+ lastFrameTb;
142
+ // Audio-specific frame tracking
143
+ lastFrameSampleRate = 0;
144
+ lastFilterInRescaleDelta = AV_NOPTS_VALUE;
145
+ // Audio output resampling
146
+ audioResampler;
147
+ resampledFrame;
148
+ resampleTarget;
149
+ resampleInputLayout;
150
+ audioResamplerSetup = false;
151
+ audioResamplerDrained = false;
152
+ // Video output rescaling
153
+ videoScaler;
154
+ scaledFrame;
155
+ hwTransferFormat;
156
+ // Worker pattern for push-based processing
157
+ inputQueue;
158
+ outputQueue;
159
+ workerPromise = null;
160
+ nextComponent = null;
161
+ pipeToPromise = null;
162
+ signal;
163
+ /**
164
+ * @param codecContext - Configured codec context
165
+ *
166
+ * @param codec - Codec being used
167
+ *
168
+ * @param stream - Media stream being decoded
169
+ *
170
+ * @param options - Decoder options
171
+ *
172
+ * Use {@link create} factory method
173
+ *
174
+ * @internal
175
+ */
176
+ constructor(codecContext, codec, stream, options = {}) {
177
+ this.codecContext = codecContext;
178
+ this.codec = codec;
179
+ this.stream = stream;
180
+ this.streamIndex = stream.index;
181
+ const avgFramerate = stream.avgFrameRate;
182
+ this.streamFramerate = avgFramerate.num > 0 && avgFramerate.den > 0 ? avgFramerate : stream.rFrameRate;
183
+ // Taken now: the stream belongs to the demuxer and reads 0/1 once it closes
184
+ this.guessedFramerate = avGuessFrameRate(stream);
185
+ this.options = options;
186
+ this.frame = new Frame();
187
+ this.frame.alloc();
188
+ this.lastFrameTb = new Rational(0, 1);
189
+ this.inputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE, (p) => p.free());
190
+ this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE, (f) => f.free());
191
+ if (options.hardwareFallback && this.isHardware() && codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
192
+ this.hardwareWatch = { started: false, proven: false, framesSincePacket: 0, silent: 0, silentKeyframes: 0, sinceSecondKeyframe: 0, held: [] };
193
+ }
194
+ }
195
+ static async create(stream, optionsOrCodec, maybeOptions) {
196
+ // Parse arguments
197
+ let options = {};
198
+ let explicitCodec;
199
+ if (optionsOrCodec !== undefined) {
200
+ // Check if first argument is a codec or options
201
+ if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
202
+ typeof optionsOrCodec === 'number' || // AVCodecID
203
+ optionsOrCodec instanceof Codec // Codec instance
204
+ ) {
205
+ // First argument is a codec
206
+ explicitCodec = optionsOrCodec;
207
+ options = { ...maybeOptions };
208
+ }
209
+ else {
210
+ // First argument is options (copied: create() fills in defaults)
211
+ options = { ...optionsOrCodec };
212
+ }
213
+ }
214
+ let codec = null;
215
+ // If explicit codec provided, use it
216
+ if (explicitCodec !== undefined) {
217
+ if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
218
+ // Already a Codec instance
219
+ codec = explicitCodec;
220
+ }
221
+ else if (typeof explicitCodec === 'string') {
222
+ // FFDecoderCodec string
223
+ codec = Codec.findDecoderByName(explicitCodec);
224
+ if (!codec) {
225
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
226
+ }
227
+ }
228
+ else {
229
+ // AVCodecID number
230
+ codec = Codec.findDecoder(explicitCodec);
231
+ if (!codec) {
232
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
233
+ }
234
+ }
235
+ }
236
+ else {
237
+ // No explicit codec - use auto-detection logic
238
+ // If hardware acceleration requested, try to find hardware decoder first
239
+ if (options.hardware) {
240
+ codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
241
+ if (!codec) {
242
+ // No hardware decoder available, fall back to software
243
+ options.hardware = undefined;
244
+ }
245
+ }
246
+ // If no hardware decoder or no hardware requested, use software decoder
247
+ if (!codec) {
248
+ codec = Codec.findDecoder(stream.codecpar.codecId);
249
+ if (!codec) {
250
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
251
+ }
252
+ }
253
+ }
254
+ // Allocate and configure codec context
255
+ const codecContext = new CodecContext();
256
+ codecContext.allocContext3(codec);
257
+ // Copy codec parameters to context
258
+ const ret = codecContext.parametersToContext(stream.codecpar);
259
+ if (ret < 0) {
260
+ codecContext.freeContext();
261
+ FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
262
+ }
263
+ // Set packet time base
264
+ codecContext.pktTimebase = stream.timeBase;
265
+ // Check if this decoder supports hardware acceleration
266
+ // Only apply hardware acceleration if the decoder supports it
267
+ // Silently ignore hardware for software decoders
268
+ const isHWDecoder = codec.isHardwareAcceleratedDecoder();
269
+ if (isHWDecoder && options.hardware) {
270
+ codecContext.hwDeviceCtx = options.hardware.deviceContext;
271
+ // Set hardware pixel format
272
+ codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
273
+ // Set extra_hw_frames if specified
274
+ if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
275
+ codecContext.extraHWFrames = options.extraHWFrames;
276
+ }
277
+ }
278
+ else {
279
+ options.hardware = undefined;
280
+ }
281
+ options.exitOnError = options.exitOnError ?? true;
282
+ // Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
283
+ codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
284
+ // Thread parameters need to be set before open2
285
+ codecContext.threadCount = options.threadCount ?? 0;
286
+ if (options.threadType !== undefined) {
287
+ codecContext.threadType = options.threadType;
288
+ }
289
+ const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
290
+ applyContextOptions(codecContext, options.context);
291
+ options.configure?.(codecContext);
292
+ // Open codec
293
+ const openRet = await codecContext.open2(codec, opts);
294
+ if (openRet < 0) {
295
+ codecContext.freeContext();
296
+ FFmpegError.throwIfError(openRet, 'Failed to open codec');
297
+ }
298
+ // Adjust extra_hw_frames for queuing
299
+ // This is done AFTER open2 because the decoder validates extra_hw_frames during open
300
+ if (isHWDecoder && options.hardware) {
301
+ const currentExtraFrames = codecContext.extraHWFrames;
302
+ if (currentExtraFrames >= 0) {
303
+ codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
304
+ }
305
+ else {
306
+ codecContext.extraHWFrames = 1;
307
+ }
308
+ }
309
+ const decoder = new Decoder(codecContext, codec, stream, options);
310
+ if (options.signal) {
311
+ options.signal.throwIfAborted();
312
+ decoder.signal = options.signal;
313
+ }
314
+ return decoder;
315
+ }
316
+ static createSync(stream, optionsOrCodec, maybeOptions) {
317
+ // Parse arguments
318
+ let options = {};
319
+ let explicitCodec;
320
+ if (optionsOrCodec !== undefined) {
321
+ // Check if first argument is a codec or options
322
+ if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
323
+ typeof optionsOrCodec === 'number' || // AVCodecID
324
+ optionsOrCodec instanceof Codec // Codec instance
325
+ ) {
326
+ // First argument is a codec
327
+ explicitCodec = optionsOrCodec;
328
+ options = { ...maybeOptions };
329
+ }
330
+ else {
331
+ // First argument is options (copied: create() fills in defaults)
332
+ options = { ...optionsOrCodec };
333
+ }
334
+ }
335
+ let codec = null;
336
+ // If explicit codec provided, use it
337
+ if (explicitCodec !== undefined) {
338
+ if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
339
+ // Already a Codec instance
340
+ codec = explicitCodec;
341
+ }
342
+ else if (typeof explicitCodec === 'string') {
343
+ // FFDecoderCodec string
344
+ codec = Codec.findDecoderByName(explicitCodec);
345
+ if (!codec) {
346
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
347
+ }
348
+ }
349
+ else {
350
+ // AVCodecID number
351
+ codec = Codec.findDecoder(explicitCodec);
352
+ if (!codec) {
353
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
354
+ }
355
+ }
356
+ }
357
+ else {
358
+ // No explicit codec - use auto-detection logic
359
+ // If hardware acceleration requested, try to find hardware decoder first
360
+ if (options.hardware) {
361
+ codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
362
+ if (!codec) {
363
+ // No hardware decoder available, fall back to software
364
+ options.hardware = undefined;
365
+ }
366
+ }
367
+ // If no hardware decoder or no hardware requested, use software decoder
368
+ if (!codec) {
369
+ codec = Codec.findDecoder(stream.codecpar.codecId);
370
+ if (!codec) {
371
+ throw new FFmpegError(AVERROR_DECODER_NOT_FOUND);
372
+ }
373
+ }
374
+ }
375
+ // Allocate and configure codec context
376
+ const codecContext = new CodecContext();
377
+ codecContext.allocContext3(codec);
378
+ // Copy codec parameters to context
379
+ const ret = codecContext.parametersToContext(stream.codecpar);
380
+ if (ret < 0) {
381
+ codecContext.freeContext();
382
+ FFmpegError.throwIfError(ret, 'Failed to copy codec parameters');
383
+ }
384
+ // Set packet time base
385
+ codecContext.pktTimebase = stream.timeBase;
386
+ // Check if this decoder supports hardware acceleration
387
+ // Only apply hardware acceleration if the decoder supports it
388
+ // Silently ignore hardware for software decoders
389
+ const isHWDecoder = codec.isHardwareAcceleratedDecoder();
390
+ if (isHWDecoder && options.hardware) {
391
+ codecContext.hwDeviceCtx = options.hardware.deviceContext;
392
+ // Set hardware pixel format and get_format callback
393
+ codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
394
+ // Set extra_hw_frames if specified
395
+ if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
396
+ codecContext.extraHWFrames = options.extraHWFrames;
397
+ }
398
+ }
399
+ else {
400
+ options.hardware = undefined;
401
+ }
402
+ options.exitOnError = options.exitOnError ?? true;
403
+ // Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
404
+ codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
405
+ // Thread parameters need to be set before open2
406
+ codecContext.threadCount = options.threadCount ?? 0;
407
+ if (options.threadType !== undefined) {
408
+ codecContext.threadType = options.threadType;
409
+ }
410
+ const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
411
+ // User-supplied context fields, then the imperative hook: final say over the
412
+ // codec context before it is opened.
413
+ applyContextOptions(codecContext, options.context);
414
+ options.configure?.(codecContext);
415
+ // Open codec synchronously
416
+ const openRet = codecContext.open2Sync(codec, opts);
417
+ if (openRet < 0) {
418
+ codecContext.freeContext();
419
+ FFmpegError.throwIfError(openRet, 'Failed to open codec');
420
+ }
421
+ // Adjust extra_hw_frames for queuing
422
+ // This is done AFTER open2 because the decoder validates extra_hw_frames during open
423
+ if (isHWDecoder && options.hardware) {
424
+ const currentExtraFrames = codecContext.extraHWFrames;
425
+ if (currentExtraFrames >= 0) {
426
+ codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
427
+ }
428
+ else {
429
+ codecContext.extraHWFrames = 1;
430
+ }
431
+ }
432
+ const decoder = new Decoder(codecContext, codec, stream, options);
433
+ if (options.signal) {
434
+ options.signal.throwIfAborted();
435
+ decoder.signal = options.signal;
436
+ }
437
+ return decoder;
438
+ }
439
+ /**
440
+ * Check if decoder is open.
441
+ *
442
+ * @returns true if decoder is open and ready
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * if (decoder.isDecoderOpen) {
447
+ * const frame = await decoder.decode(packet);
448
+ * }
449
+ * ```
450
+ */
451
+ get isDecoderOpen() {
452
+ return !this.isClosed;
453
+ }
454
+ /**
455
+ * Check if decoder has been initialized.
456
+ *
457
+ * Returns true if decoder is initialized (true by default for decoders).
458
+ * Decoders are pre-initialized from stream parameters.
459
+ *
460
+ * @returns true if decoder has been initialized
461
+ *
462
+ * @example
463
+ * ```typescript
464
+ * if (decoder.isDecoderInitialized) {
465
+ * console.log('Decoder is ready to process frames');
466
+ * }
467
+ * ```
468
+ */
469
+ get isDecoderInitialized() {
470
+ return this.initialized;
471
+ }
472
+ /**
473
+ * Number of decode errors so far.
474
+ *
475
+ * Counts packets the decoder rejected, failed frame receives and frames that
476
+ * could not be transferred from the GPU, rescaled or cropped. With
477
+ * `exitOnError: false` these are skipped instead of thrown, so this is the way
478
+ * to notice them (e.g. a hardware decoder that keeps failing).
479
+ *
480
+ * Like FFmpeg CLI's decode error count. Corrupt frames are not counted; they are
481
+ * returned with `decodeErrorFlags` or `AV_FRAME_FLAG_CORRUPT` set.
482
+ *
483
+ * @returns Decode error count
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * const decoder = await Decoder.create(stream, { exitOnError: false });
488
+ * // ...
489
+ * if (decoder.decodeErrors > lastErrors) {
490
+ * console.warn(`${decoder.decodeErrors - lastErrors} decode errors`);
491
+ * }
492
+ * ```
493
+ */
494
+ get decodeErrors() {
495
+ return this._decodeErrors;
496
+ }
497
+ /**
498
+ * Check if decoder uses hardware acceleration.
499
+ *
500
+ * @returns true if hardware-accelerated
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * if (decoder.isHardware()) {
505
+ * console.log('Using GPU acceleration');
506
+ * }
507
+ * ```
508
+ *
509
+ * @see {@link HardwareContext} For hardware setup
510
+ */
511
+ isHardware() {
512
+ return !!this.options.hardware && this.codec.isHardwareAcceleratedDecoder();
513
+ }
514
+ /**
515
+ * Check if decoder is ready for processing.
516
+ *
517
+ * @returns true if initialized and ready
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * if (decoder.isReady()) {
522
+ * const frame = await decoder.decode(packet);
523
+ * }
524
+ * ```
525
+ */
526
+ isReady() {
527
+ return this.initialized && !this.isClosed;
528
+ }
529
+ /**
530
+ * Send a packet to the decoder.
531
+ *
532
+ * Sends a compressed packet to the decoder for decoding.
533
+ * Does not return decoded frames - use {@link receive} to retrieve frames.
534
+ * A single packet can produce zero, one, or multiple frames depending on codec buffering.
535
+ * Automatically manages decoder state and error recovery.
536
+ *
537
+ * **Important**: This method only SENDS the packet to the decoder.
538
+ * You must call {@link receive} separately (potentially multiple times) to get decoded frames.
539
+ *
540
+ * Direct mapping to avcodec_send_packet().
541
+ *
542
+ * @param packet - Compressed packet to send to decoder, or null to flush
543
+ *
544
+ * @throws {FFmpegError} If sending packet fails
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * // Send packet and receive frames
549
+ * await decoder.decode(packet);
550
+ *
551
+ * // Receive all available frames
552
+ * while (true) {
553
+ * const frame = await decoder.receive();
554
+ * if (!frame) break;
555
+ * console.log(`Decoded frame with PTS: ${frame.pts}`);
556
+ * frame.free();
557
+ * }
558
+ * ```
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * for await (const packet of input.packets()) {
563
+ * // packet is null at end of stream - automatically flushes decoder
564
+ * await decoder.decode(packet);
565
+ *
566
+ * // Receive available frames
567
+ * let frame;
568
+ * while ((frame = await decoder.receive())) {
569
+ * await processFrame(frame);
570
+ * frame.free();
571
+ * }
572
+ * }
573
+ * ```
574
+ *
575
+ * @see {@link receive} For receiving decoded frames
576
+ * @see {@link decodeAll} For combined send+receive operation
577
+ * @see {@link frames} For automatic packet iteration
578
+ * @see {@link flush} For end-of-stream handling
579
+ * @see {@link decodeSync} For synchronous version
580
+ */
581
+ async decode(packet) {
582
+ this.signal?.throwIfAborted();
583
+ if (this.isClosed) {
584
+ return;
585
+ }
586
+ // Null packet = flush decoder
587
+ if (packet === null) {
588
+ await this.flush();
589
+ return;
590
+ }
591
+ if (packet.streamIndex !== this.streamIndex) {
592
+ return;
593
+ }
594
+ // Skip 0-sized packets
595
+ if (packet.size === 0) {
596
+ return;
597
+ }
598
+ // A hardware decoder that stayed silent is replaced before this packet goes in
599
+ const stuck = this.hardwareWatch ? this.watchHardware(packet) : null;
600
+ if (stuck) {
601
+ await this.fallBackToSoftware(stuck);
602
+ }
603
+ // Send packet to decoder
604
+ const sendRet = await this.codecContext.sendPacket(packet);
605
+ // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
606
+ // We read all decoded frames with receive() until done, so decoder should never be full
607
+ if (sendRet === AVERROR_EAGAIN) {
608
+ FFmpegError.throwIfError(sendRet, 'Decoder returned EAGAIN on send - this is a decoder bug');
609
+ }
610
+ // Handle send errors
611
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
612
+ this._decodeErrors++;
613
+ if (this.options.exitOnError) {
614
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
615
+ }
616
+ // exitOnError=false: Continue to receive loop to drain any buffered frames
617
+ }
618
+ }
619
+ /**
620
+ * Send a packet to the decoder synchronously.
621
+ * Synchronous version of decode.
622
+ *
623
+ * Sends a compressed packet to the decoder for decoding.
624
+ * Does not return decoded frames - use {@link receiveSync} to retrieve frames.
625
+ * A single packet can produce zero, one, or multiple frames depending on codec buffering.
626
+ * Automatically manages decoder state and error recovery.
627
+ *
628
+ * **Important**: This method only SENDS the packet to the decoder.
629
+ * You must call {@link receiveSync} separately (potentially multiple times) to get decoded frames.
630
+ *
631
+ * Direct mapping to avcodec_send_packet().
632
+ *
633
+ * @param packet - Compressed packet to send to decoder, or null to flush
634
+ *
635
+ * @throws {FFmpegError} If sending packet fails
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * // Send packet and receive frames
640
+ * decoder.decodeSync(packet);
641
+ *
642
+ * // Receive all available frames
643
+ * while (true) {
644
+ * const frame = decoder.receiveSync();
645
+ * if (!frame) break;
646
+ * console.log(`Decoded frame with PTS: ${frame.pts}`);
647
+ * frame.free();
648
+ * }
649
+ * ```
650
+ *
651
+ * @example
652
+ * ```typescript
653
+ * for (const packet of input.packetsSync()) {
654
+ * // packet is null at end of stream - automatically flushes decoder
655
+ * decoder.decodeSync(packet);
656
+ *
657
+ * // Receive available frames
658
+ * let frame;
659
+ * while ((frame = decoder.receiveSync())) {
660
+ * processFrame(frame);
661
+ * frame.free();
662
+ * }
663
+ * }
664
+ * ```
665
+ *
666
+ * @see {@link receiveSync} For receiving decoded frames
667
+ * @see {@link decodeAllSync} For combined send+receive operation
668
+ * @see {@link framesSync} For automatic packet iteration
669
+ * @see {@link flushSync} For end-of-stream handling
670
+ * @see {@link decode} For async version
671
+ */
672
+ decodeSync(packet) {
673
+ if (this.isClosed) {
674
+ return;
675
+ }
676
+ // Null packet = flush decoder
677
+ if (packet === null) {
678
+ this.flushSync();
679
+ return;
680
+ }
681
+ if (packet.streamIndex !== this.streamIndex) {
682
+ return;
683
+ }
684
+ // Skip 0-sized packets
685
+ if (packet.size === 0) {
686
+ return;
687
+ }
688
+ const stuck = this.hardwareWatch ? this.watchHardware(packet) : null;
689
+ if (stuck) {
690
+ this.fallBackToSoftwareSync(stuck);
691
+ }
692
+ // Send packet to decoder
693
+ const sendRet = this.codecContext.sendPacketSync(packet);
694
+ // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
695
+ // We read all decoded frames with receive() until done, so decoder should never be full
696
+ if (sendRet === AVERROR_EAGAIN) {
697
+ FFmpegError.throwIfError(AVERROR_EAGAIN, 'Decoder returned EAGAIN on send - this is a decoder bug');
698
+ }
699
+ // Handle send errors
700
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
701
+ this._decodeErrors++;
702
+ if (this.options.exitOnError) {
703
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
704
+ }
705
+ // exitOnError=false: Continue to receive loop to drain any buffered frames
706
+ }
707
+ }
708
+ /**
709
+ * Decode a packet to frames.
710
+ *
711
+ * Sends a packet to the decoder and receives all available decoded frames.
712
+ * Returns array of frames - may be empty if decoder needs more data.
713
+ * One packet can produce zero, one, or multiple frames depending on codec.
714
+ * Automatically manages decoder state and error recovery.
715
+ *
716
+ * Direct mapping to avcodec_send_packet() and avcodec_receive_frame().
717
+ *
718
+ * @param packet - Compressed packet to decode
719
+ *
720
+ * @returns Array of decoded frames (empty if more data needed or decoder is closed)
721
+ *
722
+ * @throws {FFmpegError} If decoding fails
723
+ *
724
+ * @example
725
+ * ```typescript
726
+ * const frames = await decoder.decodeAll(packet);
727
+ * for (const frame of frames) {
728
+ * console.log(`Decoded frame with PTS: ${frame.pts}`);
729
+ * frame.free();
730
+ * }
731
+ * ```
732
+ *
733
+ * @example
734
+ * ```typescript
735
+ * for await (const packet of input.packets()) {
736
+ * const frames = await decoder.decodeAll(packet);
737
+ * for (const frame of frames) {
738
+ * await processFrame(frame);
739
+ * frame.free();
740
+ * }
741
+ * packet.free();
742
+ * }
743
+ * ```
744
+ *
745
+ * @see {@link decode} For single packet decoding
746
+ * @see {@link frames} For automatic packet iteration
747
+ * @see {@link flush} For end-of-stream handling
748
+ * @see {@link decodeAllSync} For synchronous version
749
+ */
750
+ async decodeAll(packet) {
751
+ this.signal?.throwIfAborted();
752
+ await this.decode(packet);
753
+ // Receive all available frames
754
+ const frames = [];
755
+ while (true) {
756
+ const remaining = await this.receive();
757
+ if (!remaining)
758
+ break;
759
+ frames.push(remaining);
760
+ }
761
+ return frames;
762
+ }
763
+ /**
764
+ * Decode a packet to frames synchronously.
765
+ * Synchronous version of decodeAll.
766
+ *
767
+ * Sends packet to decoder and receives all available decoded frames.
768
+ * Returns array of frames - may be empty if decoder needs more data.
769
+ * One packet can produce zero, one, or multiple frames depending on codec.
770
+ *
771
+ * @param packet - Compressed packet to decode
772
+ *
773
+ * @returns Array of decoded frames (empty if more data needed or decoder is closed)
774
+ *
775
+ * @throws {FFmpegError} If decoding fails
776
+ *
777
+ * @example
778
+ * ```typescript
779
+ * const frames = decoder.decodeAllSync(packet);
780
+ * for (const frame of frames) {
781
+ * console.log(`Decoded: ${frame.width}x${frame.height}`);
782
+ * frame.free();
783
+ * }
784
+ *
785
+ * @example
786
+ * ```typescript
787
+ * for (const packet of input.packetsSync()) {
788
+ * const frames = await decoder.decodeAllSync(packet);
789
+ * for (const frame of frames) {
790
+ * processFrame(frame);
791
+ * frame.free();
792
+ * }
793
+ * packet.free();
794
+ * }
795
+ * ```
796
+ *
797
+ * @see {@link decodeSync} For single packet decoding
798
+ * @see {@link framesSync} For automatic packet iteration
799
+ * @see {@link flushSync} For end-of-stream handling
800
+ * @see {@link decodeAll} For async version
801
+ */
802
+ decodeAllSync(packet) {
803
+ this.decodeSync(packet);
804
+ // Receive all available frames
805
+ const frames = [];
806
+ while (true) {
807
+ const remaining = this.receiveSync();
808
+ if (!remaining)
809
+ break;
810
+ frames.push(remaining);
811
+ }
812
+ return frames;
813
+ }
814
+ /**
815
+ * Decode packet stream to frame stream.
816
+ *
817
+ * High-level async generator for complete decoding pipeline.
818
+ * Decoder is only flushed when EOF (null) signal is explicitly received.
819
+ * Primary interface for stream-based decoding.
820
+ *
821
+ * **EOF Handling:**
822
+ * - Send null to flush decoder and get remaining buffered frames
823
+ * - Generator yields null after flushing when null is received
824
+ * - No automatic flushing - decoder stays open until EOF or close()
825
+ *
826
+ * @param packets - Async iterable of packets, single packet, or null to flush
827
+ *
828
+ * @yields {Frame | null} Decoded frames, followed by null when explicitly flushed
829
+ *
830
+ * @throws {Error} If decoder is closed
831
+ *
832
+ * @throws {FFmpegError} If decoding fails
833
+ *
834
+ * @example
835
+ * ```typescript
836
+ * // Stream of packets with automatic EOF propagation
837
+ * await using input = await Demuxer.open('video.mp4');
838
+ * using decoder = await Decoder.create(input.video());
839
+ *
840
+ * for await (const frame of decoder.frames(input.packets())) {
841
+ * if (frame === null) {
842
+ * console.log('Decoding complete');
843
+ * break;
844
+ * }
845
+ * console.log(`Frame: ${frame.width}x${frame.height}`);
846
+ * frame.free();
847
+ * }
848
+ * ```
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * // Single packet (no automatic flush)
853
+ * for await (const frame of decoder.frames(singlePacket)) {
854
+ * await encoder.encode(frame);
855
+ * frame.free();
856
+ * }
857
+ * // Decoder still has buffered frames - send null to flush
858
+ * for await (const frame of decoder.frames(null)) {
859
+ * if (frame === null) break;
860
+ * await encoder.encode(frame);
861
+ * frame.free();
862
+ * }
863
+ * ```
864
+ *
865
+ * @example
866
+ * ```typescript
867
+ * // Explicit flush with EOF
868
+ * for await (const frame of decoder.frames(null)) {
869
+ * if (frame === null) {
870
+ * console.log('All buffered frames flushed');
871
+ * break;
872
+ * }
873
+ * console.log('Buffered frame:', frame.pts);
874
+ * frame.free();
875
+ * }
876
+ * ```
877
+ *
878
+ * @see {@link decode} For single packet decoding
879
+ * @see {@link Demuxer.packets} For packet source
880
+ * @see {@link framesSync} For sync version
881
+ */
882
+ async *frames(packets) {
883
+ const self = this;
884
+ const processPacket = async function* (packet) {
885
+ await self.decode(packet);
886
+ while (true) {
887
+ const frame = await self.receive();
888
+ if (!frame)
889
+ break;
890
+ yield frame;
891
+ }
892
+ }.bind(this);
893
+ const finalize = async function* () {
894
+ for await (const remaining of self.flushFrames()) {
895
+ yield remaining;
896
+ }
897
+ yield null;
898
+ }.bind(this);
899
+ if (packets === null) {
900
+ yield* finalize();
901
+ return;
902
+ }
903
+ if (packets instanceof Packet) {
904
+ yield* processPacket(packets);
905
+ return;
906
+ }
907
+ for await (const packet_1 of packets) {
908
+ const env_1 = { stack: [], error: void 0, hasError: false };
909
+ try {
910
+ const packet = __addDisposableResource(env_1, packet_1, false);
911
+ this.signal?.throwIfAborted();
912
+ if (packet === null) {
913
+ yield* finalize();
914
+ return;
915
+ }
916
+ yield* processPacket(packet);
917
+ }
918
+ catch (e_1) {
919
+ env_1.error = e_1;
920
+ env_1.hasError = true;
921
+ }
922
+ finally {
923
+ __disposeResources(env_1);
924
+ }
925
+ }
926
+ }
927
+ /**
928
+ * Decode packet stream to frame stream synchronously.
929
+ * Synchronous version of frames.
930
+ *
931
+ * High-level async generator for complete decoding pipeline.
932
+ * Decoder is only flushed when EOF (null) signal is explicitly received.
933
+ * Primary interface for stream-based decoding.
934
+ *
935
+ * **EOF Handling:**
936
+ * - Send null to flush decoder and get remaining buffered frames
937
+ * - Generator yields null after flushing when null is received
938
+ * - No automatic flushing - decoder stays open until EOF or close()
939
+ *
940
+ * @param packets - Iterable of packets, single packet, or null to flush
941
+ *
942
+ * @yields {Frame | null} Decoded frames, followed by null when explicitly flushed
943
+ *
944
+ * @throws {Error} If decoder is closed
945
+ *
946
+ * @throws {FFmpegError} If decoding fails
947
+ *
948
+ * @example
949
+ * ```typescript
950
+ * // Stream of packets with automatic EOF propagation
951
+ * await using input = await Demuxer.open('video.mp4');
952
+ * using decoder = await Decoder.create(input.video());
953
+ *
954
+ * for (const frame of decoder.framesSync(input.packetsSync())) {
955
+ * if (frame === null) {
956
+ * console.log('Decoding complete');
957
+ * break;
958
+ * }
959
+ * console.log(`Frame: ${frame.width}x${frame.height}`);
960
+ * frame.free();
961
+ * }
962
+ * ```
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * // Single packet (no automatic flush)
967
+ * for (const frame of decoder.framesSync(singlePacket)) {
968
+ * encoder.encodeSync(frame);
969
+ * frame.free();
970
+ * }
971
+ * // Decoder still has buffered frames - send null to flush
972
+ * for (const frame of decoder.framesSync(null)) {
973
+ * if (frame === null) break;
974
+ * encoder.encodeSync(frame);
975
+ * frame.free();
976
+ * }
977
+ * ```
978
+ *
979
+ * @example
980
+ * ```typescript
981
+ * // Explicit flush with EOF
982
+ * for (const frame of decoder.framesSync(null)) {
983
+ * if (frame === null) {
984
+ * console.log('All buffered frames flushed');
985
+ * break;
986
+ * }
987
+ * console.log('Buffered frame:', frame.pts);
988
+ * frame.free();
989
+ * }
990
+ * ```
991
+ */
992
+ *framesSync(packets) {
993
+ const self = this;
994
+ const processPacket = function* (packet) {
995
+ self.decodeSync(packet);
996
+ while (true) {
997
+ const frame = self.receiveSync();
998
+ if (!frame)
999
+ break;
1000
+ yield frame;
1001
+ }
1002
+ }.bind(this);
1003
+ const finalize = function* () {
1004
+ for (const remaining of self.flushFramesSync()) {
1005
+ yield remaining;
1006
+ }
1007
+ yield null;
1008
+ }.bind(this);
1009
+ if (packets === null) {
1010
+ yield* finalize();
1011
+ return;
1012
+ }
1013
+ if (packets instanceof Packet) {
1014
+ yield* processPacket(packets);
1015
+ return;
1016
+ }
1017
+ for (const packet_2 of packets) {
1018
+ const env_2 = { stack: [], error: void 0, hasError: false };
1019
+ try {
1020
+ const packet = __addDisposableResource(env_2, packet_2, false);
1021
+ if (packet === null) {
1022
+ yield* finalize();
1023
+ return;
1024
+ }
1025
+ yield* processPacket(packet);
1026
+ }
1027
+ catch (e_2) {
1028
+ env_2.error = e_2;
1029
+ env_2.hasError = true;
1030
+ }
1031
+ finally {
1032
+ __disposeResources(env_2);
1033
+ }
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Flush decoder and signal end-of-stream.
1038
+ *
1039
+ * Sends null packet to decoder to signal end-of-stream.
1040
+ * Does nothing if decoder is closed.
1041
+ * Must use receive() or flushFrames() to get remaining buffered frames.
1042
+ *
1043
+ * Direct mapping to avcodec_send_packet(NULL).
1044
+ *
1045
+ * @throws {FFmpegError} If flush fails
1046
+ *
1047
+ * @example
1048
+ * ```typescript
1049
+ * // Signal end of stream
1050
+ * await decoder.flush();
1051
+ *
1052
+ * // Then get remaining frames
1053
+ * let frame;
1054
+ * while ((frame = await decoder.receive()) !== null) {
1055
+ * console.log('Got buffered frame');
1056
+ * frame.free();
1057
+ * }
1058
+ * ```
1059
+ *
1060
+ * @see {@link flushFrames} For convenient async iteration
1061
+ * @see {@link receive} For getting buffered frames
1062
+ * @see {@link flushSync} For synchronous version
1063
+ */
1064
+ async flush() {
1065
+ this.signal?.throwIfAborted();
1066
+ if (this.isClosed) {
1067
+ return;
1068
+ }
1069
+ // Send flush packet (null)
1070
+ const ret = await this.codecContext.sendPacket(null);
1071
+ if (ret < 0 && ret !== AVERROR_EOF) {
1072
+ if (ret !== AVERROR_EAGAIN) {
1073
+ FFmpegError.throwIfError(ret, 'Failed to flush decoder');
1074
+ }
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Flush decoder and signal end-of-stream synchronously.
1079
+ * Synchronous version of flush.
1080
+ *
1081
+ * Send null packet to signal end of input stream.
1082
+ * Decoder may still have buffered frames.
1083
+ * Call receiveSync() repeatedly to get remaining frames.
1084
+ *
1085
+ * @throws {FFmpegError} If flush fails
1086
+ *
1087
+ * @example
1088
+ * ```typescript
1089
+ * decoder.flushSync();
1090
+ * // Get remaining frames
1091
+ * let frame;
1092
+ * while ((frame = decoder.receiveSync()) !== null) {
1093
+ * console.log('Buffered frame');
1094
+ * }
1095
+ * ```
1096
+ *
1097
+ * @see {@link flushFramesSync} For convenient sync iteration
1098
+ * @see {@link receiveSync} For getting buffered frames
1099
+ * @see {@link flush} For async version
1100
+ */
1101
+ flushSync() {
1102
+ if (this.isClosed) {
1103
+ return;
1104
+ }
1105
+ // Send flush packet (null)
1106
+ const ret = this.codecContext.sendPacketSync(null);
1107
+ if (ret < 0 && ret !== AVERROR_EOF) {
1108
+ if (ret !== AVERROR_EAGAIN) {
1109
+ FFmpegError.throwIfError(ret, 'Failed to flush decoder');
1110
+ }
1111
+ }
1112
+ }
1113
+ /**
1114
+ * Flush all buffered frames as async generator.
1115
+ *
1116
+ * Convenient async iteration over remaining frames.
1117
+ * Automatically sends flush signal and retrieves buffered frames.
1118
+ * Useful for end-of-stream processing.
1119
+ *
1120
+ * @yields {Frame} Buffered frames
1121
+ *
1122
+ * @example
1123
+ * ```typescript
1124
+ * // Flush at end of decoding
1125
+ * for await (const frame of decoder.flushFrames()) {
1126
+ * console.log('Processing buffered frame');
1127
+ * await encoder.encode(frame);
1128
+ * frame.free();
1129
+ * }
1130
+ * ```
1131
+ *
1132
+ * @see {@link decode} For sending packets and receiving frames
1133
+ * @see {@link flush} For signaling end-of-stream
1134
+ * @see {@link flushFramesSync} For synchronous version
1135
+ */
1136
+ async *flushFrames() {
1137
+ // Send flush signal
1138
+ await this.flush();
1139
+ while (true) {
1140
+ const remaining = await this.receive();
1141
+ if (!remaining)
1142
+ break;
1143
+ yield remaining;
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Flush all buffered frames as generator synchronously.
1148
+ * Synchronous version of flushFrames.
1149
+ *
1150
+ * Convenient sync iteration over remaining frames.
1151
+ * Automatically sends flush signal and retrieves buffered frames.
1152
+ * Useful for end-of-stream processing.
1153
+ *
1154
+ * @yields {Frame} Buffered frames
1155
+ *
1156
+ * @example
1157
+ * ```typescript
1158
+ * // Flush at end of decoding
1159
+ * for (const frame of decoder.flushFramesSync()) {
1160
+ * console.log('Processing buffered frame');
1161
+ * encoder.encodeSync(frame);
1162
+ * frame.free();
1163
+ * }
1164
+ * ```
1165
+ *
1166
+ * @see {@link decodeSync} For sending packets and receiving frames
1167
+ * @see {@link flushSync} For signaling end-of-stream
1168
+ * @see {@link flushFrames} For async version
1169
+ */
1170
+ *flushFramesSync() {
1171
+ // Send flush signal
1172
+ this.flushSync();
1173
+ while (true) {
1174
+ const remaining = this.receiveSync();
1175
+ if (!remaining)
1176
+ break;
1177
+ yield remaining;
1178
+ }
1179
+ }
1180
+ /**
1181
+ * Receive frame from decoder.
1182
+ *
1183
+ * Gets decoded frames from the codec's internal buffer.
1184
+ * Handles frame cloning and error checking.
1185
+ * Hardware frames include hw_frames_ctx reference.
1186
+ * Call repeatedly to drain all buffered frames.
1187
+ *
1188
+ * **Return Values:**
1189
+ * - `Frame` - Successfully decoded frame
1190
+ * - `null` - No frame available (AVERROR_EAGAIN), send more packets
1191
+ * - `undefined` - End of stream reached (AVERROR_EOF), decoder flushed
1192
+ *
1193
+ * Direct mapping to avcodec_receive_frame().
1194
+ *
1195
+ * @returns Decoded frame, null (need more data), or undefined (end of stream)
1196
+ *
1197
+ * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
1198
+ *
1199
+ * @throws {Error} If frame cloning fails (out of memory)
1200
+ *
1201
+ * @example
1202
+ * ```typescript
1203
+ * const frame = await decoder.receive();
1204
+ * if (frame === EOF) {
1205
+ * console.log('Decoder flushed, no more frames');
1206
+ * } else if (frame) {
1207
+ * console.log('Got decoded frame');
1208
+ * frame.free();
1209
+ * } else {
1210
+ * console.log('Need more packets');
1211
+ * }
1212
+ * ```
1213
+ *
1214
+ * @example
1215
+ * ```typescript
1216
+ * // Drain all buffered frames (stop on null or EOF)
1217
+ * let frame;
1218
+ * while ((frame = await decoder.receive()) && frame !== EOF) {
1219
+ * console.log(`Frame PTS: ${frame.pts}`);
1220
+ * frame.free();
1221
+ * }
1222
+ * ```
1223
+ *
1224
+ * @see {@link decode} For sending packets
1225
+ * @see {@link flush} For signaling end-of-stream
1226
+ * @see {@link receiveSync} For synchronous version
1227
+ * @see {@link EOF} For end-of-stream signal
1228
+ */
1229
+ async receive() {
1230
+ if (this.isClosed) {
1231
+ return EOF;
1232
+ }
1233
+ // Frames the software decoder produced from the replayed packets come first
1234
+ const replayed = this.replayedFrames.shift();
1235
+ if (replayed) {
1236
+ return replayed;
1237
+ }
1238
+ // Clear previous frame data
1239
+ this.frame.unref();
1240
+ const ret = await this.codecContext.receiveFrame(this.frame);
1241
+ if (ret === 0) {
1242
+ if (this.hardwareWatch) {
1243
+ this.hardwareWatch.framesSincePacket++;
1244
+ }
1245
+ // Set frame time_base to decoder's packet timebase
1246
+ this.frame.timeBase = this.codecContext.pktTimebase;
1247
+ // Corrupt frame: with exitOnError off it is passed on with its flags set,
1248
+ // as FFmpeg CLI does (it only warns), so callers can decide to drop it
1249
+ if (this.options.exitOnError && (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT))) {
1250
+ throw new FFmpegError(AVERROR_INVALIDDATA);
1251
+ }
1252
+ // Handles PTS assignment, duration estimation, and frame tracking
1253
+ if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
1254
+ this.processVideoFrame(this.frame);
1255
+ }
1256
+ // Handles timestamp extrapolation, sample rate changes, and duration calculation
1257
+ if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
1258
+ this.processAudioFrame(this.frame);
1259
+ if (!this.audioResamplerSetup) {
1260
+ this.setupAudioResampler(this.frame);
1261
+ }
1262
+ if (this.audioResampler) {
1263
+ // null = resampler buffered the input without emitting; caller feeds more
1264
+ return this.resampleAudioClone(this.frame);
1265
+ }
1266
+ }
1267
+ // Got a frame, clone it for the user
1268
+ const cloned = this.frame.clone();
1269
+ if (!cloned) {
1270
+ throw new Error('Failed to clone frame (out of memory)');
1271
+ }
1272
+ return cloned;
1273
+ }
1274
+ else if (ret === AVERROR_EAGAIN) {
1275
+ // Need more data
1276
+ return null;
1277
+ }
1278
+ else if (ret === AVERROR_EOF) {
1279
+ // Flush any samples buffered inside the resampler before signaling EOF
1280
+ if (this.audioResampler && !this.audioResamplerDrained) {
1281
+ const drained = this.drainResamplerClone();
1282
+ if (drained) {
1283
+ return drained;
1284
+ }
1285
+ this.audioResamplerDrained = true;
1286
+ }
1287
+ // End of stream
1288
+ return EOF;
1289
+ }
1290
+ else {
1291
+ // Error during receive
1292
+ this._decodeErrors++;
1293
+ if (this.options.exitOnError) {
1294
+ FFmpegError.throwIfError(ret, 'Failed to receive frame');
1295
+ }
1296
+ // exitOnError=false: return null, caller can retry if desired
1297
+ return null;
1298
+ }
1299
+ }
1300
+ /**
1301
+ * Receive frame from decoder synchronously.
1302
+ * Synchronous version of receive.
1303
+ *
1304
+ * Gets decoded frames from the codec's internal buffer.
1305
+ * Handles frame cloning and error checking.
1306
+ * Hardware frames include hw_frames_ctx reference.
1307
+ * Call repeatedly to drain all buffered frames.
1308
+ *
1309
+ * **Return Values:**
1310
+ * - `Frame` - Successfully decoded frame
1311
+ * - `null` - No frame available (AVERROR_EAGAIN), send more packets
1312
+ * - `undefined` - End of stream reached (AVERROR_EOF), decoder flushed
1313
+ *
1314
+ * Direct mapping to avcodec_receive_frame().
1315
+ *
1316
+ * @returns Decoded frame, null (need more data), or undefined (end of stream)
1317
+ *
1318
+ * @throws {FFmpegError} If receive fails with error other than AVERROR_EAGAIN or AVERROR_EOF
1319
+ *
1320
+ * @throws {Error} If frame cloning fails (out of memory)
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * const frame = decoder.receiveSync();
1325
+ * if (frame === EOF) {
1326
+ * console.log('Decoder flushed, no more frames');
1327
+ * } else if (frame) {
1328
+ * console.log('Got decoded frame');
1329
+ * frame.free();
1330
+ * } else {
1331
+ * console.log('Need more packets');
1332
+ * }
1333
+ * ```
1334
+ *
1335
+ * @example
1336
+ * ```typescript
1337
+ * // Drain all buffered frames (stop on null or EOF)
1338
+ * let frame;
1339
+ * while ((frame = decoder.receiveSync()) && frame !== EOF) {
1340
+ * console.log(`Frame PTS: ${frame.pts}`);
1341
+ * frame.free();
1342
+ * }
1343
+ * ```
1344
+ *
1345
+ * @see {@link decodeSync} For sending packets
1346
+ * @see {@link flushSync} For signaling end-of-stream
1347
+ * @see {@link receive} For async version
1348
+ * @see {@link EOF} For end-of-stream signal
1349
+ */
1350
+ receiveSync() {
1351
+ if (this.isClosed) {
1352
+ return EOF;
1353
+ }
1354
+ const replayed = this.replayedFrames.shift();
1355
+ if (replayed) {
1356
+ return replayed;
1357
+ }
1358
+ // Clear previous frame data
1359
+ this.frame.unref();
1360
+ const ret = this.codecContext.receiveFrameSync(this.frame);
1361
+ if (ret === 0) {
1362
+ if (this.hardwareWatch) {
1363
+ this.hardwareWatch.framesSincePacket++;
1364
+ }
1365
+ // Set frame time_base to decoder's packet timebase
1366
+ this.frame.timeBase = this.codecContext.pktTimebase;
1367
+ // Check for corrupt frame
1368
+ if (this.options.exitOnError && (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT))) {
1369
+ throw new FFmpegError(AVERROR_INVALIDDATA);
1370
+ }
1371
+ // Process video frame
1372
+ // Handles PTS assignment, duration estimation, and frame tracking
1373
+ if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
1374
+ this.processVideoFrame(this.frame);
1375
+ }
1376
+ // Process audio frame
1377
+ // Handles timestamp extrapolation, sample rate changes, and duration calculation
1378
+ if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
1379
+ this.processAudioFrame(this.frame);
1380
+ if (!this.audioResamplerSetup) {
1381
+ this.setupAudioResampler(this.frame);
1382
+ }
1383
+ if (this.audioResampler) {
1384
+ // null = resampler buffered the input without emitting; caller feeds more
1385
+ return this.resampleAudioClone(this.frame);
1386
+ }
1387
+ }
1388
+ // Got a frame, clone it for the user
1389
+ const cloned = this.frame.clone();
1390
+ if (!cloned) {
1391
+ throw new Error('Failed to clone frame (out of memory)');
1392
+ }
1393
+ return cloned;
1394
+ }
1395
+ else if (ret === AVERROR_EAGAIN) {
1396
+ // Need more data
1397
+ return null;
1398
+ }
1399
+ else if (ret === AVERROR_EOF) {
1400
+ // Flush any samples buffered inside the resampler before signaling EOF
1401
+ if (this.audioResampler && !this.audioResamplerDrained) {
1402
+ const drained = this.drainResamplerClone();
1403
+ if (drained) {
1404
+ return drained;
1405
+ }
1406
+ this.audioResamplerDrained = true;
1407
+ }
1408
+ // End of stream
1409
+ return EOF;
1410
+ }
1411
+ else {
1412
+ // Error during receive
1413
+ this._decodeErrors++;
1414
+ if (this.options.exitOnError) {
1415
+ FFmpegError.throwIfError(ret, 'Failed to receive frame');
1416
+ }
1417
+ // exitOnError=false: return null, caller can retry if desired
1418
+ return null;
1419
+ }
1420
+ }
1421
+ pipeTo(target) {
1422
+ const t = target;
1423
+ // Store reference to next component for flush propagation
1424
+ this.nextComponent = t;
1425
+ // Start worker if not already running
1426
+ this.workerPromise ??= this.runWorker();
1427
+ // Start pipe task: decoder.outputQueue -> target.inputQueue (via target.send)
1428
+ this.pipeToPromise = (async () => {
1429
+ while (true) {
1430
+ const frame = await this.receiveFromQueue();
1431
+ if (!frame)
1432
+ break;
1433
+ await t.sendToQueue(frame);
1434
+ }
1435
+ })();
1436
+ // Return scheduler for chaining (target is now the last component)
1437
+ return new Scheduler(this, t);
1438
+ }
1439
+ /**
1440
+ * Close decoder and free resources.
1441
+ *
1442
+ * Releases codec context and internal frame buffer.
1443
+ * Safe to call multiple times.
1444
+ * Automatically called by Symbol.dispose.
1445
+ *
1446
+ * @example
1447
+ * ```typescript
1448
+ * const decoder = await Decoder.create(stream);
1449
+ * try {
1450
+ * // Use decoder
1451
+ * } finally {
1452
+ * decoder.close();
1453
+ * }
1454
+ * ```
1455
+ *
1456
+ * @see {@link Symbol.dispose} For automatic cleanup
1457
+ */
1458
+ close() {
1459
+ if (this.isClosed) {
1460
+ return;
1461
+ }
1462
+ this.isClosed = true;
1463
+ this.inputQueue?.close();
1464
+ this.outputQueue?.close();
1465
+ this.inputQueue?.clear();
1466
+ this.outputQueue?.clear();
1467
+ this.audioResampler?.[Symbol.dispose]();
1468
+ this.audioResampler = undefined;
1469
+ this.resampledFrame?.free();
1470
+ this.resampledFrame = undefined;
1471
+ this.videoScaler?.[Symbol.dispose]();
1472
+ this.videoScaler = undefined;
1473
+ this.scaledFrame?.free();
1474
+ this.scaledFrame = undefined;
1475
+ for (const held of this.hardwareWatch?.held ?? []) {
1476
+ held.free();
1477
+ }
1478
+ this.hardwareWatch = undefined;
1479
+ for (const frame of this.replayedFrames) {
1480
+ frame.free();
1481
+ }
1482
+ this.replayedFrames = [];
1483
+ this.codecContext.freeContext();
1484
+ this.frame.free();
1485
+ this.initialized = false;
1486
+ }
1487
+ /**
1488
+ * Get stream object.
1489
+ *
1490
+ * Returns the underlying stream being decoded.
1491
+ * Provides access to stream metadata and parameters.
1492
+ *
1493
+ * Borrowed: the stream is owned by the demuxer's format context and must not
1494
+ * be read after that demuxer is closed.
1495
+ *
1496
+ * @returns Stream object
1497
+ *
1498
+ * @internal
1499
+ *
1500
+ * @see {@link Stream} For stream details
1501
+ */
1502
+ getStream() {
1503
+ return this.stream;
1504
+ }
1505
+ /**
1506
+ * Get the stream's guessed frame rate.
1507
+ *
1508
+ * av_guess_frame_rate() of the decoded stream, taken when the decoder was
1509
+ * created, so it outlives the demuxer. FFmpeg CLI feeds this rate into the
1510
+ * filter graph, from where it reaches the encoder.
1511
+ *
1512
+ * @returns Guessed frame rate, or 0/1 if unknown
1513
+ *
1514
+ * @internal
1515
+ */
1516
+ getGuessedFrameRate() {
1517
+ return this.guessedFramerate;
1518
+ }
1519
+ /**
1520
+ * Get decoder codec.
1521
+ *
1522
+ * Returns the codec used by this decoder.
1523
+ * Useful for checking codec capabilities and properties.
1524
+ *
1525
+ * @returns Codec instance
1526
+ *
1527
+ * @internal
1528
+ *
1529
+ * @see {@link Codec} For codec details
1530
+ */
1531
+ getCodec() {
1532
+ return this.codec;
1533
+ }
1534
+ /**
1535
+ * Get underlying codec context.
1536
+ *
1537
+ * Returns the codec context for advanced operations.
1538
+ * Useful for accessing low-level codec properties and settings.
1539
+ * Returns null if decoder is closed.
1540
+ *
1541
+ * @returns Codec context or null if closed
1542
+ *
1543
+ * @internal
1544
+ *
1545
+ * @see {@link CodecContext} For context details
1546
+ */
1547
+ getCodecContext() {
1548
+ return !this.isClosed && this.initialized ? this.codecContext : null;
1549
+ }
1550
+ /**
1551
+ * Account for the packet that went in before this one and hold this one.
1552
+ *
1553
+ * A new hardware decoder has to produce a frame within {@link HW_DECODE_PROBE}
1554
+ * packets (plus its threads' delay) after its first keyframe. A running one
1555
+ * counts as stuck once two keyframes in a row stayed silent, because a
1556
+ * single corrupt keyframe leaves even a healthy decoder silent until the next
1557
+ * one. Packets since the last silent keyframe are held, so the software
1558
+ * decoder can start over from it.
1559
+ *
1560
+ * @param packet - The packet about to go in
1561
+ *
1562
+ * @returns Why the decoder counts as stuck, or null while it is not
1563
+ *
1564
+ * @internal
1565
+ */
1566
+ watchHardware(packet) {
1567
+ const watch = this.hardwareWatch;
1568
+ if (watch.lastKeyframe !== undefined) {
1569
+ if (watch.framesSincePacket > 0) {
1570
+ watch.proven = true;
1571
+ watch.silent = 0;
1572
+ watch.silentKeyframes = 0;
1573
+ watch.sinceSecondKeyframe = 0;
1574
+ for (const held of watch.held) {
1575
+ held.free();
1576
+ }
1577
+ watch.held = [];
1578
+ }
1579
+ else if (watch.started) {
1580
+ watch.silent++;
1581
+ if (watch.lastKeyframe && ++watch.silentKeyframes === 2) {
1582
+ watch.sinceSecondKeyframe = 0;
1583
+ }
1584
+ else if (watch.silentKeyframes >= 2) {
1585
+ watch.sinceSecondKeyframe++;
1586
+ }
1587
+ }
1588
+ }
1589
+ watch.framesSincePacket = 0;
1590
+ watch.lastKeyframe = packet.isKeyframe;
1591
+ watch.started ||= packet.isKeyframe;
1592
+ // Frame threads hold back one frame each before the first comes out
1593
+ const probe = HW_DECODE_PROBE + Math.max(0, this.codecContext.threadCount);
1594
+ const stuck = watch.proven ? watch.silentKeyframes >= 2 && watch.sinceSecondKeyframe >= HW_DECODE_DELAY : watch.silent >= probe;
1595
+ if (stuck) {
1596
+ return `no frame from the last ${watch.silent} packets`;
1597
+ }
1598
+ // Only a run that starts at a keyframe can be decoded again
1599
+ if (packet.isKeyframe) {
1600
+ for (const held of watch.held) {
1601
+ held.free();
1602
+ }
1603
+ watch.held = [];
1604
+ }
1605
+ if (packet.isKeyframe || watch.held.length > 0) {
1606
+ const held = packet.clone();
1607
+ if (held) {
1608
+ watch.held.push(held);
1609
+ }
1610
+ }
1611
+ return null;
1612
+ }
1613
+ /**
1614
+ * Reopen the decoder in software and decode the held packets again.
1615
+ *
1616
+ * The frames they produce are returned by the next receive calls, before
1617
+ * anything the new decoder makes of later packets.
1618
+ *
1619
+ * @param reason - Why the hardware decoder counts as stuck
1620
+ *
1621
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1622
+ *
1623
+ * @internal
1624
+ */
1625
+ async fallBackToSoftware(reason) {
1626
+ const held = this.takeHardwareWatch(reason);
1627
+ const frames = [];
1628
+ try {
1629
+ this.adoptCodecContext(await Decoder.create(this.stream, { ...this.options, hardware: undefined, hardwareFallback: false }));
1630
+ for (const packet of held) {
1631
+ await this.decode(packet);
1632
+ let frame;
1633
+ while ((frame = await this.receive()) && frame !== EOF) {
1634
+ frames.push(frame);
1635
+ }
1636
+ }
1637
+ }
1638
+ catch (error) {
1639
+ for (const frame of frames) {
1640
+ frame.free();
1641
+ }
1642
+ throw error;
1643
+ }
1644
+ finally {
1645
+ for (const packet of held) {
1646
+ packet.free();
1647
+ }
1648
+ }
1649
+ this.replayedFrames.push(...frames);
1650
+ }
1651
+ /**
1652
+ * Reopen the decoder in software and decode the held packets again synchronously.
1653
+ * Synchronous version of fallBackToSoftware.
1654
+ *
1655
+ * @param reason - Why the hardware decoder counts as stuck
1656
+ *
1657
+ * @throws {FFmpegError} If the software decoder fails to open or decode
1658
+ *
1659
+ * @internal
1660
+ *
1661
+ * @see {@link fallBackToSoftware} For async version
1662
+ */
1663
+ fallBackToSoftwareSync(reason) {
1664
+ const held = this.takeHardwareWatch(reason);
1665
+ const frames = [];
1666
+ try {
1667
+ this.adoptCodecContext(Decoder.createSync(this.stream, { ...this.options, hardware: undefined, hardwareFallback: false }));
1668
+ for (const packet of held) {
1669
+ this.decodeSync(packet);
1670
+ let frame;
1671
+ while ((frame = this.receiveSync()) && frame !== EOF) {
1672
+ frames.push(frame);
1673
+ }
1674
+ }
1675
+ }
1676
+ catch (error) {
1677
+ for (const frame of frames) {
1678
+ frame.free();
1679
+ }
1680
+ throw error;
1681
+ }
1682
+ finally {
1683
+ for (const packet of held) {
1684
+ packet.free();
1685
+ }
1686
+ }
1687
+ this.replayedFrames.push(...frames);
1688
+ }
1689
+ /**
1690
+ * End the watch on the hardware decoder, which fell back once and for all.
1691
+ *
1692
+ * @param reason - Why the hardware decoder counts as stuck
1693
+ *
1694
+ * @returns The held packets, owned by the caller
1695
+ *
1696
+ * @internal
1697
+ */
1698
+ takeHardwareWatch(reason) {
1699
+ const held = this.hardwareWatch?.held ?? [];
1700
+ this.hardwareWatch = undefined;
1701
+ Log.log(AV_LOG_WARNING, `Hardware decoder ${this.codec.name} failed (${reason}, ${this._decodeErrors} decode errors), decoding in software\n`);
1702
+ return held;
1703
+ }
1704
+ /**
1705
+ * Take over the codec context of a freshly opened decoder and close it with the old one.
1706
+ *
1707
+ * @param software - Decoder opened in software for the same stream
1708
+ *
1709
+ * @internal
1710
+ */
1711
+ adoptCodecContext(software) {
1712
+ [this.codecContext, software.codecContext] = [software.codecContext, this.codecContext];
1713
+ [this.codec, software.codec] = [software.codec, this.codec];
1714
+ this.options.hardware = undefined;
1715
+ // Set up for the device's frames, the software frames may differ
1716
+ this.hwTransferFormat = undefined;
1717
+ this.videoScaler?.[Symbol.dispose]();
1718
+ this.videoScaler = undefined;
1719
+ software.close();
1720
+ }
1721
+ /**
1722
+ * Worker loop for push-based processing.
1723
+ *
1724
+ * @internal
1725
+ */
1726
+ async runWorker() {
1727
+ try {
1728
+ // Outer loop - receive packets
1729
+ while (!this.inputQueue.isClosed) {
1730
+ const env_3 = { stack: [], error: void 0, hasError: false };
1731
+ try {
1732
+ const packet = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
1733
+ if (!packet)
1734
+ break;
1735
+ // Skip packets for other streams
1736
+ if (packet.streamIndex !== this.streamIndex) {
1737
+ continue;
1738
+ }
1739
+ if (packet.size === 0) {
1740
+ continue;
1741
+ }
1742
+ await this.decode(packet);
1743
+ // Receive ALL available frames immediately
1744
+ // This ensures frames are yielded ASAP without latency
1745
+ while (!this.outputQueue.isClosed) {
1746
+ const frame = await this.receive();
1747
+ if (!frame)
1748
+ break; // EAGAIN or EOF
1749
+ await this.outputQueue.send(frame);
1750
+ }
1751
+ }
1752
+ catch (e_3) {
1753
+ env_3.error = e_3;
1754
+ env_3.hasError = true;
1755
+ }
1756
+ finally {
1757
+ __disposeResources(env_3);
1758
+ }
1759
+ }
1760
+ // Flush decoder at end
1761
+ await this.flush();
1762
+ while (!this.outputQueue.isClosed) {
1763
+ const frame = await this.receive();
1764
+ if (!frame)
1765
+ break;
1766
+ await this.outputQueue.send(frame);
1767
+ }
1768
+ }
1769
+ catch (error) {
1770
+ // Propagate error to both queues so upstream and downstream know
1771
+ const err = error instanceof Error ? error : new Error(String(error));
1772
+ this.inputQueue?.closeWithError(err);
1773
+ this.outputQueue?.closeWithError(err);
1774
+ }
1775
+ finally {
1776
+ // Close output queue when done (if not already closed with error)
1777
+ this.outputQueue?.close();
1778
+ }
1779
+ }
1780
+ /**
1781
+ * Send packet to input queue or flush the pipeline.
1782
+ *
1783
+ * When packet is provided, queues it for processing.
1784
+ * When null is provided, triggers flush sequence:
1785
+ * - Closes input queue
1786
+ * - Waits for worker completion
1787
+ * - Flushes decoder and sends remaining frames to output queue
1788
+ * - Closes output queue
1789
+ * - Waits for pipeTo task completion
1790
+ * - Propagates flush to next component (if any)
1791
+ *
1792
+ * Used by scheduler system for pipeline control.
1793
+ *
1794
+ * @param packet - Packet to send, or null to flush
1795
+ *
1796
+ * @internal
1797
+ */
1798
+ async sendToQueue(packet) {
1799
+ if (packet) {
1800
+ await this.inputQueue.send(packet);
1801
+ }
1802
+ else {
1803
+ // Close input queue to signal end of stream to worker
1804
+ this.inputQueue.close();
1805
+ // Wait for worker to finish processing all packets (if exists)
1806
+ if (this.workerPromise) {
1807
+ await this.workerPromise;
1808
+ }
1809
+ // Flush decoder at end
1810
+ await this.flush();
1811
+ // Send all flushed frames to output queue
1812
+ while (true) {
1813
+ const frame = await this.receive();
1814
+ if (!frame)
1815
+ break;
1816
+ await this.outputQueue.send(frame);
1817
+ }
1818
+ // Close output queue to signal end of stream to pipeTo() task
1819
+ this.outputQueue.close();
1820
+ // Wait for pipeTo() task to finish processing all frames (if exists)
1821
+ if (this.pipeToPromise) {
1822
+ await this.pipeToPromise;
1823
+ }
1824
+ // Then propagate flush to next component
1825
+ if (this.nextComponent) {
1826
+ await this.nextComponent.sendToQueue(null);
1827
+ }
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Receive frame from output queue.
1832
+ *
1833
+ * @returns Frame from output queue or null if closed
1834
+ *
1835
+ * @internal
1836
+ */
1837
+ async receiveFromQueue() {
1838
+ return await this.outputQueue.receive();
1839
+ }
1840
+ /**
1841
+ * Estimate video frame duration.
1842
+ *
1843
+ * Implements FFmpeg CLI's video_duration_estimate() logic.
1844
+ * Uses multiple heuristics to determine frame duration when not explicitly available:
1845
+ * 1. Frame duration from container (if reliable)
1846
+ * 2. Duration from codec framerate
1847
+ * 3. PTS difference between frames
1848
+ * 4. Stream framerate
1849
+ * 5. Last frame's estimated duration
1850
+ *
1851
+ * @param frame - Frame to estimate duration for
1852
+ *
1853
+ * @returns Estimated duration in frame's timebase units
1854
+ *
1855
+ * @internal
1856
+ */
1857
+ estimateVideoDuration(frame) {
1858
+ // Difference between this and last frame's timestamps
1859
+ const tsDiff = frame.pts !== AV_NOPTS_VALUE && this.lastFramePts !== AV_NOPTS_VALUE ? frame.pts - this.lastFramePts : -1n;
1860
+ // Frame duration is unreliable (typically guessed by lavf) when it is equal
1861
+ // to 1 and the actual duration of the last frame is more than 2x larger
1862
+ const durationUnreliable = frame.duration === 1n && tsDiff > 2n * frame.duration;
1863
+ // Prefer frame duration for containers with timestamps
1864
+ if (frame.duration > 0n && !durationUnreliable) {
1865
+ return frame.duration;
1866
+ }
1867
+ // Calculate codec duration from framerate
1868
+ let codecDuration = 0n;
1869
+ const framerate = this.codecContext.framerate;
1870
+ if (framerate && framerate.den > 0 && framerate.num > 0) {
1871
+ const fields = (frame.repeatPict ?? 0) + 2;
1872
+ const fieldRate = avMulQ(framerate, { num: 2, den: 1 });
1873
+ codecDuration = avRescaleQ(fields, avInvQ(fieldRate), frame.timeBase);
1874
+ }
1875
+ // When timestamps are available, repeat last frame's actual duration
1876
+ if (tsDiff > 0n) {
1877
+ return tsDiff;
1878
+ }
1879
+ // Try frame/codec duration
1880
+ if (frame.duration > 0n) {
1881
+ return frame.duration;
1882
+ }
1883
+ if (codecDuration > 0n) {
1884
+ return codecDuration;
1885
+ }
1886
+ // Try stream framerate
1887
+ const streamFramerate = this.streamFramerate;
1888
+ if (streamFramerate && streamFramerate.num > 0 && streamFramerate.den > 0) {
1889
+ const d = avRescaleQ(1, avInvQ(streamFramerate), frame.timeBase);
1890
+ if (d > 0n) {
1891
+ return d;
1892
+ }
1893
+ }
1894
+ // Last resort is last frame's estimated duration, and 1
1895
+ return this.lastFrameDurationEst > 0n ? this.lastFrameDurationEst : 1n;
1896
+ }
1897
+ /**
1898
+ * Process video frame after decoding.
1899
+ *
1900
+ * Implements FFmpeg CLI's video_frame_process() logic.
1901
+ * Handles:
1902
+ * - Hardware frame transfer to software format
1903
+ * - PTS assignment from best_effort_timestamp
1904
+ * - PTS extrapolation when missing
1905
+ * - Duration estimation
1906
+ * - Frame tracking for next frame
1907
+ *
1908
+ * @param frame - Decoded frame to process
1909
+ *
1910
+ * @internal
1911
+ */
1912
+ processVideoFrame(frame) {
1913
+ // Video output rescaling (DecoderOptions.rescale). Replaces the former
1914
+ // hwaccelOutputFormat: hardware frames are first transferred to a supported
1915
+ // software format (swscale can't read GPU surfaces), then converted to the
1916
+ // requested pixel format / size on the CPU. Hardware frames are left untouched
1917
+ // (kept on the GPU) when rescale is not set.
1918
+ if (this.options.rescale) {
1919
+ if (frame.isHwFrame()) {
1920
+ if (!this.transferHwFrameToSoftware(frame)) {
1921
+ return; // transfer failed and exitOnError is off - leave the frame as-is
1922
+ }
1923
+ }
1924
+ // Only software frames can be converted with swscale; a still-hardware frame
1925
+ // (transfer above is the only path to software) is left untouched.
1926
+ if (!frame.isHwFrame()) {
1927
+ this.rescaleVideoInPlace(frame);
1928
+ }
1929
+ }
1930
+ // Set PTS from best_effort_timestamp
1931
+ frame.pts = frame.bestEffortTimestamp;
1932
+ // DECODER_FLAG_FRAMERATE_FORCED: Ignores all timestamps and generates constant framerate
1933
+ if (this.options.forcedFramerate) {
1934
+ frame.pts = AV_NOPTS_VALUE;
1935
+ frame.duration = 1n;
1936
+ const invFramerate = avInvQ(this.options.forcedFramerate);
1937
+ frame.timeBase = new Rational(invFramerate.num, invFramerate.den);
1938
+ }
1939
+ // No timestamp available - extrapolate from previous frame duration
1940
+ if (frame.pts === AV_NOPTS_VALUE) {
1941
+ frame.pts = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
1942
+ }
1943
+ // Update timestamp history
1944
+ this.lastFrameDurationEst = this.estimateVideoDuration(frame);
1945
+ this.lastFramePts = frame.pts;
1946
+ this.lastFrameTb = new Rational(frame.timeBase.num, frame.timeBase.den);
1947
+ // SAR override
1948
+ if (this.options.sarOverride) {
1949
+ frame.sampleAspectRatio = new Rational(this.options.sarOverride.num, this.options.sarOverride.den);
1950
+ }
1951
+ // Apply cropping
1952
+ if (this.options.applyCropping) {
1953
+ const ret = frame.applyCropping(1); // AV_FRAME_CROP_UNALIGNED = 1
1954
+ if (ret < 0) {
1955
+ this._decodeErrors++;
1956
+ if (this.options.exitOnError) {
1957
+ FFmpegError.throwIfError(ret, 'Error applying decoder cropping');
1958
+ }
1959
+ }
1960
+ }
1961
+ }
1962
+ /**
1963
+ * Audio samplerate update - handles sample rate changes.
1964
+ *
1965
+ * Based on FFmpeg's audio_samplerate_update().
1966
+ *
1967
+ * On sample rate change, chooses a new internal timebase that can represent
1968
+ * timestamps from all sample rates seen so far. Uses GCD to find minimal
1969
+ * common timebase, with fallback to LCM of common sample rates (28224000).
1970
+ *
1971
+ * Handles:
1972
+ * - Sample rate change detection
1973
+ * - Timebase calculation via GCD
1974
+ * - Overflow detection and fallback
1975
+ * - Frame timebase optimization
1976
+ * - Rescaling existing timestamps
1977
+ *
1978
+ * @param frame - Audio frame to process
1979
+ *
1980
+ * @returns Timebase to use for this frame
1981
+ *
1982
+ * @internal
1983
+ */
1984
+ audioSamplerateUpdate(frame) {
1985
+ const prev = this.lastFrameTb.den;
1986
+ const sr = frame.sampleRate;
1987
+ // No change - return existing timebase
1988
+ if (sr === this.lastFrameSampleRate) {
1989
+ return this.lastFrameTb;
1990
+ }
1991
+ // Calculate GCD to find minimal common timebase
1992
+ const gcd = avGcd(prev, sr);
1993
+ let tbNew;
1994
+ // Check for overflow
1995
+ if (Number(prev) / Number(gcd) >= INT_MAX / sr) {
1996
+ // LCM of 192000, 44100 - represents all common sample rates
1997
+ tbNew = { num: 1, den: 28224000 };
1998
+ }
1999
+ else {
2000
+ // Normal case
2001
+ tbNew = { num: 1, den: (Number(prev) / Number(gcd)) * sr };
2002
+ }
2003
+ // Keep frame's timebase if strictly better
2004
+ // "Strictly better" means: num=1, den > tbNew.den, and tbNew.den divides den evenly
2005
+ const frameTb = frame.timeBase;
2006
+ if (frameTb.num === 1 && frameTb.den > tbNew.den && frameTb.den % tbNew.den === 0) {
2007
+ tbNew = { num: frameTb.num, den: frameTb.den };
2008
+ }
2009
+ // Rescale existing timestamps to new timebase
2010
+ if (this.lastFramePts !== AV_NOPTS_VALUE) {
2011
+ this.lastFramePts = avRescaleQ(this.lastFramePts, this.lastFrameTb, tbNew);
2012
+ }
2013
+ this.lastFrameDurationEst = avRescaleQ(this.lastFrameDurationEst, this.lastFrameTb, tbNew);
2014
+ this.lastFrameTb = new Rational(tbNew.num, tbNew.den);
2015
+ this.lastFrameSampleRate = sr;
2016
+ return this.lastFrameTb;
2017
+ }
2018
+ /**
2019
+ * Audio timestamp processing - handles audio frame timestamps.
2020
+ *
2021
+ * Based on FFmpeg's audio_ts_process().
2022
+ *
2023
+ * Processes audio frame timestamps with:
2024
+ * - Sample rate change handling via audioSamplerateUpdate()
2025
+ * - PTS extrapolation when missing (pts_pred)
2026
+ * - Gap detection (resets av_rescale_delta state)
2027
+ * - Smooth timestamp conversion via av_rescale_delta
2028
+ * - Duration calculation from nb_samples
2029
+ * - Conversion to filtering timebase {1, sample_rate}
2030
+ *
2031
+ * Handles:
2032
+ * - Dynamic sample rate changes
2033
+ * - Missing timestamps (AV_NOPTS_VALUE)
2034
+ * - Timestamp gaps/discontinuities
2035
+ * - Sample-accurate timestamp generation
2036
+ * - Frame duration calculation
2037
+ *
2038
+ * @param frame - Decoded audio frame to process
2039
+ *
2040
+ * @internal
2041
+ */
2042
+ processAudioFrame(frame) {
2043
+ const nbSamples = frame.nbSamples;
2044
+ let pts = frame.pts;
2045
+ let frameTb = frame.timeBase;
2046
+ // Filtering timebase is always {1, sample_rate} for audio
2047
+ const tbFilter = { num: 1, den: frame.sampleRate };
2048
+ // Handle sample rate change - updates internal timebase
2049
+ const tb = this.audioSamplerateUpdate(frame);
2050
+ // Predict next PTS based on last frame + duration
2051
+ const ptsPred = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
2052
+ // No timestamp - use predicted value (in the internal timebase)
2053
+ if (pts === AV_NOPTS_VALUE) {
2054
+ pts = ptsPred;
2055
+ frameTb = tb;
2056
+ }
2057
+ else if (this.lastFramePts !== AV_NOPTS_VALUE) {
2058
+ // Detect timestamp gap - compare with predicted timestamp
2059
+ const ptsPredInFrameTb = avRescaleQRnd(ptsPred, tb, frameTb, AV_ROUND_UP);
2060
+ if (pts > ptsPredInFrameTb) {
2061
+ // Gap detected - reset rescale_delta state for smooth conversion
2062
+ this.lastFilterInRescaleDelta = AV_NOPTS_VALUE;
2063
+ }
2064
+ }
2065
+ // Smooth timestamp conversion with av_rescale_delta
2066
+ // This maintains fractional sample accuracy across timebase conversions
2067
+ // avRescaleDelta modifies lastRef in place (simulates C's &last_filter_in_rescale_delta)
2068
+ const lastRef = { value: this.lastFilterInRescaleDelta };
2069
+ pts = avRescaleDelta(frameTb, pts, tb, nbSamples, lastRef, tb);
2070
+ this.lastFilterInRescaleDelta = lastRef.value;
2071
+ // Update frame tracking
2072
+ this.lastFramePts = pts;
2073
+ this.lastFrameDurationEst = avRescaleQ(BigInt(nbSamples), tbFilter, tb);
2074
+ // Convert to filtering timebase and write the results back once
2075
+ frame.pts = avRescaleQ(pts, tb, tbFilter);
2076
+ frame.duration = BigInt(nbSamples);
2077
+ frame.timeBase = new Rational(tbFilter.num, tbFilter.den);
2078
+ }
2079
+ /**
2080
+ * Lazily configure the audio output resampler from the first decoded frame.
2081
+ *
2082
+ * Reads the source format from the frame, resolves the target from
2083
+ * `options.resample` (omitted fields keep the source value), and builds a
2084
+ * `SoftwareResampleContext` only when something actually differs. Runs once.
2085
+ *
2086
+ * @param frame - First decoded audio frame
2087
+ *
2088
+ * @throws {FFmpegError} If the resampler cannot be configured or initialized
2089
+ *
2090
+ * @internal
2091
+ */
2092
+ setupAudioResampler(frame) {
2093
+ this.audioResamplerSetup = true;
2094
+ const cfg = this.options.resample;
2095
+ if (!cfg) {
2096
+ return;
2097
+ }
2098
+ const inRate = frame.sampleRate;
2099
+ const inFmt = frame.format;
2100
+ // swr requires a concrete layout on both sides. Decoded PCM frames often carry
2101
+ // an unspecified layout (order UNSPEC, mask 0); normalize it to the canonical
2102
+ // native layout and re-apply it to each input frame so swr's input matches.
2103
+ let inLayout = frame.channelLayout;
2104
+ if (inLayout.order === AV_CHANNEL_ORDER_UNSPEC) {
2105
+ inLayout = avChannelLayoutDefault(inLayout.nbChannels);
2106
+ this.resampleInputLayout = inLayout;
2107
+ }
2108
+ const targetRate = cfg.sampleRate ?? inRate;
2109
+ const targetFmt = cfg.sampleFormat ?? inFmt;
2110
+ const targetLayout = cfg.channelLayout ?? inLayout;
2111
+ const needsResample = targetRate !== inRate || targetFmt !== inFmt || targetLayout.nbChannels !== inLayout.nbChannels;
2112
+ if (!needsResample) {
2113
+ this.resampleInputLayout = undefined;
2114
+ return;
2115
+ }
2116
+ this.resampleTarget = { rate: targetRate, fmt: targetFmt, layout: targetLayout };
2117
+ const swr = new SoftwareResampleContext();
2118
+ FFmpegError.throwIfError(swr.allocSetOpts2(targetLayout, targetFmt, targetRate, inLayout, inFmt, inRate), 'Failed to configure audio resampler');
2119
+ FFmpegError.throwIfError(swr.init(), 'Failed to initialize audio resampler');
2120
+ this.audioResampler = swr;
2121
+ }
2122
+ /**
2123
+ * Lazily allocate the reused resampler output frame.
2124
+ *
2125
+ * @returns The allocated output frame
2126
+ *
2127
+ * @internal
2128
+ */
2129
+ getResampleFrame() {
2130
+ if (!this.resampledFrame) {
2131
+ this.resampledFrame = new Frame();
2132
+ this.resampledFrame.alloc();
2133
+ }
2134
+ return this.resampledFrame;
2135
+ }
2136
+ /**
2137
+ * Resample a decoded audio frame to the target format and clone it for the user.
2138
+ *
2139
+ * `swr_convert_frame` allocates/sizes the output buffer; the sample timestamp
2140
+ * (carried in the frame's own timebase) is preserved, while `nb_samples` and
2141
+ * `sample_rate` reflect the new rate. Returns null when the resampler buffered
2142
+ * the input without emitting samples yet (caller should feed more).
2143
+ *
2144
+ * @param frame - Decoded source audio frame
2145
+ *
2146
+ * @returns Cloned resampled frame owned by the caller, or null if none emitted
2147
+ *
2148
+ * @throws {FFmpegError} If resampling fails
2149
+ *
2150
+ * @internal
2151
+ */
2152
+ resampleAudioClone(frame) {
2153
+ // Give swr the concrete layout it was configured with when the source is unspecified.
2154
+ if (this.resampleInputLayout) {
2155
+ frame.channelLayout = this.resampleInputLayout;
2156
+ }
2157
+ const out = this.getResampleFrame();
2158
+ out.unref();
2159
+ out.format = this.resampleTarget.fmt;
2160
+ out.sampleRate = this.resampleTarget.rate;
2161
+ out.channelLayout = this.resampleTarget.layout;
2162
+ FFmpegError.throwIfError(this.audioResampler.convertFrame(out, frame), 'Failed to resample audio frame');
2163
+ if (out.nbSamples <= 0) {
2164
+ return null;
2165
+ }
2166
+ // Keep the frame self-consistent: audio timebase is 1/sampleRate, so the
2167
+ // output carries the target rate's timebase with the pts rescaled into it
2168
+ // (and the duration in the same units), not the source frame's timebase.
2169
+ const outTb = new Rational(1, this.resampleTarget.rate);
2170
+ out.timeBase = outTb;
2171
+ out.pts = frame.pts === AV_NOPTS_VALUE ? AV_NOPTS_VALUE : avRescaleQ(frame.pts, frame.timeBase, outTb);
2172
+ out.duration = BigInt(out.nbSamples);
2173
+ const cloned = out.clone();
2174
+ if (!cloned) {
2175
+ throw new Error('Failed to clone resampled frame (out of memory)');
2176
+ }
2177
+ return cloned;
2178
+ }
2179
+ /**
2180
+ * Drain samples buffered inside the resampler (rate-conversion delay) at EOF.
2181
+ *
2182
+ * @returns Cloned drained frame owned by the caller, or null when empty
2183
+ *
2184
+ * @throws {FFmpegError} If draining fails
2185
+ *
2186
+ * @internal
2187
+ */
2188
+ drainResamplerClone() {
2189
+ const out = this.getResampleFrame();
2190
+ out.unref();
2191
+ out.format = this.resampleTarget.fmt;
2192
+ out.sampleRate = this.resampleTarget.rate;
2193
+ out.channelLayout = this.resampleTarget.layout;
2194
+ const ret = this.audioResampler.convertFrame(out, null);
2195
+ if (ret < 0 || out.nbSamples <= 0) {
2196
+ return null;
2197
+ }
2198
+ out.timeBase = new Rational(1, this.resampleTarget.rate);
2199
+ out.pts = AV_NOPTS_VALUE;
2200
+ out.duration = BigInt(out.nbSamples);
2201
+ const cloned = out.clone();
2202
+ if (!cloned) {
2203
+ throw new Error('Failed to clone resampled frame (out of memory)');
2204
+ }
2205
+ return cloned;
2206
+ }
2207
+ /**
2208
+ * Resolve (once, then cache) the software format to download hardware frames to.
2209
+ *
2210
+ * When `rescale.pixelFormat` is set and the frame's hardware frames context can
2211
+ * transfer directly to it, that format is used so the subsequent swscale can be
2212
+ * skipped entirely (when no resize is requested) or stay format-preserving. When
2213
+ * it cannot, `AV_PIX_FMT_NONE` is returned so `av_hwframe_transfer_data` picks a
2214
+ * supported format and the swscale handles the final conversion. Cached because
2215
+ * the hardware frames context is stable for the lifetime of the decoder.
2216
+ *
2217
+ * @param frame - First hardware frame (used to read the hardware frames context)
2218
+ *
2219
+ * @returns The download format, or `AV_PIX_FMT_NONE` to let FFmpeg choose
2220
+ *
2221
+ * @internal
2222
+ */
2223
+ resolveHwTransferFormat(frame) {
2224
+ if (this.hwTransferFormat !== undefined) {
2225
+ return this.hwTransferFormat;
2226
+ }
2227
+ let chosen = AV_PIX_FMT_NONE;
2228
+ const target = this.options.rescale?.pixelFormat;
2229
+ const hwCtx = frame.hwFramesCtx;
2230
+ if (target !== undefined && hwCtx) {
2231
+ const formats = hwCtx.transferGetFormats(AV_HWFRAME_TRANSFER_DIRECTION_FROM);
2232
+ if (Array.isArray(formats) && formats.includes(target)) {
2233
+ chosen = target;
2234
+ }
2235
+ }
2236
+ this.hwTransferFormat = chosen;
2237
+ return chosen;
2238
+ }
2239
+ /**
2240
+ * Transfer a hardware frame down to system memory, replacing it in place.
2241
+ *
2242
+ * The destination software format is the requested one when the GPU can transfer
2243
+ * to it directly (see {@link resolveHwTransferFormat}), otherwise one chosen
2244
+ * automatically by FFmpeg. The downloaded data replaces the hardware frame's
2245
+ * contents so the rest of the pipeline operates on a software frame.
2246
+ *
2247
+ * @param frame - Hardware frame to download (modified in place)
2248
+ *
2249
+ * @returns True on success; false when the transfer failed and `exitOnError` is off
2250
+ *
2251
+ * @throws {FFmpegError} If the transfer fails and `exitOnError` is enabled
2252
+ *
2253
+ * @internal
2254
+ */
2255
+ transferHwFrameToSoftware(frame) {
2256
+ const swFrame = new Frame();
2257
+ swFrame.alloc();
2258
+ // Transfer straight to the requested pixel format when the GPU supports it (so
2259
+ // the later swscale can be skipped); otherwise let FFmpeg pick a supported one.
2260
+ swFrame.format = this.resolveHwTransferFormat(frame);
2261
+ const ret = frame.hwframeTransferDataSync(swFrame, 0);
2262
+ if (ret < 0) {
2263
+ swFrame.free();
2264
+ this._decodeErrors++;
2265
+ if (this.options.exitOnError) {
2266
+ FFmpegError.throwIfError(ret, 'Failed to transfer hardware frame data');
2267
+ }
2268
+ return false;
2269
+ }
2270
+ swFrame.copyProps(frame);
2271
+ frame.unref();
2272
+ const refRet = frame.ref(swFrame);
2273
+ swFrame.free();
2274
+ if (refRet < 0) {
2275
+ this._decodeErrors++;
2276
+ if (this.options.exitOnError) {
2277
+ FFmpegError.throwIfError(refRet, 'Failed to reference software frame');
2278
+ }
2279
+ return false;
2280
+ }
2281
+ return true;
2282
+ }
2283
+ /**
2284
+ * Lazily allocate the reused rescaler output frame.
2285
+ *
2286
+ * @returns The allocated output frame
2287
+ *
2288
+ * @internal
2289
+ */
2290
+ getScaledFrame() {
2291
+ if (!this.scaledFrame) {
2292
+ this.scaledFrame = new Frame();
2293
+ this.scaledFrame.alloc();
2294
+ }
2295
+ return this.scaledFrame;
2296
+ }
2297
+ /**
2298
+ * Convert a (software) video frame to the requested `rescale` pixel format and/or
2299
+ * size, replacing it in place.
2300
+ *
2301
+ * Each omitted target field keeps the source value, and the conversion is skipped
2302
+ * entirely when the frame already matches. The swscale context is configured
2303
+ * lazily from the first frame that needs conversion. `sws_scale_frame` copies the
2304
+ * frame properties (timing, colorimetry, aspect ratio).
2305
+ *
2306
+ * @param frame - Software video frame to convert (modified in place)
2307
+ *
2308
+ * @throws {FFmpegError} If the rescaler fails to configure or convert and `exitOnError` is enabled
2309
+ *
2310
+ * @internal
2311
+ */
2312
+ rescaleVideoInPlace(frame) {
2313
+ const srcW = frame.width;
2314
+ const srcH = frame.height;
2315
+ const srcFmt = frame.format;
2316
+ const dstW = this.options.rescale.width ?? srcW;
2317
+ const dstH = this.options.rescale.height ?? srcH;
2318
+ const dstFmt = this.options.rescale.pixelFormat ?? srcFmt;
2319
+ // Already in the requested shape - nothing to do.
2320
+ if (dstW === srcW && dstH === srcH && dstFmt === srcFmt) {
2321
+ return;
2322
+ }
2323
+ if (!this.videoScaler) {
2324
+ const sws = new SoftwareScaleContext();
2325
+ sws.getContext(srcW, srcH, srcFmt, dstW, dstH, dstFmt, SWS_BILINEAR);
2326
+ FFmpegError.throwIfError(sws.initContext(), 'Failed to configure video rescaler');
2327
+ this.videoScaler = sws;
2328
+ }
2329
+ const out = this.getScaledFrame();
2330
+ out.unref();
2331
+ out.format = dstFmt;
2332
+ out.width = dstW;
2333
+ out.height = dstH;
2334
+ const ret = this.videoScaler.scaleFrameSync(out, frame);
2335
+ if (ret < 0) {
2336
+ this._decodeErrors++;
2337
+ if (this.options.exitOnError) {
2338
+ FFmpegError.throwIfError(ret, 'Failed to rescale video frame');
2339
+ }
2340
+ return; // leave the original frame untouched on failure
2341
+ }
2342
+ // Move the converted result back into `frame` so the rest of processVideoFrame
2343
+ // (and the caller) sees it (mirrors the hardware-transfer replace above).
2344
+ frame.unref();
2345
+ frame.ref(out);
2346
+ }
2347
+ /**
2348
+ * Dispose of decoder.
2349
+ *
2350
+ * Implements Disposable interface for automatic cleanup.
2351
+ * Equivalent to calling close().
2352
+ *
2353
+ * @example
2354
+ * ```typescript
2355
+ * {
2356
+ * using decoder = await Decoder.create(stream);
2357
+ * // Decode frames...
2358
+ * } // Automatically closed
2359
+ * ```
2360
+ *
2361
+ * @see {@link close} For manual cleanup
2362
+ */
2363
+ [Symbol.dispose]() {
2364
+ this.close();
2365
+ }
2366
+ }
2367
+ //# sourceMappingURL=decoder.js.map