node-av-light 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,1344 @@
1
+ import { AV_NOPTS_VALUE } from '../constants/constants.js';
2
+ // ============================================================================
3
+ // Implementation
4
+ // ============================================================================
5
+ /**
6
+ * Pipeline implementation.
7
+ *
8
+ * Creates a processing pipeline from media components.
9
+ * Automatically handles type conversions and proper flushing order.
10
+ *
11
+ * @param args - Variable arguments depending on pipeline type
12
+ *
13
+ * @returns PipelineControl if output is present, AsyncGenerator otherwise
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Simple pipeline
18
+ * const control = pipeline(
19
+ * input,
20
+ * decoder,
21
+ * filter,
22
+ * encoder,
23
+ * output
24
+ * );
25
+ * await control.completion;
26
+ *
27
+ * // Named pipeline for muxing
28
+ * const control = pipeline(
29
+ * { video: videoInput, audio: audioInput },
30
+ * {
31
+ * video: [videoDecoder, scaleFilter, videoEncoder],
32
+ * audio: [audioDecoder, volumeFilter, audioEncoder]
33
+ * },
34
+ * output
35
+ * );
36
+ * await control.completion;
37
+ * ```
38
+ */
39
+ export function pipeline(...args) {
40
+ // Extract PipelineOptions if last arg is undefined or a PipelineOptions object.
41
+ // PipelineOptions is identified as a plain object carrying a known option key
42
+ // ('signal', 'onProgress', or 'progressInterval'). This distinguishes it from
43
+ // NamedInputs/NamedStages/NamedOutputs which have stream name keys.
44
+ let pipelineOptions;
45
+ const lastArg = args[args.length - 1];
46
+ if (lastArg === undefined) {
47
+ args.pop();
48
+ }
49
+ else if (args.length > 0 &&
50
+ typeof lastArg === 'object' &&
51
+ lastArg !== null &&
52
+ Object.getPrototypeOf(lastArg) === Object.prototype &&
53
+ ('signal' in lastArg || 'onProgress' in lastArg || 'progressInterval' in lastArg)) {
54
+ pipelineOptions = args.pop();
55
+ }
56
+ pipelineOptions?.signal?.throwIfAborted();
57
+ // Detect pipeline type based on first argument
58
+ const firstArg = args[0];
59
+ const secondArg = args[1];
60
+ // Check for shared Demuxer + NamedStages pattern
61
+ if (isDemuxer(firstArg) && isNamedStages(secondArg)) {
62
+ // Convert shared input to NamedInputs based on stages keys
63
+ const sharedInput = firstArg;
64
+ const stages = secondArg;
65
+ const namedInputs = {};
66
+ // Create NamedInputs with shared input for all streams in stages
67
+ for (const streamName of Object.keys(stages)) {
68
+ namedInputs[streamName] = sharedInput;
69
+ }
70
+ if (args.length === 3) {
71
+ // Full named pipeline with output(s)
72
+ return runNamedPipeline(namedInputs, stages, args[2], pipelineOptions);
73
+ }
74
+ else {
75
+ // Partial named pipeline
76
+ return runNamedPartialPipeline(namedInputs, stages);
77
+ }
78
+ }
79
+ if (isNamedInputs(firstArg)) {
80
+ // Named pipeline (2 or 3 arguments)
81
+ if (args.length === 2) {
82
+ // Partial named pipeline - return generators
83
+ return runNamedPartialPipeline(args[0], args[1]);
84
+ }
85
+ else {
86
+ // Full named pipeline with output
87
+ return runNamedPipeline(args[0], args[1], args[2], pipelineOptions);
88
+ }
89
+ }
90
+ else if (isDemuxer(firstArg)) {
91
+ // Check if this is a stream copy (Demuxer → Muxer)
92
+ if (args.length === 2 && isMuxer(args[1])) {
93
+ // Stream copy all streams
94
+ return runDemuxerPipeline(args[0], args[1], pipelineOptions);
95
+ }
96
+ else {
97
+ // Simple pipeline starting with Demuxer
98
+ return runSimplePipeline(args, pipelineOptions);
99
+ }
100
+ }
101
+ else {
102
+ // Simple pipeline (variable arguments)
103
+ return runSimplePipeline(args, pipelineOptions);
104
+ }
105
+ }
106
+ // ============================================================================
107
+ // PipelineControl Implementation
108
+ // ============================================================================
109
+ /**
110
+ * Accumulates output statistics and emits throttled progress updates.
111
+ *
112
+ * @internal
113
+ */
114
+ class ProgressTracker {
115
+ onProgress;
116
+ throttleMs;
117
+ frames = 0;
118
+ bytes = 0;
119
+ maxTime = 0;
120
+ startTime = Date.now();
121
+ lastEmit = 0;
122
+ /**
123
+ * @param onProgress - Optional callback for push updates
124
+ *
125
+ * @param throttleMs - Minimum interval between callbacks
126
+ *
127
+ * @internal
128
+ */
129
+ constructor(onProgress, throttleMs = 250) {
130
+ this.onProgress = onProgress;
131
+ this.throttleMs = throttleMs;
132
+ }
133
+ /**
134
+ * Record a written output packet.
135
+ *
136
+ * @param packet - The packet written to the output
137
+ *
138
+ * @internal
139
+ */
140
+ record(packet) {
141
+ this.frames++;
142
+ this.bytes += packet.size;
143
+ const tb = packet.timeBase;
144
+ if (packet.pts !== AV_NOPTS_VALUE && tb && tb.den > 0) {
145
+ const t = (Number(packet.pts) * tb.num) / tb.den;
146
+ if (t > this.maxTime) {
147
+ this.maxTime = t;
148
+ }
149
+ }
150
+ const now = Date.now();
151
+ if (this.onProgress && now - this.lastEmit >= this.throttleMs) {
152
+ this.lastEmit = now;
153
+ this.onProgress(this.snapshot());
154
+ }
155
+ }
156
+ /**
157
+ * Build a progress snapshot from the current counters.
158
+ *
159
+ * @returns The current progress statistics
160
+ *
161
+ * @internal
162
+ */
163
+ snapshot() {
164
+ const elapsed = (Date.now() - this.startTime) / 1000;
165
+ return {
166
+ frames: this.frames,
167
+ bytes: this.bytes,
168
+ time: this.maxTime,
169
+ fps: elapsed > 0 ? this.frames / elapsed : 0,
170
+ bitrate: this.maxTime > 0 ? (this.bytes * 8) / this.maxTime : 0,
171
+ speed: elapsed > 0 ? this.maxTime / elapsed : 0,
172
+ elapsed,
173
+ };
174
+ }
175
+ /**
176
+ * Emit a final progress update.
177
+ *
178
+ * @internal
179
+ */
180
+ finish() {
181
+ this.onProgress?.(this.snapshot());
182
+ }
183
+ }
184
+ const EMPTY_PROGRESS = { frames: 0, bytes: 0, time: 0, fps: 0, bitrate: 0, speed: 0, elapsed: 0 };
185
+ /**
186
+ * Pipeline control implementation.
187
+ *
188
+ * @internal
189
+ */
190
+ export class PipelineControlImpl {
191
+ _stopped = false;
192
+ _completion;
193
+ signalCleanup;
194
+ tracker;
195
+ sources;
196
+ /**
197
+ * @param executionPromise - Promise that resolves when pipeline completes
198
+ *
199
+ * @param signal - Optional AbortSignal for cancellation
200
+ *
201
+ * @param tracker - Optional progress tracker shared with the runner
202
+ *
203
+ * @param sources - Demuxer sources to interrupt on stop (unblocks a stalled read)
204
+ *
205
+ * @internal
206
+ */
207
+ constructor(executionPromise, signal, tracker, sources = []) {
208
+ this.tracker = tracker;
209
+ this.sources = sources;
210
+ // Don't resolve immediately on stop, wait for the actual pipeline to finish.
211
+ // Emit a final progress update once the pipeline settles.
212
+ this._completion = tracker ? executionPromise.finally(() => tracker.finish()) : executionPromise;
213
+ if (signal) {
214
+ const handler = () => this.stop();
215
+ signal.addEventListener('abort', handler, { once: true });
216
+ this.signalCleanup = () => signal.removeEventListener('abort', handler);
217
+ }
218
+ }
219
+ /**
220
+ * Stop the pipeline.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const control = pipeline(input, decoder, filter, encoder, output);
225
+ * control.stop();
226
+ * ```
227
+ *
228
+ * @see {@link PipelineControl.isStopped}
229
+ */
230
+ stop() {
231
+ this._stopped = true;
232
+ this.signalCleanup?.();
233
+ this.signalCleanup = undefined;
234
+ // Interrupt the source demuxer(s) so a blocking read (e.g. a quiet RTSP
235
+ // source) returns immediately. Without this, completion would wait for the
236
+ // read loop while the read waits to be unblocked - a teardown deadlock.
237
+ for (const source of this.sources) {
238
+ source.interrupt();
239
+ }
240
+ }
241
+ /**
242
+ * Check if pipeline is stopped.
243
+ *
244
+ * @returns True if stopped
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const control = pipeline(input, decoder, filter, encoder, output);
249
+ * const isStopped = control.isStopped();
250
+ * ```
251
+ *
252
+ * @see {@link PipelineControl.stop}
253
+ */
254
+ isStopped() {
255
+ return this._stopped;
256
+ }
257
+ /**
258
+ * Get completion promise.
259
+ */
260
+ get completion() {
261
+ return this._completion;
262
+ }
263
+ /**
264
+ * Get the latest progress snapshot.
265
+ */
266
+ get progress() {
267
+ return this.tracker?.snapshot() ?? EMPTY_PROGRESS;
268
+ }
269
+ }
270
+ // ============================================================================
271
+ // Demuxer Pipeline Implementation
272
+ // ============================================================================
273
+ /**
274
+ * Run a demuxer pipeline for stream copy.
275
+ *
276
+ * @param input - Media input source
277
+ *
278
+ * @param output - Media output destination
279
+ *
280
+ * @param options - Pipeline options for cancellation support
281
+ *
282
+ * @returns Pipeline control interface
283
+ *
284
+ * @internal
285
+ */
286
+ function runDemuxerPipeline(input, output, options) {
287
+ const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
288
+ let control;
289
+ // eslint-disable-next-line prefer-const
290
+ control = new PipelineControlImpl(runDemuxerPipelineAsync(input, output, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, [input]);
291
+ return control;
292
+ }
293
+ /**
294
+ * Run demuxer pipeline asynchronously.
295
+ *
296
+ * @param input - Media input source
297
+ *
298
+ * @param output - Media output destination
299
+ *
300
+ * @param shouldStop - Function to check if pipeline should stop
301
+ *
302
+ * @param tracker - Optional progress tracker for output statistics
303
+ *
304
+ * @internal
305
+ */
306
+ async function runDemuxerPipelineAsync(input, output, shouldStop, tracker) {
307
+ // Get all streams from input
308
+ const videoStream = input.video();
309
+ const audioStream = input.audio();
310
+ const streams = [];
311
+ // Add video stream if present
312
+ if (videoStream) {
313
+ const outputIndex = output.addStream(videoStream);
314
+ streams.push({ stream: videoStream, index: outputIndex });
315
+ }
316
+ // Add audio stream if present
317
+ if (audioStream) {
318
+ const outputIndex = output.addStream(audioStream);
319
+ streams.push({ stream: audioStream, index: outputIndex });
320
+ }
321
+ // Add any other streams
322
+ const allStreams = input.streams;
323
+ for (const stream of allStreams) {
324
+ // Skip if already added
325
+ if (stream !== videoStream && stream !== audioStream) {
326
+ const outputIndex = output.addStream(stream);
327
+ streams.push({ stream, index: outputIndex });
328
+ }
329
+ }
330
+ // Get iterator to properly clean up on stop
331
+ const packetsIterable = input.packets();
332
+ const iterator = packetsIterable[Symbol.asyncIterator]();
333
+ try {
334
+ // Copy all packets
335
+ while (true) {
336
+ // Check if we should stop before getting next item
337
+ if (shouldStop()) {
338
+ break;
339
+ }
340
+ const { value: packet, done } = await iterator.next();
341
+ if (done)
342
+ break;
343
+ // Handle EOF signal (null packet from input means all streams are done)
344
+ if (packet === null) {
345
+ // Signal EOF for all streams
346
+ for (const mapping of streams) {
347
+ await output.writePacket(null, mapping.index);
348
+ }
349
+ break;
350
+ }
351
+ try {
352
+ // Find the corresponding output stream index
353
+ const mapping = streams.find((s) => s.stream.index === packet.streamIndex);
354
+ if (mapping) {
355
+ await output.writePacket(packet, mapping.index);
356
+ tracker?.record(packet);
357
+ }
358
+ }
359
+ finally {
360
+ // Free the packet after use
361
+ if (packet && typeof packet.free === 'function') {
362
+ packet.free();
363
+ }
364
+ }
365
+ }
366
+ }
367
+ finally {
368
+ // Always clean up the generator to prevent memory leaks
369
+ if (iterator.return) {
370
+ await iterator.return(undefined);
371
+ }
372
+ }
373
+ await output.close();
374
+ }
375
+ // ============================================================================
376
+ // Simple Pipeline Implementation
377
+ // ============================================================================
378
+ /**
379
+ * Run a simple linear pipeline.
380
+ *
381
+ * @param args - Pipeline arguments
382
+ *
383
+ * @param options - Pipeline options for cancellation support
384
+ *
385
+ * @returns Pipeline control or async generator
386
+ *
387
+ * @internal
388
+ */
389
+ function runSimplePipeline(args, options) {
390
+ const [source, ...stages] = args;
391
+ // Check if last stage is Muxer (consumes stream)
392
+ const lastStage = stages[stages.length - 1];
393
+ const isOutput = isMuxer(lastStage);
394
+ // Track metadata through pipeline
395
+ const metadata = {};
396
+ // Store Demuxer reference if we have one
397
+ if (isDemuxer(source)) {
398
+ metadata.demuxer = source;
399
+ }
400
+ // Build the pipeline generator
401
+ // If output is present, exclude it from stages for processing
402
+ const processStages = isOutput ? stages.slice(0, -1) : stages;
403
+ // Process metadata first by walking through stages
404
+ for (const stage of processStages) {
405
+ if (isDecoder(stage)) {
406
+ metadata.decoder = stage;
407
+ }
408
+ else if (isEncoder(stage)) {
409
+ metadata.encoder = stage;
410
+ }
411
+ else if (isBitStreamFilterAPI(stage)) {
412
+ metadata.bitStreamFilter = stage;
413
+ }
414
+ }
415
+ // Convert Demuxer to packet stream if needed
416
+ // If we have a decoder or BSF, filter packets by stream index
417
+ let actualSource;
418
+ if (isDemuxer(source)) {
419
+ if (metadata.decoder) {
420
+ // Filter packets for the decoder's stream
421
+ const streamIndex = metadata.decoder.getStream().index;
422
+ actualSource = source.packets(streamIndex);
423
+ }
424
+ else if (metadata.bitStreamFilter) {
425
+ // Filter packets for the BSF's stream
426
+ const streamIndex = metadata.bitStreamFilter.getStream().index;
427
+ actualSource = source.packets(streamIndex);
428
+ }
429
+ else {
430
+ // No decoder or BSF, pass all packets
431
+ actualSource = source.packets();
432
+ }
433
+ }
434
+ else {
435
+ actualSource = source;
436
+ }
437
+ const generator = buildSimplePipeline(actualSource, processStages);
438
+ // If output, consume the generator
439
+ if (isOutput) {
440
+ const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
441
+ let control;
442
+ // eslint-disable-next-line prefer-const
443
+ control = new PipelineControlImpl(consumeSimplePipeline(generator, lastStage, metadata, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, isDemuxer(source) ? [source] : []);
444
+ return control;
445
+ }
446
+ // Otherwise return the generator for further processing
447
+ return generator;
448
+ }
449
+ /**
450
+ * Build a simple pipeline generator.
451
+ *
452
+ * @param source - Source of packets or frames
453
+ *
454
+ * @param stages - Processing stages
455
+ *
456
+ * @yields {Packet | Frame} Processed packets or frames
457
+ *
458
+ * @internal
459
+ */
460
+ async function* buildSimplePipeline(source, stages) {
461
+ let stream = source;
462
+ for (const stage of stages) {
463
+ if (isDecoder(stage)) {
464
+ stream = stage.frames(stream);
465
+ }
466
+ else if (isEncoder(stage)) {
467
+ stream = stage.packets(stream);
468
+ }
469
+ else if (isFilterAPI(stage)) {
470
+ stream = stage.frames(stream);
471
+ }
472
+ else if (isBitStreamFilterAPI(stage)) {
473
+ stream = stage.packets(stream);
474
+ }
475
+ else if (Array.isArray(stage)) {
476
+ // Chain multiple filters or BSFs
477
+ for (const filter of stage) {
478
+ if (isFilterAPI(filter)) {
479
+ stream = filter.frames(stream);
480
+ }
481
+ else if (isBitStreamFilterAPI(filter)) {
482
+ stream = filter.packets(stream);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ yield* stream;
488
+ }
489
+ /**
490
+ * Consume a simple pipeline stream and write to output.
491
+ *
492
+ * @param stream - Stream of packets or frames
493
+ *
494
+ * @param output - Media output destination
495
+ *
496
+ * @param metadata - Stream metadata
497
+ *
498
+ * @param shouldStop - Function to check if pipeline should stop
499
+ *
500
+ * @param tracker - Optional progress tracker for output statistics
501
+ *
502
+ * @internal
503
+ */
504
+ async function consumeSimplePipeline(stream, output, metadata, shouldStop, tracker) {
505
+ // Add stream to output if we have encoder or decoder info
506
+ let streamIndex = 0;
507
+ if (metadata.encoder) {
508
+ // Encoding path
509
+ if (metadata.decoder) {
510
+ // Have decoder - use its stream for metadata/properties
511
+ const originalStream = metadata.decoder.getStream();
512
+ streamIndex = output.addStream(originalStream, { encoder: metadata.encoder, bsf: metadata.bitStreamFilter });
513
+ }
514
+ else {
515
+ // Encoder-only mode (e.g., frame generator) - no input stream
516
+ streamIndex = output.addStream(metadata.encoder, { bsf: metadata.bitStreamFilter });
517
+ }
518
+ }
519
+ else if (metadata.decoder) {
520
+ // Stream copy - use decoder's original stream
521
+ const originalStream = metadata.decoder.getStream();
522
+ streamIndex = output.addStream(originalStream);
523
+ }
524
+ else if (metadata.bitStreamFilter) {
525
+ // BSF without encoder/decoder - use BSF's original stream
526
+ const originalStream = metadata.bitStreamFilter.getStream();
527
+ streamIndex = output.addStream(originalStream);
528
+ }
529
+ else {
530
+ // For direct Demuxer → Muxer, we redirect to runDemuxerPipeline
531
+ // This case shouldn't happen in simple pipeline
532
+ throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
533
+ }
534
+ // Get iterator to properly clean up on stop
535
+ const iterator = stream[Symbol.asyncIterator]();
536
+ try {
537
+ // Process stream
538
+ while (true) {
539
+ // Check if we should stop before getting next item
540
+ if (shouldStop()) {
541
+ break;
542
+ }
543
+ const { value: item, done } = await iterator.next();
544
+ if (done)
545
+ break;
546
+ // Handle EOF signal
547
+ if (item === null) {
548
+ await output.writePacket(null, streamIndex);
549
+ break;
550
+ }
551
+ // Use explicit resource management for the item
552
+ try {
553
+ if (isPacket(item) || item === null) {
554
+ await output.writePacket(item, streamIndex);
555
+ if (item !== null) {
556
+ tracker?.record(item);
557
+ }
558
+ }
559
+ else {
560
+ throw new Error('Cannot write frames directly to Muxer. Use an encoder first.');
561
+ }
562
+ }
563
+ finally {
564
+ // Free the packet/frame after use
565
+ if (item && typeof item.free === 'function') {
566
+ item.free();
567
+ }
568
+ }
569
+ }
570
+ }
571
+ finally {
572
+ // Always clean up the generator to prevent memory leaks
573
+ if (iterator.return) {
574
+ await iterator.return(undefined);
575
+ }
576
+ }
577
+ await output.close();
578
+ }
579
+ // ============================================================================
580
+ // Named Pipeline Implementation
581
+ // ============================================================================
582
+ /**
583
+ * Run a named partial pipeline.
584
+ *
585
+ * @param inputs - Named input sources
586
+ *
587
+ * @param stages - Named processing stages
588
+ *
589
+ * @returns Record of async generators
590
+ *
591
+ * @internal
592
+ */
593
+ function runNamedPartialPipeline(inputs, stages) {
594
+ const result = {};
595
+ for (const [streamName, streamStages] of Object.entries(stages)) {
596
+ const input = inputs[streamName];
597
+ if (!input) {
598
+ throw new Error(`No input found for stream: ${streamName}`);
599
+ }
600
+ // Get the appropriate stream based on the stream name
601
+ let stream = null;
602
+ switch (streamName) {
603
+ case 'video':
604
+ stream = input.video() ?? null;
605
+ break;
606
+ case 'audio':
607
+ stream = input.audio() ?? null;
608
+ break;
609
+ default:
610
+ // This should never happen
611
+ throw new Error(`Invalid stream name: ${streamName}. Must be 'video' or 'audio'.`);
612
+ }
613
+ if (!stream) {
614
+ throw new Error(`No ${streamName} stream found in input.`);
615
+ }
616
+ // Normalize stages: if array contains only undefined, treat as passthrough
617
+ // Also filter out undefined entries from the array
618
+ let normalizedStages = streamStages;
619
+ if (Array.isArray(streamStages)) {
620
+ const definedStages = streamStages.filter((stage) => stage !== undefined);
621
+ if (definedStages.length === 0) {
622
+ normalizedStages = 'passthrough';
623
+ }
624
+ else {
625
+ normalizedStages = definedStages;
626
+ }
627
+ }
628
+ if (normalizedStages === 'passthrough') {
629
+ // Direct passthrough - return input packets for this specific stream
630
+ result[streamName] = (async function* () {
631
+ for await (const packet of input.packets(stream.index)) {
632
+ yield packet;
633
+ }
634
+ })();
635
+ }
636
+ else {
637
+ // Process the stream - pass packets for this specific stream only
638
+ // Build pipeline for this stream (can return frames or packets)
639
+ const metadata = {};
640
+ const stages = normalizedStages;
641
+ result[streamName] = buildFlexibleNamedStreamPipeline(input.packets(stream.index), stages, metadata);
642
+ }
643
+ }
644
+ return result;
645
+ }
646
+ /**
647
+ * Run a named pipeline with outputs.
648
+ *
649
+ * @param inputs - Named input sources
650
+ *
651
+ * @param stages - Named processing stages
652
+ *
653
+ * @param output - Output destination(s)
654
+ *
655
+ * @param options - Pipeline options for cancellation support
656
+ *
657
+ * @returns Pipeline control interface
658
+ *
659
+ * @internal
660
+ */
661
+ function runNamedPipeline(inputs, stages, output, options) {
662
+ const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
663
+ // Collect the unique Demuxer sources so stop() can interrupt their reads.
664
+ const sources = [...new Set(Object.values(inputs).filter(isDemuxer))];
665
+ let control;
666
+ // eslint-disable-next-line prefer-const
667
+ control = new PipelineControlImpl(runNamedPipelineAsync(inputs, stages, output, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, sources);
668
+ return control;
669
+ }
670
+ /**
671
+ * Run named pipeline asynchronously.
672
+ *
673
+ * @param inputs - Named input sources
674
+ *
675
+ * @param stages - Named processing stages
676
+ *
677
+ * @param output - Output destination(s)
678
+ *
679
+ * @param shouldStop - Function to check if pipeline should stop
680
+ *
681
+ * @param tracker - Optional progress tracker for output statistics
682
+ *
683
+ * @internal
684
+ */
685
+ async function runNamedPipelineAsync(inputs, stages, output, shouldStop, tracker) {
686
+ // Check if all inputs reference the same Demuxer instance
687
+ const inputValues = Object.values(inputs);
688
+ const allSameInput = inputValues.length > 1 && inputValues.every((input) => input === inputValues[0]);
689
+ // Track metadata for each stream
690
+ const streamMetadata = {};
691
+ // Process each named stream into generators
692
+ const processedStreams = {};
693
+ // If all inputs are the same instance, use Demuxer's built-in parallel packet generators
694
+ if (allSameInput) {
695
+ const sharedInput = inputValues[0];
696
+ // Single pass: collect metadata and build pipelines directly using input.packets(streamIndex)
697
+ for (const [streamName, streamStages] of Object.entries(stages)) {
698
+ const metadata = {};
699
+ streamMetadata[streamName] = metadata;
700
+ // Normalize stages
701
+ let normalizedStages = streamStages;
702
+ if (Array.isArray(streamStages)) {
703
+ const definedStages = streamStages.filter((stage) => stage !== undefined);
704
+ if (definedStages.length === 0) {
705
+ normalizedStages = 'passthrough';
706
+ }
707
+ else {
708
+ normalizedStages = definedStages;
709
+ }
710
+ }
711
+ // Determine stream index and build pipeline
712
+ let streamIndex;
713
+ if (normalizedStages !== 'passthrough') {
714
+ const stages = normalizedStages;
715
+ // Set stream type
716
+ metadata.type = streamName;
717
+ // Populate metadata by walking through ALL stages
718
+ for (const stage of stages) {
719
+ if (isDecoder(stage)) {
720
+ metadata.decoder = stage;
721
+ streamIndex ??= stage.getStream().index;
722
+ }
723
+ else if (isBitStreamFilterAPI(stage)) {
724
+ metadata.bitStreamFilter = stage;
725
+ streamIndex ??= stage.getStream().index;
726
+ }
727
+ else if (isEncoder(stage)) {
728
+ metadata.encoder = stage;
729
+ }
730
+ }
731
+ // If no decoder/BSF, use stream name to determine index
732
+ if (streamIndex === undefined) {
733
+ const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
734
+ if (!stream) {
735
+ throw new Error(`No ${streamName} stream found in input.`);
736
+ }
737
+ streamIndex = stream.index;
738
+ }
739
+ // Build pipeline with packets from this specific stream
740
+ processedStreams[streamName] = buildNamedStreamPipeline(sharedInput.packets(streamIndex), stages, metadata);
741
+ }
742
+ else {
743
+ // Passthrough - use Demuxer's built-in stream filtering
744
+ metadata.type = streamName;
745
+ metadata.demuxer = sharedInput;
746
+ const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
747
+ if (!stream) {
748
+ throw new Error(`No ${streamName} stream found in input for passthrough.`);
749
+ }
750
+ streamIndex = stream.index;
751
+ // Direct passthrough using input.packets(streamIndex)
752
+ processedStreams[streamName] = sharedInput.packets(streamIndex);
753
+ }
754
+ }
755
+ }
756
+ else {
757
+ // Original logic: separate inputs or single input
758
+ for (const [streamName, streamStages] of Object.entries(stages)) {
759
+ const metadata = {};
760
+ streamMetadata[streamName] = metadata;
761
+ const input = inputs[streamName];
762
+ if (!input) {
763
+ throw new Error(`No input found for stream: ${streamName}`);
764
+ }
765
+ // Normalize stages: if array contains only undefined, treat as passthrough
766
+ // Also filter out undefined entries from the array
767
+ let normalizedStages = streamStages;
768
+ if (Array.isArray(streamStages)) {
769
+ const definedStages = streamStages.filter((stage) => stage !== undefined);
770
+ if (definedStages.length === 0) {
771
+ normalizedStages = 'passthrough';
772
+ }
773
+ else {
774
+ normalizedStages = definedStages;
775
+ }
776
+ }
777
+ if (normalizedStages === 'passthrough') {
778
+ // Direct passthrough - no processing
779
+ let stream = null;
780
+ switch (streamName) {
781
+ case 'video':
782
+ stream = input.video() ?? null;
783
+ metadata.type = 'video';
784
+ break;
785
+ case 'audio':
786
+ stream = input.audio() ?? null;
787
+ metadata.type = 'audio';
788
+ break;
789
+ }
790
+ if (!stream) {
791
+ throw new Error(`No ${streamName} stream found in input for passthrough.`);
792
+ }
793
+ processedStreams[streamName] = input.packets(stream.index);
794
+ metadata.demuxer = input; // Track Demuxer for passthrough
795
+ }
796
+ else {
797
+ // Process the stream - normalizedStages is guaranteed to be an array here
798
+ const stages = normalizedStages;
799
+ // Pre-populate metadata by walking through stages
800
+ for (const stage of stages) {
801
+ if (isDecoder(stage)) {
802
+ metadata.decoder = stage;
803
+ }
804
+ else if (isEncoder(stage)) {
805
+ metadata.encoder = stage;
806
+ }
807
+ else if (isBitStreamFilterAPI(stage)) {
808
+ metadata.bitStreamFilter = stage;
809
+ }
810
+ }
811
+ // Get packets - filter by stream index based on decoder, BSF, or stream type
812
+ let packets;
813
+ if (metadata.decoder) {
814
+ const streamIndex = metadata.decoder.getStream().index;
815
+ packets = input.packets(streamIndex);
816
+ }
817
+ else if (metadata.bitStreamFilter) {
818
+ const streamIndex = metadata.bitStreamFilter.getStream().index;
819
+ packets = input.packets(streamIndex);
820
+ }
821
+ else {
822
+ // No decoder or BSF - determine stream by name
823
+ let stream = null;
824
+ switch (streamName) {
825
+ case 'video':
826
+ stream = input.video() ?? null;
827
+ break;
828
+ case 'audio':
829
+ stream = input.audio() ?? null;
830
+ break;
831
+ }
832
+ if (!stream) {
833
+ throw new Error(`No ${streamName} stream found in input.`);
834
+ }
835
+ packets = input.packets(stream.index);
836
+ }
837
+ // Build pipeline for this stream
838
+ processedStreams[streamName] = buildNamedStreamPipeline(packets, stages, metadata);
839
+ }
840
+ }
841
+ }
842
+ // Write to output(s)
843
+ if (isMuxer(output)) {
844
+ // Always write streams in parallel - Muxer's SyncQueue handles interleaving internally
845
+ const streamIndices = {};
846
+ // Add all streams to output first
847
+ for (const [name, meta] of Object.entries(streamMetadata)) {
848
+ if (meta.encoder) {
849
+ // Encoding path
850
+ if (meta.decoder) {
851
+ // Have decoder - use its stream for metadata/properties
852
+ const originalStream = meta.decoder.getStream();
853
+ streamIndices[name] = output.addStream(originalStream, { encoder: meta.encoder, bsf: meta.bitStreamFilter });
854
+ }
855
+ else {
856
+ // Encoder-only mode (e.g., frame generator) - no input stream
857
+ streamIndices[name] = output.addStream(meta.encoder, { bsf: meta.bitStreamFilter });
858
+ }
859
+ }
860
+ else if (meta.decoder) {
861
+ // Stream copy - use decoder's original stream
862
+ const originalStream = meta.decoder.getStream();
863
+ streamIndices[name] = output.addStream(originalStream);
864
+ }
865
+ else if (meta.bitStreamFilter) {
866
+ // BSF - use BSF's original stream
867
+ const originalStream = meta.bitStreamFilter.getStream();
868
+ streamIndices[name] = output.addStream(originalStream);
869
+ }
870
+ else if (meta.demuxer) {
871
+ // Passthrough from Demuxer
872
+ const stream = name.includes('video') ? meta.demuxer.video() : meta.demuxer.audio();
873
+ if (!stream) {
874
+ throw new Error(`No matching stream found in Demuxer for ${name}`);
875
+ }
876
+ streamIndices[name] = output.addStream(stream);
877
+ }
878
+ else {
879
+ throw new Error(`Cannot determine stream configuration for ${name}. This is likely a bug in the pipeline.`);
880
+ }
881
+ }
882
+ // Write all streams in parallel - Muxer's SyncQueue handles interleaving
883
+ const promises = [];
884
+ for (const [name, stream] of Object.entries(processedStreams)) {
885
+ const streamIndex = streamIndices[name];
886
+ if (streamIndex !== undefined) {
887
+ promises.push(consumeStreamInParallel(stream, output, streamIndex, shouldStop, tracker));
888
+ }
889
+ }
890
+ await Promise.all(promises);
891
+ await output.close();
892
+ }
893
+ else {
894
+ // Multiple outputs - write each stream to its output. Group consumers by
895
+ // Muxer so each output is closed exactly once, after ALL streams feeding
896
+ // it are done - closing writes the trailer, without which containers like
897
+ // MP4 are left unreadable. Shared outputs (two streams into one Muxer)
898
+ // therefore must not be closed per stream.
899
+ const outputs = output;
900
+ const outputConsumers = new Map();
901
+ for (const [streamName, stream] of Object.entries(processedStreams)) {
902
+ const streamOutput = outputs[streamName];
903
+ const metadata = streamMetadata[streamName];
904
+ if (streamOutput && metadata) {
905
+ const consumer = consumeNamedStream(stream, streamOutput, metadata, shouldStop, tracker);
906
+ const consumers = outputConsumers.get(streamOutput);
907
+ if (consumers) {
908
+ consumers.push(consumer);
909
+ }
910
+ else {
911
+ outputConsumers.set(streamOutput, [consumer]);
912
+ }
913
+ }
914
+ }
915
+ // Finalize each output once its streams complete. A failing consumer
916
+ // rejects its output group and propagates to the completion promise
917
+ // (matching the single-output branch, which also skips close() on error).
918
+ await Promise.all([...outputConsumers].map(async ([muxer, consumers]) => {
919
+ await Promise.all(consumers);
920
+ await muxer.close();
921
+ }));
922
+ }
923
+ }
924
+ /**
925
+ * Build a flexible named stream pipeline.
926
+ *
927
+ * @param source - Source packets
928
+ *
929
+ * @param stages - Processing stages
930
+ *
931
+ * @param metadata - Stream metadata
932
+ *
933
+ * @yields {Packet | Frame} Processed packets or frames
934
+ *
935
+ * @internal
936
+ */
937
+ async function* buildFlexibleNamedStreamPipeline(source, stages, metadata) {
938
+ let stream = source;
939
+ for (const stage of stages) {
940
+ if (isDecoder(stage)) {
941
+ metadata.decoder = stage;
942
+ stream = stage.frames(stream);
943
+ }
944
+ else if (isEncoder(stage)) {
945
+ metadata.encoder = stage;
946
+ stream = stage.packets(stream);
947
+ }
948
+ else if (isFilterAPI(stage)) {
949
+ stream = stage.frames(stream);
950
+ }
951
+ else if (isBitStreamFilterAPI(stage)) {
952
+ metadata.bitStreamFilter = stage;
953
+ stream = stage.packets(stream);
954
+ }
955
+ else if (Array.isArray(stage)) {
956
+ // Chain multiple filters or BSFs
957
+ for (const filter of stage) {
958
+ if (isFilterAPI(filter)) {
959
+ stream = filter.frames(stream);
960
+ }
961
+ else if (isBitStreamFilterAPI(filter)) {
962
+ stream = filter.packets(stream);
963
+ }
964
+ }
965
+ }
966
+ }
967
+ // Yield whatever the pipeline produces (frames or packets)
968
+ yield* stream;
969
+ }
970
+ /**
971
+ * Build a named stream pipeline.
972
+ *
973
+ * @param source - Source packets
974
+ *
975
+ * @param stages - Processing stages
976
+ *
977
+ * @param metadata - Stream metadata
978
+ *
979
+ * @yields {Packet} Processed packets
980
+ *
981
+ * @internal
982
+ */
983
+ async function* buildNamedStreamPipeline(source, stages, metadata) {
984
+ let stream = source;
985
+ for (const stage of stages) {
986
+ if (isDecoder(stage)) {
987
+ metadata.decoder = stage;
988
+ stream = stage.frames(stream);
989
+ }
990
+ else if (isEncoder(stage)) {
991
+ metadata.encoder = stage;
992
+ stream = stage.packets(stream);
993
+ }
994
+ else if (isFilterAPI(stage)) {
995
+ stream = stage.frames(stream);
996
+ }
997
+ else if (isBitStreamFilterAPI(stage)) {
998
+ metadata.bitStreamFilter = stage;
999
+ stream = stage.packets(stream);
1000
+ }
1001
+ else if (Array.isArray(stage)) {
1002
+ // Chain multiple filters or BSFs
1003
+ for (const filter of stage) {
1004
+ if (isFilterAPI(filter)) {
1005
+ stream = filter.frames(stream);
1006
+ }
1007
+ else if (isBitStreamFilterAPI(filter)) {
1008
+ stream = filter.packets(stream);
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+ // Ensure we're yielding packets
1014
+ for await (const item of stream) {
1015
+ if (isPacket(item) || item === null) {
1016
+ yield item;
1017
+ }
1018
+ else {
1019
+ throw new Error('Named pipeline must end with packets (use encoder after filters)');
1020
+ }
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Consume a stream in parallel (for passthrough pipelines).
1025
+ * Stream index is already added to output.
1026
+ *
1027
+ * @param stream - Stream of packets
1028
+ *
1029
+ * @param output - Media output destination
1030
+ *
1031
+ * @param streamIndex - Pre-allocated stream index in output
1032
+ *
1033
+ * @param shouldStop - Function to check if pipeline should stop
1034
+ *
1035
+ * @param tracker - Optional progress tracker for output statistics
1036
+ *
1037
+ * @internal
1038
+ */
1039
+ export async function consumeStreamInParallel(stream, output, streamIndex, shouldStop, tracker) {
1040
+ // Get iterator to properly clean up on stop
1041
+ const iterator = stream[Symbol.asyncIterator]();
1042
+ try {
1043
+ // Write all packets (including EOF null)
1044
+ while (true) {
1045
+ // Check if we should stop before getting next item
1046
+ if (shouldStop()) {
1047
+ break;
1048
+ }
1049
+ const { value: packet, done } = await iterator.next();
1050
+ if (done)
1051
+ break;
1052
+ try {
1053
+ await output.writePacket(packet, streamIndex);
1054
+ if (packet !== null) {
1055
+ tracker?.record(packet);
1056
+ }
1057
+ }
1058
+ finally {
1059
+ // Free the packet after use (but not null)
1060
+ if (packet && typeof packet.free === 'function') {
1061
+ packet.free();
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ finally {
1067
+ // Always clean up the generator to prevent memory leaks
1068
+ if (iterator.return) {
1069
+ await iterator.return(undefined);
1070
+ }
1071
+ }
1072
+ // Note: Don't close output here - it will be closed by the caller after all streams finish
1073
+ }
1074
+ /**
1075
+ * Marks a pull that lost the race against the stop request.
1076
+ *
1077
+ * @internal
1078
+ */
1079
+ const FRAME_SOURCE_STOPPED = Symbol('frameSourceStopped');
1080
+ /**
1081
+ * Wrap a caller-supplied frame source so a stop request unblocks a stalled pull.
1082
+ *
1083
+ * A demuxer-backed pipeline unwinds promptly because stop() interrupts the
1084
+ * blocking read. A frame source offers no such lever: the consuming loop only
1085
+ * notices the stop request once the source yields again, so a source that goes
1086
+ * silent (a wedged capture device, a generator awaiting an event that never
1087
+ * fires) keeps the pipeline - and with it teardown - waiting forever. Calling
1088
+ * `return()` on the source does not help, because an async generator queues
1089
+ * that request behind the pending `next()` that is already stuck.
1090
+ *
1091
+ * Racing every pull against `stopped` supplies the missing lever.
1092
+ *
1093
+ * @param source - Caller-supplied frame source
1094
+ *
1095
+ * @param stopped - Resolves once teardown has been requested
1096
+ *
1097
+ * @yields {Frame | null} Frames from the source until it ends or teardown starts
1098
+ *
1099
+ * @internal
1100
+ */
1101
+ export async function* interruptibleFrameSource(source, stopped) {
1102
+ const iterator = source[Symbol.asyncIterator]();
1103
+ const stopRequested = stopped.then(() => FRAME_SOURCE_STOPPED);
1104
+ try {
1105
+ for (;;) {
1106
+ const result = await Promise.race([iterator.next(), stopRequested]);
1107
+ if (result === FRAME_SOURCE_STOPPED || result.done) {
1108
+ return;
1109
+ }
1110
+ yield result.value;
1111
+ }
1112
+ }
1113
+ finally {
1114
+ // Deliberately not awaited: a stalled source never settles the pending
1115
+ // next(), and return() would queue behind it - reintroducing the very hang
1116
+ // this wrapper exists to prevent.
1117
+ void (async () => {
1118
+ try {
1119
+ await iterator.return?.(undefined);
1120
+ }
1121
+ catch {
1122
+ // The source failed while winding down; teardown proceeds regardless.
1123
+ }
1124
+ })();
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Consume a named stream and write to output.
1129
+ *
1130
+ * @param stream - Stream of packets
1131
+ *
1132
+ * @param output - Media output destination
1133
+ *
1134
+ * @param metadata - Stream metadata
1135
+ *
1136
+ * @param shouldStop - Function to check if pipeline should stop
1137
+ *
1138
+ * @param tracker - Optional progress tracker for output statistics
1139
+ *
1140
+ * @internal
1141
+ */
1142
+ async function consumeNamedStream(stream, output, metadata, shouldStop, tracker) {
1143
+ // Add stream to output
1144
+ let streamIndex = 0;
1145
+ if (metadata.encoder) {
1146
+ // Encoding path
1147
+ if (metadata.decoder) {
1148
+ // Have decoder - use its stream for metadata/properties
1149
+ const originalStream = metadata.decoder.getStream();
1150
+ streamIndex = output.addStream(originalStream, { encoder: metadata.encoder, bsf: metadata.bitStreamFilter });
1151
+ }
1152
+ else {
1153
+ // Encoder-only mode (e.g., frame generator) - no input stream
1154
+ streamIndex = output.addStream(metadata.encoder, { bsf: metadata.bitStreamFilter });
1155
+ }
1156
+ }
1157
+ else if (metadata.decoder) {
1158
+ // Stream copy - use decoder's original stream
1159
+ const originalStream = metadata.decoder.getStream();
1160
+ streamIndex = output.addStream(originalStream);
1161
+ }
1162
+ else if (metadata.bitStreamFilter) {
1163
+ // BSF - use BSF's original stream
1164
+ const originalStream = metadata.bitStreamFilter.getStream();
1165
+ streamIndex = output.addStream(originalStream);
1166
+ }
1167
+ else if (metadata.demuxer) {
1168
+ // Passthrough from Demuxer - use type hint from metadata
1169
+ const inputStream = metadata.type === 'video' ? metadata.demuxer.video() : metadata.demuxer.audio();
1170
+ if (!inputStream) {
1171
+ throw new Error(`No ${metadata.type} stream found in Demuxer`);
1172
+ }
1173
+ streamIndex = output.addStream(inputStream);
1174
+ }
1175
+ else {
1176
+ // This should not happen with the new API
1177
+ throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
1178
+ }
1179
+ // Store for later use
1180
+ metadata.streamIndex = streamIndex;
1181
+ // Get iterator to properly clean up on stop
1182
+ const iterator = stream[Symbol.asyncIterator]();
1183
+ try {
1184
+ // Write all packets (including EOF null)
1185
+ while (true) {
1186
+ // Check if we should stop before getting next item
1187
+ if (shouldStop()) {
1188
+ break;
1189
+ }
1190
+ const { value: packet, done } = await iterator.next();
1191
+ if (done)
1192
+ break;
1193
+ try {
1194
+ await output.writePacket(packet, streamIndex);
1195
+ if (packet !== null) {
1196
+ tracker?.record(packet);
1197
+ }
1198
+ }
1199
+ finally {
1200
+ // Free the packet after use (but not null)
1201
+ if (packet && typeof packet.free === 'function') {
1202
+ packet.free();
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ finally {
1208
+ // Always clean up the generator to prevent memory leaks
1209
+ if (iterator.return) {
1210
+ await iterator.return(undefined);
1211
+ }
1212
+ }
1213
+ // Note: Output is closed by runNamedPipelineAsync once every stream feeding
1214
+ // it has finished (shared outputs are closed exactly once).
1215
+ }
1216
+ // ============================================================================
1217
+ // Type Guards
1218
+ // ============================================================================
1219
+ /**
1220
+ * Check if object is named inputs.
1221
+ *
1222
+ * @param obj - Object to check
1223
+ *
1224
+ * @returns True if object is NamedInputs
1225
+ *
1226
+ * @internal
1227
+ */
1228
+ function isNamedInputs(obj) {
1229
+ return obj && typeof obj === 'object' && !Array.isArray(obj) && !isAsyncIterable(obj) && !isDemuxer(obj);
1230
+ }
1231
+ /**
1232
+ * Check if object is named stages.
1233
+ *
1234
+ * @param obj - Object to check
1235
+ *
1236
+ * @returns True if object is NamedStages
1237
+ *
1238
+ * @internal
1239
+ */
1240
+ function isNamedStages(obj) {
1241
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1242
+ return false;
1243
+ }
1244
+ // Check if object has at least one stream name key (video or audio)
1245
+ const keys = Object.keys(obj);
1246
+ return keys.length > 0 && keys.every((key) => key === 'video' || key === 'audio');
1247
+ }
1248
+ /**
1249
+ * Check if object is async iterable.
1250
+ *
1251
+ * @param obj - Object to check
1252
+ *
1253
+ * @returns True if object is AsyncIterable
1254
+ *
1255
+ * @internal
1256
+ */
1257
+ function isAsyncIterable(obj) {
1258
+ return obj && typeof obj[Symbol.asyncIterator] === 'function';
1259
+ }
1260
+ /**
1261
+ * Check if object is Demuxer.
1262
+ *
1263
+ * @param obj - Object to check
1264
+ *
1265
+ * @returns True if object is Demuxer
1266
+ *
1267
+ * @internal
1268
+ */
1269
+ function isDemuxer(obj) {
1270
+ return obj && typeof obj.packets === 'function' && typeof obj.video === 'function' && typeof obj.audio === 'function';
1271
+ }
1272
+ /**
1273
+ * Check if object is Decoder.
1274
+ *
1275
+ * @param obj - Object to check
1276
+ *
1277
+ * @returns True if object is Decoder
1278
+ *
1279
+ * @internal
1280
+ */
1281
+ function isDecoder(obj) {
1282
+ return obj && typeof obj.decode === 'function' && typeof obj.flush === 'function';
1283
+ }
1284
+ /**
1285
+ * Check if object is Encoder.
1286
+ *
1287
+ * @param obj - Object to check
1288
+ *
1289
+ * @returns True if object is Encoder
1290
+ *
1291
+ * @internal
1292
+ */
1293
+ function isEncoder(obj) {
1294
+ return obj && typeof obj.encode === 'function' && typeof obj.flush === 'function';
1295
+ }
1296
+ /**
1297
+ * Check if object is FilterAPI.
1298
+ *
1299
+ * @param obj - Object to check
1300
+ *
1301
+ * @returns True if object is FilterAPI
1302
+ *
1303
+ * @internal
1304
+ */
1305
+ function isFilterAPI(obj) {
1306
+ return obj && typeof obj.process === 'function' && typeof obj.receive === 'function';
1307
+ }
1308
+ /**
1309
+ * Check if object is BitStreamFilterAPI.
1310
+ *
1311
+ * @param obj - Object to check
1312
+ *
1313
+ * @returns True if object is BitStreamFilterAPI
1314
+ *
1315
+ * @internal
1316
+ */
1317
+ function isBitStreamFilterAPI(obj) {
1318
+ return obj && typeof obj.filter === 'function' && typeof obj.flushPackets === 'function' && typeof obj.reset === 'function';
1319
+ }
1320
+ /**
1321
+ * Check if object is Muxer.
1322
+ *
1323
+ * @param obj - Object to check
1324
+ *
1325
+ * @returns True if object is Muxer
1326
+ *
1327
+ * @internal
1328
+ */
1329
+ function isMuxer(obj) {
1330
+ return obj && typeof obj.writePacket === 'function' && typeof obj.addStream === 'function';
1331
+ }
1332
+ /**
1333
+ * Check if object is Packet.
1334
+ *
1335
+ * @param obj - Object to check
1336
+ *
1337
+ * @returns True if object is Packet
1338
+ *
1339
+ * @internal
1340
+ */
1341
+ function isPacket(obj) {
1342
+ return obj && 'streamIndex' in obj && 'pts' in obj && 'dts' in obj;
1343
+ }
1344
+ //# sourceMappingURL=pipeline.js.map