node-av-lgpl 0.0.0 → 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 +711 -0
  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 +165 -3
@@ -0,0 +1,1027 @@
1
+ import { AV_NOPTS_VALUE, AV_SAMPLE_FMT_U8, AV_SAMPLE_FMT_U8P } from '../constants/constants.js';
2
+ import { AudioFifo } from '../lib/audio-fifo.js';
3
+ import { FFmpegError } from '../lib/error.js';
4
+ import { Frame } from '../lib/frame.js';
5
+ import { Rational } from '../lib/rational.js';
6
+ import { avGetBytesPerSample, avRescaleQ, avSampleFmtIsPlanar } from '../lib/utilities.js';
7
+ /**
8
+ * Deviation (seconds) between an input frame's timestamp and the sample count that still counts as continuous input.
9
+ *
10
+ * @internal
11
+ */
12
+ const RESYNC_TOLERANCE = 0.02;
13
+ /**
14
+ * Input (seconds) over which a larger deviation must persist before it is compensated.
15
+ *
16
+ * @internal
17
+ */
18
+ const CONFIRM_DURATION = 0.2;
19
+ /**
20
+ * Frames over which a larger deviation must persist before it is compensated.
21
+ *
22
+ * @internal
23
+ */
24
+ const CONFIRM_FRAMES = 3;
25
+ /**
26
+ * Largest overlap (seconds) dropped from the input instead of restarting the timeline.
27
+ *
28
+ * @internal
29
+ */
30
+ const MAX_TRIM = 1;
31
+ /**
32
+ * Default largest gap (seconds) filled with silence instead of restarting the timeline.
33
+ *
34
+ * @internal
35
+ */
36
+ const DEFAULT_MAX_GAP_FILL = 1;
37
+ /**
38
+ * Check the options of an audio frame buffer.
39
+ *
40
+ * @param options - Options to check
41
+ *
42
+ * @throws {RangeError} If an option is out of range
43
+ *
44
+ * @internal
45
+ */
46
+ export function assertAudioFrameBufferOptions(options) {
47
+ const { timestamps, maxGapFill } = options;
48
+ if (timestamps !== undefined && timestamps !== 'input' && timestamps !== 'samples') {
49
+ throw new RangeError(`timestamps must be 'input' or 'samples', got ${String(timestamps)}`);
50
+ }
51
+ if (maxGapFill !== undefined && !(Number.isFinite(maxGapFill) && maxGapFill >= 0)) {
52
+ throw new RangeError(`maxGapFill must be a finite number of seconds >= 0, got ${maxGapFill}`);
53
+ }
54
+ }
55
+ /**
56
+ * Middle value of a list of timestamps.
57
+ *
58
+ * @param values - Non-empty list of timestamps
59
+ *
60
+ * @returns The value in the middle after sorting
61
+ *
62
+ * @internal
63
+ */
64
+ function median(values) {
65
+ const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
66
+ return sorted[sorted.length >> 1];
67
+ }
68
+ /**
69
+ * Audio frame buffering utility for encoders with fixed frame size requirements.
70
+ *
71
+ * Many audio encoders (Opus, AAC, MP3, etc.) require frames with a specific number
72
+ * of samples (frame_size). This class buffers incoming frames and outputs frames
73
+ * with exactly the required size.
74
+ *
75
+ * Output frames keep the input timing: each carries the timestamp of its first
76
+ * sample, in the time base of the pushed frames, and counts on by samples while the
77
+ * input is continuous. Deviations of up to 20 ms count as continuous, and larger ones
78
+ * only once they persist over 0.2 s of input, so jitter and single frames with a wrong
79
+ * timestamp leave the audio untouched. A persistent gap of up to a second is then
80
+ * filled with silence and an overlap of up to a second is dropped, which keeps the
81
+ * audio contiguous and in sync; a larger jump restarts the timeline at the new
82
+ * timestamp on the next frame boundary. Frames without a timestamp or time base
83
+ * continue the previous one.
84
+ *
85
+ * Uses FFmpeg's AVAudioFifo internally for efficient sample buffering.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * import { AudioFrameBuffer } from 'node-av/api';
90
+ *
91
+ * // Create buffer for 480-sample frames (e.g., Opus at 24kHz)
92
+ * using buffer = AudioFrameBuffer.create(480, AV_SAMPLE_FMT_FLT, 48000, 'mono', 1);
93
+ *
94
+ * // Push variable-sized frames from filter
95
+ * for await (const frame of filterOutput) {
96
+ * await buffer.push(frame);
97
+ *
98
+ * // Pull fixed-size frames for encoder
99
+ * let outputFrame;
100
+ * while ((outputFrame = await buffer.pull()) !== null) {
101
+ * await encoder.encode(outputFrame);
102
+ * outputFrame.free();
103
+ * }
104
+ * }
105
+ *
106
+ * // Flush remaining samples (final partial frame is padded with silence)
107
+ * let outputFrame;
108
+ * while ((outputFrame = await buffer.pull()) !== null) {
109
+ * await encoder.encode(outputFrame);
110
+ * outputFrame.free();
111
+ * }
112
+ * const tail = await buffer.pullPartial();
113
+ * if (tail) {
114
+ * await encoder.encode(tail);
115
+ * tail.free();
116
+ * }
117
+ * ```
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * // Real-time output: input gaps become timestamp jumps instead of silence
122
+ * using buffer = AudioFrameBuffer.create(960, AV_SAMPLE_FMT_FLT, 48000, 'mono', 1, { maxGapFill: 0 });
123
+ * ```
124
+ */
125
+ export class AudioFrameBuffer {
126
+ fifo;
127
+ frame;
128
+ frameSize;
129
+ channels;
130
+ sampleStride;
131
+ staging;
132
+ stagingPlanes;
133
+ silenceValue;
134
+ sampleTimeBase;
135
+ timeBase = null;
136
+ countsInSamples = true;
137
+ followInput;
138
+ tolerance;
139
+ confirmSamples;
140
+ maxFill;
141
+ maxTrim;
142
+ headIndex = 0;
143
+ tailIndex = 0;
144
+ segments = [{ index: 0, pts: 0n }];
145
+ silence = [];
146
+ timed = false;
147
+ trimPending = 0;
148
+ deviation = null;
149
+ /**
150
+ * @param fifo - Underlying AudioFifo instance
151
+ *
152
+ * @param frameSize - Number of samples per output frame
153
+ *
154
+ * @param sampleFormat - Audio sample format
155
+ *
156
+ * @param sampleRate - Sample rate in Hz
157
+ *
158
+ * @param channelLayout - Channel layout
159
+ *
160
+ * @param options - Timing options
161
+ *
162
+ * @internal
163
+ */
164
+ constructor(fifo, frameSize, sampleFormat, sampleRate, channelLayout, options) {
165
+ this.fifo = fifo;
166
+ this.frameSize = frameSize;
167
+ this.channels = channelLayout.nbChannels;
168
+ this.frame = new Frame();
169
+ this.frame.alloc();
170
+ this.frame.nbSamples = frameSize;
171
+ this.frame.format = sampleFormat;
172
+ this.frame.sampleRate = sampleRate;
173
+ this.frame.channelLayout = channelLayout;
174
+ // Until the first pushed frame brings a time base, timestamps count samples.
175
+ this.sampleTimeBase = new Rational(1, sampleRate);
176
+ this.frame.timeBase = this.sampleTimeBase;
177
+ this.frame.getBuffer(0); // Allocate buffer once
178
+ // Frame.data is a copy instead of a view where external buffers are not allowed (Electron),
179
+ // so samples are staged in JS memory and copied into the frame with fromBuffer()
180
+ const planar = avSampleFmtIsPlanar(sampleFormat);
181
+ const planeCount = planar ? this.channels : 1;
182
+ this.sampleStride = avGetBytesPerSample(sampleFormat) * (planar ? 1 : this.channels);
183
+ const planeSize = frameSize * this.sampleStride;
184
+ this.staging = Buffer.alloc(planeSize * planeCount);
185
+ this.stagingPlanes = Array.from({ length: planeCount }, (_, i) => this.staging.subarray(i * planeSize, (i + 1) * planeSize));
186
+ // av_samples_set_silence() semantics: unsigned 8-bit formats are silent at 0x80
187
+ this.silenceValue = sampleFormat === AV_SAMPLE_FMT_U8 || sampleFormat === AV_SAMPLE_FMT_U8P ? 0x80 : 0;
188
+ this.followInput = options.timestamps !== 'samples';
189
+ this.tolerance = Math.round(sampleRate * RESYNC_TOLERANCE);
190
+ this.confirmSamples = Math.round(sampleRate * CONFIRM_DURATION);
191
+ // At least one frame: a restart pads the current frame with up to that much silence anyway,
192
+ // and the padding must never reach past the new timestamp
193
+ this.maxFill = Math.max(Math.round(sampleRate * (options.maxGapFill ?? DEFAULT_MAX_GAP_FILL)), frameSize);
194
+ this.maxTrim = Math.round(sampleRate * MAX_TRIM);
195
+ }
196
+ /**
197
+ * Create an audio frame buffer.
198
+ *
199
+ * @param frameSize - Required frame size in samples
200
+ *
201
+ * @param sampleFormat - Audio sample format
202
+ *
203
+ * @param sampleRate - Sample rate in Hz
204
+ *
205
+ * @param channelLayout - Channel layout (e.g., 'mono', 'stereo')
206
+ *
207
+ * @param channels - Number of audio channels
208
+ *
209
+ * @param options - Timing options
210
+ *
211
+ * @returns Configured audio frame buffer
212
+ *
213
+ * @throws {RangeError} If an option is out of range
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * // For Opus encoder at 48kHz with 20ms frames
218
+ * const buffer = AudioFrameBuffer.create(960, AV_SAMPLE_FMT_FLT, 48000, 'mono', 1);
219
+ * ```
220
+ */
221
+ static create(frameSize, sampleFormat, sampleRate, channelLayout, channels, options = {}) {
222
+ assertAudioFrameBufferOptions(options);
223
+ const fifo = new AudioFifo();
224
+ // Allocate FIFO with capacity for multiple frames
225
+ fifo.alloc(sampleFormat, channels, frameSize * 4);
226
+ return new AudioFrameBuffer(fifo, frameSize, sampleFormat, sampleRate, channelLayout, options);
227
+ }
228
+ /**
229
+ * Get number of samples currently in buffer.
230
+ *
231
+ * Includes silence inserted for gaps in the input.
232
+ *
233
+ * @returns Number of buffered samples
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * console.log(`Buffer contains ${buffer.size} samples`);
238
+ * ```
239
+ */
240
+ get size() {
241
+ return this.available();
242
+ }
243
+ /**
244
+ * Check if a complete frame is available.
245
+ *
246
+ * Returns true if the buffer holds at least frameSize samples.
247
+ *
248
+ * @returns True if a full frame can be pulled
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * while (buffer.hasFrame()) {
253
+ * const frame = buffer.pull();
254
+ * // Process frame...
255
+ * }
256
+ * ```
257
+ */
258
+ hasFrame() {
259
+ return this.available() >= this.frameSize;
260
+ }
261
+ /**
262
+ * Push an audio frame into the buffer asynchronously.
263
+ *
264
+ * The frame's samples are added to the internal FIFO.
265
+ * Call hasFrame() and pull() to retrieve fixed-size output frames.
266
+ *
267
+ * The first frame with a valid time base fixes the time base of the output frames.
268
+ * When the frame timestamps deviate from the buffered samples by more than 20 ms
269
+ * over at least 0.2 s of input (and 3 frames), a gap is filled with silence (up to
270
+ * `maxGapFill`) and an overlap of up to 1 s is dropped from the input; a larger jump
271
+ * restarts the timeline at the new timestamp after padding the current output frame
272
+ * with silence. The frames that arrive until then still continue the previous
273
+ * timeline. Each such correction is a short dropout or cut in the audio: input whose
274
+ * timestamps drift against its sample count, such as a capture device stamped from
275
+ * the system clock, gets one per 20 ms of drift, which keeps it within about 20 ms
276
+ * of its timestamps.
277
+ *
278
+ * @param frame - Audio frame to buffer
279
+ *
280
+ * @throws {Error} If the frame is not an audio frame
281
+ *
282
+ * @throws {FFmpegError} If the samples cannot be written to the FIFO
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * await buffer.push(filterFrame);
287
+ * ```
288
+ *
289
+ * @see {@link pushSync} For synchronous version
290
+ */
291
+ async push(frame) {
292
+ // Every Frame getter is a call into the addon, so each one is read only once per push
293
+ // (this is frame.isAudio() without its second nbSamples read)
294
+ const nbSamples = frame.nbSamples;
295
+ if (!(frame.sampleRate > 0 && nbSamples > 0)) {
296
+ throw new Error('AudioFrameBuffer.push() requires an audio frame');
297
+ }
298
+ const skip = this.planPush(frame, nbSamples);
299
+ const samples = nbSamples - skip;
300
+ if (samples <= 0) {
301
+ return;
302
+ }
303
+ try {
304
+ FFmpegError.throwIfError(await this.fifo.write(this.inputPlanes(frame, skip), samples), 'Failed to write samples to the audio FIFO');
305
+ }
306
+ catch (error) {
307
+ this.resyncTail();
308
+ throw error;
309
+ }
310
+ }
311
+ /**
312
+ * Push an audio frame into the buffer synchronously.
313
+ * Synchronous version of push.
314
+ *
315
+ * The frame's samples are added to the internal FIFO.
316
+ * Call hasFrame() and pullSync() to retrieve fixed-size output frames.
317
+ * Timestamps are handled as in push().
318
+ *
319
+ * @param frame - Audio frame to buffer
320
+ *
321
+ * @throws {Error} If the frame is not an audio frame
322
+ *
323
+ * @throws {FFmpegError} If the samples cannot be written to the FIFO
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * buffer.pushSync(filterFrame);
328
+ * ```
329
+ *
330
+ * @see {@link push} For async version
331
+ */
332
+ pushSync(frame) {
333
+ const nbSamples = frame.nbSamples;
334
+ if (!(frame.sampleRate > 0 && nbSamples > 0)) {
335
+ throw new Error('AudioFrameBuffer.pushSync() requires an audio frame');
336
+ }
337
+ const skip = this.planPush(frame, nbSamples);
338
+ const samples = nbSamples - skip;
339
+ if (samples <= 0) {
340
+ return;
341
+ }
342
+ try {
343
+ FFmpegError.throwIfError(this.fifo.writeSync(this.inputPlanes(frame, skip), samples), 'Failed to write samples to the audio FIFO');
344
+ }
345
+ catch (error) {
346
+ this.resyncTail();
347
+ throw error;
348
+ }
349
+ }
350
+ /**
351
+ * Pull a fixed-size audio frame from the buffer asynchronously.
352
+ *
353
+ * Reads exactly frameSize samples from the FIFO and returns a cloned Frame
354
+ * stamped with the timestamp of its first sample.
355
+ * Returns null if not enough samples are available.
356
+ * Reuses internal frame buffer for efficiency (like Decoder does).
357
+ *
358
+ * @returns Audio frame with exactly frameSize samples, or null if insufficient samples
359
+ *
360
+ * @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
361
+ *
362
+ * @throws {Error} If frame cloning fails (out of memory)
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * using frame = await buffer.pull();
367
+ * if (frame) {
368
+ * await encoder.encode(frame);
369
+ * }
370
+ * ```
371
+ *
372
+ * @see {@link pullSync} For synchronous version
373
+ */
374
+ async pull() {
375
+ if (!this.hasFrame()) {
376
+ return null;
377
+ }
378
+ const pts = this.headPts();
379
+ // Read samples from FIFO and copy them into the reusable frame
380
+ await this.readStaging(this.frameSize);
381
+ this.commitStaging();
382
+ return this.emitFrame(pts);
383
+ }
384
+ /**
385
+ * Pull a fixed-size audio frame from the buffer synchronously.
386
+ * Synchronous version of pull.
387
+ *
388
+ * Reads exactly frameSize samples from the FIFO and returns a cloned Frame
389
+ * stamped with the timestamp of its first sample.
390
+ * Returns null if not enough samples are available.
391
+ * Reuses internal frame buffer for efficiency (like Decoder does).
392
+ *
393
+ * @returns Audio frame with exactly frameSize samples, or null if insufficient samples
394
+ *
395
+ * @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
396
+ *
397
+ * @throws {Error} If frame cloning fails (out of memory)
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * using frame = buffer.pullSync();
402
+ * if (frame) {
403
+ * encoder.encodeSync(frame);
404
+ * }
405
+ * ```
406
+ *
407
+ * @see {@link pull} For async version
408
+ */
409
+ pullSync() {
410
+ if (!this.hasFrame()) {
411
+ return null;
412
+ }
413
+ const pts = this.headPts();
414
+ // Read samples from FIFO and copy them into the reusable frame
415
+ this.readStagingSync(this.frameSize);
416
+ this.commitStaging();
417
+ return this.emitFrame(pts);
418
+ }
419
+ /**
420
+ * Pull the final partial frame from the buffer asynchronously.
421
+ *
422
+ * Reads the remaining samples (fewer than frameSize) and pads the tail with
423
+ * silence so the returned frame still carries exactly frameSize samples.
424
+ * Intended for end-of-stream flushing only - without it the tail (up to
425
+ * frameSize - 1 samples) would be dropped. Returns null if the buffer is
426
+ * empty, or while a complete frame is still available (drain those with
427
+ * pull() first). The padding counts as audio, so frames pushed afterwards are
428
+ * timed after it.
429
+ *
430
+ * @returns Silence-padded audio frame with frameSize samples, or null if nothing to drain
431
+ *
432
+ * @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
433
+ *
434
+ * @throws {Error} If frame cloning fails (out of memory)
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * // At end of stream, after pull() has returned null
439
+ * using tail = await buffer.pullPartial();
440
+ * if (tail) {
441
+ * await encoder.encode(tail);
442
+ * }
443
+ * ```
444
+ *
445
+ * @see {@link pullPartialSync} For synchronous version
446
+ * @see {@link pull} For pulling complete frames
447
+ */
448
+ async pullPartial() {
449
+ const remaining = this.available();
450
+ if (remaining <= 0 || remaining >= this.frameSize) {
451
+ return null;
452
+ }
453
+ const pts = this.headPts();
454
+ // Read the remaining samples into the head of the staging buffer
455
+ await this.readStaging(remaining);
456
+ // Pad the tail with silence up to frameSize
457
+ this.fillSilence(remaining, this.frameSize);
458
+ this.commitStaging();
459
+ const frame = this.emitFrame(pts);
460
+ this.advanceTailToHead();
461
+ return frame;
462
+ }
463
+ /**
464
+ * Pull the final partial frame from the buffer synchronously.
465
+ * Synchronous version of pullPartial.
466
+ *
467
+ * Reads the remaining samples (fewer than frameSize) and pads the tail with
468
+ * silence so the returned frame still carries exactly frameSize samples.
469
+ * Intended for end-of-stream flushing only. Returns null if the buffer is
470
+ * empty, or while a complete frame is still available.
471
+ *
472
+ * @returns Silence-padded audio frame with frameSize samples, or null if nothing to drain
473
+ *
474
+ * @throws {FFmpegError} If the FIFO cannot be read or the frame cannot be filled
475
+ *
476
+ * @throws {Error} If frame cloning fails (out of memory)
477
+ *
478
+ * @example
479
+ * ```typescript
480
+ * // At end of stream, after pullSync() has returned null
481
+ * using tail = buffer.pullPartialSync();
482
+ * if (tail) {
483
+ * encoder.encodeSync(tail);
484
+ * }
485
+ * ```
486
+ *
487
+ * @see {@link pullPartial} For async version
488
+ * @see {@link pullSync} For pulling complete frames
489
+ */
490
+ pullPartialSync() {
491
+ const remaining = this.available();
492
+ if (remaining <= 0 || remaining >= this.frameSize) {
493
+ return null;
494
+ }
495
+ const pts = this.headPts();
496
+ // Read the remaining samples into the head of the staging buffer
497
+ this.readStagingSync(remaining);
498
+ // Pad the tail with silence up to frameSize
499
+ this.fillSilence(remaining, this.frameSize);
500
+ this.commitStaging();
501
+ const frame = this.emitFrame(pts);
502
+ this.advanceTailToHead();
503
+ return frame;
504
+ }
505
+ /**
506
+ * Reset the buffer, discarding all buffered samples.
507
+ *
508
+ * The next pushed frame starts a new timeline, and its time base becomes
509
+ * the output time base.
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * buffer.reset();
514
+ * ```
515
+ */
516
+ reset() {
517
+ this.fifo.reset();
518
+ this.headIndex = 0;
519
+ this.tailIndex = 0;
520
+ this.segments = [{ index: 0, pts: 0n }];
521
+ this.silence = [];
522
+ this.timed = false;
523
+ this.trimPending = 0;
524
+ this.deviation = null;
525
+ this.timeBase = null;
526
+ this.countsInSamples = true;
527
+ this.frame.timeBase = this.sampleTimeBase;
528
+ }
529
+ /**
530
+ * Decide how a pushed frame continues the buffered samples.
531
+ *
532
+ * The only place that moves the write position: it records the silence before the
533
+ * frame and accounts for the samples the caller then writes, so the position always
534
+ * runs ahead of the buffered samples by exactly the writes still in flight.
535
+ *
536
+ * @param frame - Audio frame about to be written
537
+ *
538
+ * @param nbSamples - Number of samples in the frame
539
+ *
540
+ * @returns Samples to drop from the head of the frame
541
+ *
542
+ * @internal
543
+ */
544
+ planPush(frame, nbSamples) {
545
+ this.timeBase ??= this.lockTimeBase(frame);
546
+ const pts = this.followInput ? this.inputPts(frame) : AV_NOPTS_VALUE;
547
+ if (pts !== AV_NOPTS_VALUE && nbSamples > 0) {
548
+ this.addSilence(this.timed ? this.trackInput(pts, nbSamples) : this.anchorInput(pts));
549
+ }
550
+ const skip = Math.min(this.trimPending, nbSamples);
551
+ this.trimPending -= skip;
552
+ this.tailIndex += nbSamples - skip;
553
+ return skip;
554
+ }
555
+ /**
556
+ * Append silence at the write position.
557
+ *
558
+ * Silence only exists as a run of sample indices and is generated while pulling, so
559
+ * a long gap costs neither a buffer of its size nor a FIFO grown to hold it (an
560
+ * AVAudioFifo never shrinks again).
561
+ *
562
+ * @param samples - Number of silent samples
563
+ *
564
+ * @internal
565
+ */
566
+ addSilence(samples) {
567
+ if (samples <= 0) {
568
+ return;
569
+ }
570
+ const last = this.silence[this.silence.length - 1];
571
+ if (last && last.index + last.length === this.tailIndex) {
572
+ last.length += samples;
573
+ }
574
+ else {
575
+ this.silence.push({ index: this.tailIndex, length: samples });
576
+ }
577
+ this.tailIndex += samples;
578
+ }
579
+ /**
580
+ * Anchor the timeline at the first frame that carries a timestamp.
581
+ *
582
+ * @param pts - Timestamp of the frame in the output time base
583
+ *
584
+ * @returns Silence to write before the frame
585
+ *
586
+ * @internal
587
+ */
588
+ anchorInput(pts) {
589
+ this.timed = true;
590
+ const first = this.segments[0];
591
+ if (this.segments.length === 1 && this.headIndex === first.index) {
592
+ // Nothing pulled yet: samples buffered without a timestamp end where this frame starts
593
+ first.pts = pts - this.ticks(this.tailIndex - first.index);
594
+ return 0;
595
+ }
596
+ // Samples without a timestamp already went out on a made-up timeline, which this
597
+ // frame cannot be measured against
598
+ return this.restart(pts);
599
+ }
600
+ /**
601
+ * Compare a frame's timestamp with the buffered samples and compensate a persistent deviation.
602
+ *
603
+ * Within the tolerance the frame counts on by samples. A larger deviation is only
604
+ * acted on once it has lasted over enough input in the same direction: jitter and
605
+ * frames with a wrong timestamp come and go, while gaps, overlaps and clock steps
606
+ * persist. The correction is the median deviation of that run, so jitter on top of
607
+ * a real step does not leak into it.
608
+ *
609
+ * @param pts - Timestamp of the frame in the output time base
610
+ *
611
+ * @param nbSamples - Number of samples in the frame
612
+ *
613
+ * @returns Silence to write before the frame
614
+ *
615
+ * @internal
616
+ */
617
+ trackInput(pts, nbSamples) {
618
+ // Where this frame belongs: after the buffered samples, minus the input still to be dropped
619
+ const expected = this.trimPending > 0 ? this.tailPts() - this.ticks(this.trimPending) : this.tailPts();
620
+ const offset = pts - expected;
621
+ const drift = this.samples(offset);
622
+ if (Math.abs(drift) <= this.tolerance) {
623
+ this.deviation = null;
624
+ return 0;
625
+ }
626
+ const sign = drift > 0 ? 1 : -1;
627
+ if (this.deviation?.sign !== sign) {
628
+ this.deviation = { sign, frames: 0, samples: 0, offsets: [] };
629
+ }
630
+ const deviation = this.deviation;
631
+ deviation.frames++;
632
+ deviation.samples += nbSamples;
633
+ deviation.offsets.push(offset);
634
+ if (deviation.frames < CONFIRM_FRAMES || deviation.samples < this.confirmSamples) {
635
+ return 0;
636
+ }
637
+ this.deviation = null;
638
+ return this.compensate(median(deviation.offsets), expected);
639
+ }
640
+ /**
641
+ * Apply a confirmed deviation to the timeline.
642
+ *
643
+ * A gap is filled with silence and an overlap is dropped from the input, which keeps
644
+ * the output contiguous and on its frame grid. Beyond their limits the timeline
645
+ * restarts at the frame's timestamp instead.
646
+ *
647
+ * @param offset - Deviation of the frame in the output time base
648
+ *
649
+ * @param expected - Timestamp the frame would have without the deviation
650
+ *
651
+ * @returns Silence to write before the frame
652
+ *
653
+ * @internal
654
+ */
655
+ compensate(offset, expected) {
656
+ const drift = this.samples(offset);
657
+ if (drift > 0) {
658
+ // Input still to be dropped is ahead of the expectation by the same amount
659
+ const cancelled = Math.min(drift, this.trimPending);
660
+ this.trimPending -= cancelled;
661
+ const gap = drift - cancelled;
662
+ if (gap <= this.maxFill) {
663
+ return gap;
664
+ }
665
+ }
666
+ else if (this.trimPending - drift <= this.maxTrim) {
667
+ // Dropping keeps the output monotonic; following a small backward step would make
668
+ // the muxer clamp every packet of the overlap to one tick after the previous one
669
+ this.trimPending -= drift;
670
+ return 0;
671
+ }
672
+ return this.restart(expected + offset);
673
+ }
674
+ /**
675
+ * Start a new segment at the next output frame boundary.
676
+ *
677
+ * Every output frame then lies within one segment, so its timestamp stays well
678
+ * defined; the current partial frame is padded with silence to get there.
679
+ *
680
+ * @param pts - Timestamp of the frame about to be written
681
+ *
682
+ * @returns Silence to write before the frame
683
+ *
684
+ * @internal
685
+ */
686
+ restart(pts) {
687
+ const pad = (this.frameSize - (this.tailIndex % this.frameSize)) % this.frameSize;
688
+ const index = this.tailIndex + pad;
689
+ const last = this.segments[this.segments.length - 1];
690
+ if (last.index === index) {
691
+ last.pts = pts;
692
+ }
693
+ else {
694
+ this.segments.push({ index, pts });
695
+ }
696
+ this.trimPending = 0;
697
+ this.deviation = null;
698
+ return pad;
699
+ }
700
+ /**
701
+ * Fix the output time base from the first pushed frame.
702
+ *
703
+ * Frames without a valid time base leave the output counting samples (1/sample_rate).
704
+ *
705
+ * @param frame - First pushed frame
706
+ *
707
+ * @returns Output time base
708
+ *
709
+ * @internal
710
+ */
711
+ lockTimeBase(frame) {
712
+ const tb = frame.timeBase;
713
+ const timeBase = tb.num > 0 && tb.den > 0 ? tb : this.sampleTimeBase;
714
+ this.countsInSamples = timeBase.num * this.sampleTimeBase.den === timeBase.den;
715
+ this.frame.timeBase = timeBase;
716
+ return timeBase;
717
+ }
718
+ /**
719
+ * Read a frame's timestamp in the output time base.
720
+ *
721
+ * @param frame - Pushed frame
722
+ *
723
+ * @returns Timestamp, or AV_NOPTS_VALUE if the frame has no timestamp or time base
724
+ *
725
+ * @internal
726
+ */
727
+ inputPts(frame) {
728
+ const pts = frame.pts;
729
+ if (pts === AV_NOPTS_VALUE) {
730
+ return pts;
731
+ }
732
+ const tb = frame.timeBase;
733
+ if (tb.num <= 0 || tb.den <= 0) {
734
+ return AV_NOPTS_VALUE;
735
+ }
736
+ const out = this.timeBase;
737
+ return tb.num === out.num && tb.den === out.den ? pts : avRescaleQ(pts, tb, out);
738
+ }
739
+ /**
740
+ * Timestamp of the next sample to pull.
741
+ *
742
+ * Drops segments the head has reached the end of first, so the head lies in the first one.
743
+ *
744
+ * @returns Timestamp in the output time base
745
+ *
746
+ * @internal
747
+ */
748
+ headPts() {
749
+ while (this.segments.length > 1 && this.segments[1].index <= this.headIndex) {
750
+ this.segments.shift();
751
+ }
752
+ const segment = this.segments[0];
753
+ return segment.pts + this.ticks(this.headIndex - segment.index);
754
+ }
755
+ /**
756
+ * Timestamp right after the last buffered sample.
757
+ *
758
+ * @returns Timestamp in the output time base
759
+ *
760
+ * @internal
761
+ */
762
+ tailPts() {
763
+ const segment = this.segments[this.segments.length - 1];
764
+ return segment.pts + this.ticks(this.tailIndex - segment.index);
765
+ }
766
+ /**
767
+ * Treat the silence padding of a partial frame as written audio.
768
+ *
769
+ * @internal
770
+ */
771
+ advanceTailToHead() {
772
+ this.tailIndex = this.headIndex;
773
+ // Deviations seen so far were measured against the unpadded end
774
+ this.deviation = null;
775
+ }
776
+ /**
777
+ * Realign the write position with the FIFO after a failed write.
778
+ *
779
+ * @internal
780
+ */
781
+ resyncTail() {
782
+ this.tailIndex = this.headIndex + this.available();
783
+ while (this.silence.length > 0 && this.silence[this.silence.length - 1].index >= this.tailIndex) {
784
+ this.silence.pop();
785
+ }
786
+ while (this.segments.length > 1 && this.segments[this.segments.length - 1].index > this.tailIndex) {
787
+ this.segments.pop();
788
+ }
789
+ this.deviation = null;
790
+ }
791
+ /**
792
+ * Number of samples that can be pulled: buffered samples plus the silence between them.
793
+ *
794
+ * Stops at the first silence run that lies behind a write still in flight.
795
+ *
796
+ * @returns Number of samples available from the read position
797
+ *
798
+ * @internal
799
+ */
800
+ available() {
801
+ let buffered = this.fifo.size;
802
+ if (this.silence.length === 0) {
803
+ return buffered;
804
+ }
805
+ let index = this.headIndex;
806
+ for (const run of this.silence) {
807
+ // Negative while the read position lies inside the run
808
+ const before = run.index - index;
809
+ if (before > buffered) {
810
+ break;
811
+ }
812
+ buffered -= Math.max(before, 0);
813
+ index = run.index + run.length;
814
+ }
815
+ return index - this.headIndex + buffered;
816
+ }
817
+ /**
818
+ * Split the next samples to pull into FIFO reads and silence.
819
+ *
820
+ * @param count - Number of samples to pull
821
+ *
822
+ * @returns Parts in pull order
823
+ *
824
+ * @internal
825
+ */
826
+ readRuns(count) {
827
+ const runs = [];
828
+ const end = this.headIndex + count;
829
+ let index = this.headIndex;
830
+ for (const run of this.silence) {
831
+ if (run.index + run.length <= index) {
832
+ continue;
833
+ }
834
+ const start = Math.max(run.index, index);
835
+ if (start >= end) {
836
+ break;
837
+ }
838
+ if (start > index) {
839
+ runs.push({ length: start - index, silent: false });
840
+ }
841
+ index = Math.min(run.index + run.length, end);
842
+ runs.push({ length: index - start, silent: true });
843
+ }
844
+ if (index < end) {
845
+ runs.push({ length: end - index, silent: false });
846
+ }
847
+ return runs;
848
+ }
849
+ /**
850
+ * Read the next samples into the head of the staging buffer.
851
+ *
852
+ * @param count - Number of samples to read
853
+ *
854
+ * @throws {FFmpegError} If the FIFO cannot be read
855
+ *
856
+ * @internal
857
+ */
858
+ async readStaging(count) {
859
+ if (this.silence.length === 0) {
860
+ FFmpegError.throwIfError(await this.fifo.read(this.stagingPlanes, count), 'Failed to read samples from the audio FIFO');
861
+ return;
862
+ }
863
+ let offset = 0;
864
+ for (const run of this.readRuns(count)) {
865
+ if (run.silent) {
866
+ this.fillSilence(offset, offset + run.length);
867
+ }
868
+ else {
869
+ FFmpegError.throwIfError(await this.fifo.read(this.stagingAt(offset), run.length), 'Failed to read samples from the audio FIFO');
870
+ }
871
+ offset += run.length;
872
+ }
873
+ }
874
+ /**
875
+ * Read the next samples into the head of the staging buffer synchronously.
876
+ * Synchronous version of readStaging.
877
+ *
878
+ * @param count - Number of samples to read
879
+ *
880
+ * @throws {FFmpegError} If the FIFO cannot be read
881
+ *
882
+ * @internal
883
+ */
884
+ readStagingSync(count) {
885
+ if (this.silence.length === 0) {
886
+ FFmpegError.throwIfError(this.fifo.readSync(this.stagingPlanes, count), 'Failed to read samples from the audio FIFO');
887
+ return;
888
+ }
889
+ let offset = 0;
890
+ for (const run of this.readRuns(count)) {
891
+ if (run.silent) {
892
+ this.fillSilence(offset, offset + run.length);
893
+ }
894
+ else {
895
+ FFmpegError.throwIfError(this.fifo.readSync(this.stagingAt(offset), run.length), 'Failed to read samples from the audio FIFO');
896
+ }
897
+ offset += run.length;
898
+ }
899
+ }
900
+ /**
901
+ * Staging planes starting at a sample offset.
902
+ *
903
+ * @param offset - First sample to write to
904
+ *
905
+ * @returns Plane buffers for a FIFO read
906
+ *
907
+ * @internal
908
+ */
909
+ stagingAt(offset) {
910
+ if (offset === 0) {
911
+ return this.stagingPlanes;
912
+ }
913
+ const start = offset * this.sampleStride;
914
+ return this.stagingPlanes.map((plane) => plane.subarray(start));
915
+ }
916
+ /**
917
+ * Convert a sample count to output time base ticks.
918
+ *
919
+ * @param samples - Number of samples
920
+ *
921
+ * @returns Duration in the output time base
922
+ *
923
+ * @internal
924
+ */
925
+ ticks(samples) {
926
+ return this.countsInSamples ? BigInt(samples) : avRescaleQ(samples, this.sampleTimeBase, this.timeBase ?? this.sampleTimeBase);
927
+ }
928
+ /**
929
+ * Convert output time base ticks to a sample count.
930
+ *
931
+ * @param ticks - Duration in the output time base
932
+ *
933
+ * @returns Number of samples (rounded)
934
+ *
935
+ * @internal
936
+ */
937
+ samples(ticks) {
938
+ return Number(this.countsInSamples ? ticks : avRescaleQ(ticks, this.timeBase ?? this.sampleTimeBase, this.sampleTimeBase));
939
+ }
940
+ /**
941
+ * Plane buffers of a pushed frame, starting at a sample offset.
942
+ *
943
+ * @param frame - Pushed frame
944
+ *
945
+ * @param skip - Samples to leave out at the head
946
+ *
947
+ * @returns Plane buffers to write to the FIFO
948
+ *
949
+ * @internal
950
+ */
951
+ inputPlanes(frame, skip) {
952
+ const planes = frame.data;
953
+ if (skip === 0) {
954
+ return planes;
955
+ }
956
+ const offset = skip * this.sampleStride;
957
+ return planes.map((plane) => plane.subarray(offset));
958
+ }
959
+ /**
960
+ * Stamp the reusable frame, advance the read position by one frame and hand out a clone.
961
+ *
962
+ * @param pts - Timestamp of the frame's first sample
963
+ *
964
+ * @returns Cloned output frame
965
+ *
966
+ * @throws {Error} If frame cloning fails (out of memory)
967
+ *
968
+ * @internal
969
+ */
970
+ emitFrame(pts) {
971
+ this.frame.pts = pts;
972
+ this.headIndex += this.frameSize;
973
+ while (this.silence.length > 0 && this.silence[0].index + this.silence[0].length <= this.headIndex) {
974
+ this.silence.shift();
975
+ }
976
+ // Clone frame for user (like Decoder does)
977
+ const cloned = this.frame.clone();
978
+ if (!cloned) {
979
+ throw new Error('Failed to clone frame (out of memory)');
980
+ }
981
+ return cloned;
982
+ }
983
+ /**
984
+ * Fill the staged sample range [fromSample, toSample) with silence.
985
+ *
986
+ * Planar frames carry one channel per plane; packed frames interleave all
987
+ * channels in plane 0.
988
+ *
989
+ * @param fromSample - First sample index to silence
990
+ *
991
+ * @param toSample - Sample index after the last one to silence
992
+ *
993
+ * @internal
994
+ */
995
+ fillSilence(fromSample, toSample) {
996
+ for (const plane of this.stagingPlanes) {
997
+ plane.fill(this.silenceValue, fromSample * this.sampleStride, toSample * this.sampleStride);
998
+ }
999
+ }
1000
+ /**
1001
+ * Copy the staged samples into the reusable frame.
1002
+ *
1003
+ * A frame pulled earlier may still share the frame's buffer, so the buffer is
1004
+ * made writable first instead of overwriting the samples under that frame.
1005
+ *
1006
+ * @throws {FFmpegError} If the frame buffer cannot be made writable or filled
1007
+ *
1008
+ * @internal
1009
+ */
1010
+ commitStaging() {
1011
+ FFmpegError.throwIfError(this.frame.makeWritable(), 'Failed to make frame writable');
1012
+ FFmpegError.throwIfError(this.frame.fromBuffer(this.staging), 'Failed to copy samples into frame');
1013
+ }
1014
+ /**
1015
+ * Free the buffer and all resources.
1016
+ *
1017
+ * @example
1018
+ * ```typescript
1019
+ * buffer.free();
1020
+ * ```
1021
+ */
1022
+ [Symbol.dispose]() {
1023
+ this.frame.free();
1024
+ this.fifo[Symbol.dispose]();
1025
+ }
1026
+ }
1027
+ //# sourceMappingURL=audio-frame-buffer.js.map