node-av-lgpl 0.0.0 → 7.1.0-beta.2

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 +384 -0
  283. package/dist/lib/stream.js +525 -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,1131 @@
1
+ import { isRtcp, RtpPacket } from 'werift';
2
+ import { AV_CODEC_ID_HEVC, AV_HWDEVICE_TYPE_NONE, AV_SAMPLE_FMT_FLTP } from '../constants/constants.js';
3
+ import { FF_ENCODER_LIBOPUS, FF_ENCODER_LIBX264, FF_ENCODER_LIBX265 } from '../constants/encoders.js';
4
+ import { Codec } from '../lib/codec.js';
5
+ import { Rational } from '../lib/rational.js';
6
+ import { avChannelLayoutDefault } from '../lib/utilities.js';
7
+ import { BitStreamFilterAPI } from './bitstream-filter.js';
8
+ import { MAX_PACKET_SIZE } from './constants.js';
9
+ import { Demuxer } from './demuxer.js';
10
+ import { Encoder } from './encoder.js';
11
+ import { FilterPreset } from './filter-presets.js';
12
+ import { FilterAPI } from './filter.js';
13
+ import { HardwareContext } from './hardware.js';
14
+ import { Muxer } from './muxer.js';
15
+ import { interruptibleFrameSource, pipeline } from './pipeline.js';
16
+ import { openAudioDecoder } from './utilities/audio-transcode.js';
17
+ import { pickSupportedLayout, pickSupportedRate, pickSupportedSampleFormat } from './utilities/codec-format.js';
18
+ import { configureLiveDecoderThreading } from './utilities/live-decoding.js';
19
+ import { transcodeScale } from './utilities/scale-size.js';
20
+ import { applyVideoBitrate, createVideoTranscodeFilter, findSoftwareEncoder, findUsableHardwareEncoder, openTranscodeDecoder } from './utilities/video-transcode.js';
21
+ /**
22
+ * Check whether a streaming-class input is a frame source (not a URL or Demuxer).
23
+ *
24
+ * @param input - The input passed to `create()`
25
+ *
26
+ * @returns True if `input` is a {@link MediaFrameSource}
27
+ *
28
+ * @internal
29
+ */
30
+ export function isMediaFrameSource(input) {
31
+ return typeof input !== 'string' && !(input instanceof Demuxer);
32
+ }
33
+ /**
34
+ * Combine one or two pipeline controls behind a single {@link PipelineControl}.
35
+ *
36
+ * The frame-source path runs separate video/audio pipelines; this presents them
37
+ * as one control so `stop()`/`completion` work uniformly.
38
+ *
39
+ * @internal
40
+ */
41
+ class CombinedPipelineControl {
42
+ controls;
43
+ constructor(controls) {
44
+ this.controls = controls;
45
+ }
46
+ stop() {
47
+ for (const control of this.controls) {
48
+ control.stop();
49
+ }
50
+ }
51
+ isStopped() {
52
+ return this.controls.every((control) => control.isStopped());
53
+ }
54
+ get completion() {
55
+ return Promise.all(this.controls.map((control) => control.completion)).then(() => undefined);
56
+ }
57
+ get progress() {
58
+ // Delegate to the first control (video when present) - a merged view is not
59
+ // meaningful across independently-clocked streams.
60
+ return this.controls[0]?.progress ?? { frames: 0, bytes: 0, time: 0, fps: 0, bitrate: 0, speed: 0, elapsed: 0 };
61
+ }
62
+ }
63
+ /**
64
+ * Generic RTP streaming with automatic codec detection and transcoding.
65
+ *
66
+ * Provides library-agnostic RTP streaming for various applications.
67
+ * Automatically detects input codecs and transcodes non-compatible formats.
68
+ * Supports hardware acceleration for video transcoding.
69
+ * Essential component for building RTP streaming servers.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * import { RTPStream } from 'node-av/api';
74
+ *
75
+ * // Create stream with RTP packet callbacks
76
+ * const stream = RTPStream.create('rtsp://camera.local/stream', {
77
+ * mtu: 1200,
78
+ * hardware: 'auto',
79
+ * onVideoPacket: (rtp) => {
80
+ * // Send RTP packet
81
+ * sendRtpPacket(rtp);
82
+ * },
83
+ * onAudioPacket: (rtp) => {
84
+ * sendRtpPacket(rtp);
85
+ * }
86
+ * });
87
+ *
88
+ * // Start streaming
89
+ * await stream.start();
90
+ * ```
91
+ *
92
+ * @see {@link Demuxer} For input media handling
93
+ * @see {@link HardwareContext} For GPU acceleration
94
+ */
95
+ export class RTPStream {
96
+ options;
97
+ inputUrl;
98
+ inputOptions;
99
+ input;
100
+ source;
101
+ videoOutput;
102
+ audioOutput;
103
+ hardwareContext;
104
+ videoDecoder;
105
+ videoFilter;
106
+ videoEncoder;
107
+ videoBitstreamFilter;
108
+ audioDecoder;
109
+ audioFilter;
110
+ audioEncoder;
111
+ pipeline;
112
+ startPromise;
113
+ signal;
114
+ supportedVideoCodecs;
115
+ supportedAudioCodecs;
116
+ abortHandler;
117
+ stopRequested = false;
118
+ stopPromise;
119
+ startAbort;
120
+ stopSignal = Promise.withResolvers();
121
+ /**
122
+ * @param input - Media input URL or pre-opened Demuxer
123
+ *
124
+ * @param options - Stream configuration options
125
+ *
126
+ * Use {@link create} factory method
127
+ *
128
+ * @internal
129
+ */
130
+ constructor(input, options) {
131
+ if (typeof input === 'string') {
132
+ this.inputUrl = input;
133
+ }
134
+ else if (input instanceof Demuxer) {
135
+ this.inputUrl = null;
136
+ this.input = input;
137
+ }
138
+ else {
139
+ this.inputUrl = null;
140
+ this.source = input;
141
+ }
142
+ const inputUrl = this.inputUrl ?? '';
143
+ this.inputOptions = {
144
+ ...options.inputOptions,
145
+ options: {
146
+ flags: 'low_delay',
147
+ // fflags: 'nobuffer',
148
+ analyzeduration: 0,
149
+ probesize: 32,
150
+ timeout: 10000000,
151
+ rtsp_transport: inputUrl.toLowerCase().startsWith('rtsp') ? 'tcp' : undefined,
152
+ ...options.inputOptions?.options,
153
+ },
154
+ };
155
+ this.signal = options.signal;
156
+ // If no supported codecs specified, empty set means ALL codecs are supported (passthrough only)
157
+ this.supportedVideoCodecs = new Set(options.supportedVideoCodecs?.filter(Boolean));
158
+ this.supportedAudioCodecs = new Set(options.supportedAudioCodecs?.filter(Boolean));
159
+ this.options = {
160
+ onVideoPacket: options.onVideoPacket ?? (() => { }),
161
+ onAudioPacket: options.onAudioPacket ?? (() => { }),
162
+ onClose: options.onClose ?? (() => { }),
163
+ supportedVideoCodecs: Array.from(this.supportedVideoCodecs),
164
+ supportedAudioCodecs: Array.from(this.supportedAudioCodecs),
165
+ hardware: options.hardware ?? { deviceType: AV_HWDEVICE_TYPE_NONE },
166
+ inputOptions: options.inputOptions,
167
+ video: {
168
+ ssrc: options.video?.ssrc,
169
+ payloadType: options.video?.payloadType,
170
+ mtu: options.video?.mtu ?? MAX_PACKET_SIZE,
171
+ fps: options.video?.fps,
172
+ width: options.video?.width,
173
+ height: options.video?.height,
174
+ bitrate: options.video?.bitrate,
175
+ dumpExtra: options.video?.dumpExtra,
176
+ transcode: options.video?.transcode,
177
+ encoderOptions: options.video?.encoderOptions ?? {},
178
+ },
179
+ audio: {
180
+ ssrc: options.audio?.ssrc,
181
+ payloadType: options.audio?.payloadType,
182
+ mtu: options.audio?.mtu ?? MAX_PACKET_SIZE,
183
+ sampleRate: options.audio?.sampleRate,
184
+ channels: options.audio?.channels,
185
+ transcode: options.audio?.transcode,
186
+ encoderOptions: options.audio?.encoderOptions,
187
+ sampleFormat: options.audio?.sampleFormat,
188
+ },
189
+ };
190
+ }
191
+ /**
192
+ * Create an RTP stream from a media source.
193
+ *
194
+ * Configures the stream with input URL and options. The input is not opened
195
+ * until start() is called, allowing the stream to be reused after stop().
196
+ *
197
+ * @param input - Media source URL (RTSP, file path, HTTP, etc.) or a pre-opened {@link Demuxer}
198
+ *
199
+ * @param options - Stream configuration options
200
+ *
201
+ * @returns Configured RTP stream instance
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // Stream from RTSP camera
206
+ * const stream = RTPStream.create('rtsp://camera.local/stream', {
207
+ * mtu: 1200,
208
+ * onVideoPacket: (rtp) => sendPacket(rtp),
209
+ * onAudioPacket: (rtp) => sendPacket(rtp)
210
+ * });
211
+ * ```
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * // Stream file with auto hardware acceleration
216
+ * const stream = RTPStream.create('video.mp4', {
217
+ * hardware: 'auto'
218
+ * });
219
+ * ```
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * // Stream from a pre-opened Demuxer (e.g. Device.openScreen())
224
+ * const demuxer = await Device.openScreen({ frameRate: 30 });
225
+ * const stream = RTPStream.create(demuxer, {
226
+ * hardware: 'auto',
227
+ * onVideoPacket: (rtp) => sendPacket(rtp)
228
+ * });
229
+ * ```
230
+ */
231
+ static create(input, options = {}) {
232
+ return new RTPStream(input, options);
233
+ }
234
+ /**
235
+ * Check if the stream is active.
236
+ *
237
+ * Stays true while a requested stop is still unwinding: the pipeline reports
238
+ * itself stopped as soon as stop() is called, but its packets keep flowing
239
+ * and its native resources stay open until completion settles.
240
+ *
241
+ * @returns True if the stream is active, false otherwise
242
+ */
243
+ get isStreamActive() {
244
+ return this.pipeline !== undefined;
245
+ }
246
+ /**
247
+ * Get the demuxer instance.
248
+ *
249
+ * Used for accessing the underlying demuxer.
250
+ * Only available after start() is called.
251
+ *
252
+ * @returns Demuxer instance or undefined if not started
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const stream = RTPStream.create('input.mp4', {
257
+ * onVideoPacket: (rtp) => sendRtp(rtp)
258
+ * });
259
+ * await stream.start();
260
+ * const input = stream.getInput();
261
+ * console.log('Bitrate:', input?.bitRate);
262
+ * ```
263
+ */
264
+ getInput() {
265
+ return this.input;
266
+ }
267
+ /**
268
+ * Request that the next encoded video frame is a keyframe.
269
+ *
270
+ * Used to answer receiver feedback (e.g. a WebRTC PLI) so a receiver that
271
+ * missed the stream start or lost packets can resync without waiting for the
272
+ * next scheduled GOP keyframe. Effective whenever the video is encoded, from
273
+ * frame sources and from a transcoded URL or {@link Demuxer} input; a stream
274
+ * passed through keeps the source's keyframes.
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * transceiver.sender.onPictureLossIndication.subscribe(() => {
279
+ * stream.requestVideoKeyframe();
280
+ * });
281
+ * ```
282
+ */
283
+ requestVideoKeyframe() {
284
+ this.videoEncoder?.requestKeyframe();
285
+ }
286
+ /**
287
+ * Start streaming media to RTP packets.
288
+ *
289
+ * Begins the media processing pipeline, reading packets from input,
290
+ * transcoding if necessary, and invoking RTP packet callbacks.
291
+ * Automatically handles video and audio streams in parallel.
292
+ * This method returns immediately after starting the pipeline.
293
+ *
294
+ * @returns Promise that resolves when pipeline is started
295
+ *
296
+ * @throws {Error} If no video stream found in input
297
+ *
298
+ * @throws {FFmpegError} If setup fails
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * const stream = RTPStream.create('rtsp://camera.local/stream', {
303
+ * onVideoPacket: (rtp) => sendRtp(rtp)
304
+ * });
305
+ *
306
+ * // Start streaming (returns immediately)
307
+ * await stream.start();
308
+ *
309
+ * // Later: stop streaming
310
+ * await stream.stop();
311
+ * ```
312
+ */
313
+ async start() {
314
+ // Idempotent: concurrent and repeated calls share a single startup (e.g.
315
+ // an explicit start() racing WebRTCStream.setOffer()'s automatic one).
316
+ this.startPromise ??= this.doStart().catch((error) => {
317
+ this.startPromise = undefined;
318
+ throw error;
319
+ });
320
+ await this.startPromise;
321
+ }
322
+ /**
323
+ * Perform the actual startup (see {@link start}).
324
+ *
325
+ * @internal
326
+ */
327
+ async doStart() {
328
+ // A start racing an in-flight stop() waits for the teardown to finish,
329
+ // otherwise it would build the pipeline on top of freed resources.
330
+ if (this.stopPromise) {
331
+ await this.stopPromise;
332
+ }
333
+ if (this.pipeline) {
334
+ return;
335
+ }
336
+ this.signal?.throwIfAborted();
337
+ if (this.signal && !this.abortHandler) {
338
+ // Stop on abort. Keep a reference so stop() can detach the listener again,
339
+ // otherwise restarts would stack one listener per start(). Errors from the
340
+ // async stop() are swallowed here; they already surface via onClose(error).
341
+ this.abortHandler = () => {
342
+ this.abortHandler = undefined;
343
+ this.stop().catch(() => { });
344
+ };
345
+ this.signal.addEventListener('abort', this.abortHandler, { once: true });
346
+ }
347
+ // Re-armed per start so a restart is not torn down by the previous run.
348
+ this.stopSignal = Promise.withResolvers();
349
+ // Lets stop() interrupt a startup that is blocked in the input open
350
+ // (an RTSP connect can take seconds).
351
+ this.startAbort = new AbortController();
352
+ // Frame-source path: encode pre-composited frames directly (no demuxer/decoder).
353
+ if (this.source) {
354
+ await this.startFromFrames();
355
+ return;
356
+ }
357
+ if (!this.input) {
358
+ if (!this.inputUrl) {
359
+ throw new Error('No input URL or Demuxer provided');
360
+ }
361
+ const openSignal = this.signal ? AbortSignal.any([this.signal, this.startAbort.signal]) : this.startAbort.signal;
362
+ this.input = await Demuxer.open(this.inputUrl, { ...this.inputOptions, signal: openSignal });
363
+ }
364
+ this.throwIfStopRequested();
365
+ const videoStream = this.input.video();
366
+ const audioStream = this.input.audio();
367
+ let videoOutputIsHevc = videoStream?.codecpar.codecId === AV_CODEC_ID_HEVC;
368
+ // Setup video transcoding if needed
369
+ if (videoStream && (this.options.video.transcode || !this.isVideoCodecSupported(videoStream.codecpar.codecId))) {
370
+ // Check if we need hardware acceleration
371
+ if (this.options.hardware === 'auto') {
372
+ this.hardwareContext = HardwareContext.auto();
373
+ }
374
+ else if (this.options.hardware.deviceType !== AV_HWDEVICE_TYPE_NONE) {
375
+ this.hardwareContext = HardwareContext.create(this.options.hardware.deviceType, this.options.hardware.device, this.options.hardware.options);
376
+ }
377
+ const currentFps = videoStream.avgFrameRate.num / videoStream.avgFrameRate.den;
378
+ const needsFps = this.options.video.fps !== undefined && isFinite(currentFps) && this.options.video.fps !== currentFps;
379
+ // Get first supported codec
380
+ const targetCodecId = this.options.supportedVideoCodecs[0];
381
+ if (!targetCodecId) {
382
+ throw new Error('No supported video codec specified for transcoding');
383
+ }
384
+ const { codecpar } = videoStream;
385
+ const scale = transcodeScale(codecpar.width, codecpar.height, this.options.video.width, this.options.video.height);
386
+ // a GPU encodes up to a size of its own, test it with the size it will get
387
+ const encoderCodec = await this.findVideoEncoder(targetCodecId, scale.output);
388
+ if (!encoderCodec) {
389
+ throw new Error(`No encoder found for codec ID ${targetCodecId}`);
390
+ }
391
+ this.throwIfStopRequested();
392
+ this.videoDecoder = await openTranscodeDecoder(videoStream, this.hardwareContext, {
393
+ exitOnError: false,
394
+ // Frame threading holds back one frame per thread before the first output
395
+ configure: configureLiveDecoderThreading,
396
+ });
397
+ // The graph follows the frames: whether they come from the device and in
398
+ // which format shows only once they are decoded
399
+ this.videoFilter = createVideoTranscodeFilter({
400
+ ...scale.target,
401
+ fps: needsFps ? this.options.video.fps : undefined,
402
+ encoder: encoderCodec,
403
+ hardware: this.hardwareContext,
404
+ });
405
+ const encoderOptions = liveVideoEncoderOptions(encoderCodec.name, this.options.video.encoderOptions);
406
+ const bitrate = this.options.video.bitrate;
407
+ this.videoEncoder = await Encoder.create(encoderCodec, {
408
+ decoder: this.videoDecoder,
409
+ filter: this.videoFilter,
410
+ maxBFrames: 0,
411
+ options: encoderOptions,
412
+ configure: (ctx) => {
413
+ // encoder defaults are 120 (VAAPI) or 250 frames (libx264, NVENC), a receiver that lost a packet waits that long;
414
+ // after the short probe the input rate is often unknown, the context has it from the frames
415
+ const fps = ctx.framerate.num > 0 ? ctx.framerate.num / ctx.framerate.den : 30;
416
+ ctx.gopSize = Math.max(1, Math.round(fps * 2));
417
+ applyVideoBitrate(ctx, bitrate);
418
+ },
419
+ });
420
+ this.throwIfStopRequested();
421
+ videoOutputIsHevc = encoderCodec.id === AV_CODEC_ID_HEVC;
422
+ }
423
+ if (videoStream && videoOutputIsHevc && this.options.video?.dumpExtra) {
424
+ this.videoBitstreamFilter = BitStreamFilterAPI.create('dump_extra', this.videoEncoder ?? videoStream, { options: { freq: 'k' } });
425
+ }
426
+ // Setup video output
427
+ this.videoOutput = await this.createVideoOutput();
428
+ this.throwIfStopRequested();
429
+ // Setup audio if available and needs transcoding
430
+ if (audioStream && (this.options.audio.transcode || !this.isAudioCodecSupported(audioStream.codecpar.codecId))) {
431
+ this.audioDecoder = await openAudioDecoder(audioStream, {
432
+ exitOnError: false,
433
+ });
434
+ // Get first supported audio codec
435
+ const targetCodecId = this.options.supportedAudioCodecs[0];
436
+ if (!targetCodecId) {
437
+ throw new Error('No supported audio codec specified for transcoding');
438
+ }
439
+ let encoderCodec = null;
440
+ if (typeof targetCodecId === 'string') {
441
+ encoderCodec = Codec.findEncoderByName(targetCodecId);
442
+ }
443
+ else {
444
+ encoderCodec = Codec.findEncoder(targetCodecId);
445
+ }
446
+ if (!encoderCodec) {
447
+ throw new Error(`No encoder found for codec ID ${targetCodecId}`);
448
+ }
449
+ // Determine target audio parameters from options
450
+ const desiredSampleFormat = this.options.audio?.sampleFormat ?? audioStream.codecpar.format;
451
+ const desiredSampleRate = this.options.audio?.sampleRate ?? (audioStream.codecpar.sampleRate > 0 ? audioStream.codecpar.sampleRate : 48000);
452
+ const desiredChannels = this.options.audio?.channels ?? (audioStream.codecpar.channels > 0 ? audioStream.codecpar.channels : 2);
453
+ // Select best supported parameters from codec
454
+ const targetSampleFormat = this.selectSampleFormat(encoderCodec, desiredSampleFormat);
455
+ const targetSampleRate = this.selectSampleRate(encoderCodec, desiredSampleRate);
456
+ const channelLayoutStr = this.selectChannelLayout(encoderCodec, desiredChannels);
457
+ // Create audio filter for resampling
458
+ const filterChain = FilterPreset.chain().aformat(targetSampleFormat, targetSampleRate, channelLayoutStr).build();
459
+ // FilterAPI now auto-calculates timeBase from first frame (FFmpeg behavior)
460
+ this.audioFilter = FilterAPI.create(filterChain);
461
+ let encoderOptions = {};
462
+ if (encoderCodec.name === FF_ENCODER_LIBOPUS) {
463
+ encoderOptions.application = 'lowdelay';
464
+ encoderOptions.frame_duration = 20;
465
+ }
466
+ encoderOptions = {
467
+ ...encoderOptions,
468
+ ...this.options.audio.encoderOptions,
469
+ };
470
+ this.audioEncoder = await Encoder.create(encoderCodec, {
471
+ decoder: this.audioDecoder,
472
+ filter: this.audioFilter,
473
+ options: encoderOptions,
474
+ // Silence for an input gap would only go out once the input resumes, as a burst of
475
+ // late packets; receivers conceal a timestamp gap like packet loss instead
476
+ audioFrameBuffer: { maxGapFill: 0 },
477
+ });
478
+ this.throwIfStopRequested();
479
+ }
480
+ // Setup audio output if available
481
+ if (audioStream) {
482
+ this.audioOutput = await this.createAudioOutput();
483
+ this.throwIfStopRequested();
484
+ }
485
+ // Start pipeline in background (don't await)
486
+ this.attachCompletion(this.runPipeline());
487
+ }
488
+ /**
489
+ * Abort an in-flight startup once stop() was requested.
490
+ *
491
+ * Checked after every await in the startup path: the resources created so
492
+ * far are all assigned to instance fields, so the stop() that is waiting on
493
+ * this startup tears them down the moment it unwinds.
494
+ *
495
+ * @throws {Error} If stop() was called while starting
496
+ *
497
+ * @internal
498
+ */
499
+ throwIfStopRequested() {
500
+ if (this.stopRequested) {
501
+ throw new Error('RTPStream stopped during start');
502
+ }
503
+ }
504
+ /**
505
+ * Wire the pipeline completion to the onClose/error callbacks.
506
+ *
507
+ * On success invokes `onClose()`; on error stops the stream and invokes
508
+ * `onClose(error)`. Either way `onClose` runs exactly once and the detached
509
+ * chain never rejects. Runs in the background (not awaited).
510
+ *
511
+ * @param completion - The pipeline completion promise
512
+ *
513
+ * @internal
514
+ */
515
+ attachCompletion(completion) {
516
+ // Captured so a restart that installed a newer pipeline keeps it.
517
+ const control = this.pipeline;
518
+ // One then() with both handlers: a throwing onClose() in the success path
519
+ // must not fall through into the error path and fire onClose a second time.
520
+ completion
521
+ .then(() => {
522
+ // Pipeline completed successfully
523
+ this.emitClose();
524
+ }, async (reason) => {
525
+ const error = reason instanceof Error ? reason : new Error(String(reason));
526
+ // A stop() in flight or an aborted signal means the owner is tearing
527
+ // the stream down - the pipeline unwinding with an AbortError (or an
528
+ // interrupted FFmpeg call) is the expected shutdown path, not a failure.
529
+ // Decided before the stop() below, which would make every failure look deliberate.
530
+ const deliberate = this.isDeliberateStop(reason);
531
+ if (!deliberate) {
532
+ console.error('[RTPStream] Pipeline error:', error);
533
+ }
534
+ // A teardown failure must not swallow the onClose call: without it
535
+ // the owner never learns the stream is gone.
536
+ try {
537
+ await this.stop();
538
+ }
539
+ catch {
540
+ // doStop() already released whatever it could.
541
+ }
542
+ this.emitClose(deliberate ? undefined : error);
543
+ })
544
+ .finally(() => {
545
+ // Drop the finished pipeline so isStreamActive reports inactive. The
546
+ // demuxer path already clears it in runPipeline(); a frame source has
547
+ // no such step, so without this it would look active forever.
548
+ if (this.pipeline === control) {
549
+ this.pipeline = undefined;
550
+ }
551
+ });
552
+ }
553
+ /**
554
+ * Invoke the owner's onClose callback.
555
+ *
556
+ * The completion chain runs detached, so an exception from the callback
557
+ * could only surface as an unhandled rejection - it is logged instead.
558
+ *
559
+ * @param error - Pipeline error, or undefined for a clean end
560
+ *
561
+ * @internal
562
+ */
563
+ emitClose(error) {
564
+ try {
565
+ this.options.onClose(error);
566
+ }
567
+ catch (callbackError) {
568
+ // The stream is already closed; logged so a broken restart path in the
569
+ // owner does not vanish silently.
570
+ console.error('[RTPStream] onClose callback threw:', callbackError);
571
+ }
572
+ }
573
+ /**
574
+ * Whether a pipeline rejection was caused by an intentional shutdown.
575
+ *
576
+ * @param error - The pipeline rejection reason
577
+ *
578
+ * @returns True when stop() is in flight or the owner's signal aborted
579
+ *
580
+ * @internal
581
+ */
582
+ isDeliberateStop(error) {
583
+ return this.stopRequested || this.stopPromise !== undefined || this.signal?.aborted === true || (error instanceof Error && error.name === 'AbortError');
584
+ }
585
+ /**
586
+ * Set up encoders and RTP outputs for a pre-composited frame source and start
587
+ * encoding. No demuxer or decoder is involved - the provided frames are encoded
588
+ * straight to RTP. Video and audio (whichever are present) run as separate
589
+ * pipelines combined behind a single control.
590
+ *
591
+ * @throws {Error} If the source has neither video nor audio frames
592
+ *
593
+ * @internal
594
+ */
595
+ async startFromFrames() {
596
+ const source = this.source;
597
+ if (!source.video && !source.audio) {
598
+ throw new Error('Frame source must provide video and/or audio frames');
599
+ }
600
+ const controls = [];
601
+ const opts = this.signal ? { signal: this.signal } : undefined;
602
+ // Video
603
+ if (source.video) {
604
+ if (this.options.hardware === 'auto') {
605
+ this.hardwareContext = HardwareContext.auto();
606
+ }
607
+ else if (this.options.hardware.deviceType !== AV_HWDEVICE_TYPE_NONE) {
608
+ this.hardwareContext = HardwareContext.create(this.options.hardware.deviceType, this.options.hardware.device, this.options.hardware.options);
609
+ }
610
+ const targetCodecId = this.options.supportedVideoCodecs[0] ?? FF_ENCODER_LIBX264;
611
+ const encoderCodec = await this.findVideoEncoder(targetCodecId);
612
+ if (!encoderCodec) {
613
+ throw new Error(`No encoder found for video codec ${String(targetCodecId)}`);
614
+ }
615
+ this.throwIfStopRequested();
616
+ const encoderOptions = liveVideoEncoderOptions(encoderCodec.name, this.options.video.encoderOptions);
617
+ let fps = this.options.video.fps ?? 20;
618
+ if (!isFinite(fps) || fps <= 0 || isNaN(fps)) {
619
+ fps = 20;
620
+ }
621
+ // No decoder/filter: the encoder adopts width/height/pixelFormat/timeBase
622
+ // from the first composited frame. Set the framerate explicitly, otherwise
623
+ // the encoder infers it from the frame timebase (often a fine-grained tick
624
+ // like 1/12800), which makes x264 pick an absurd level (huge MB rate) that
625
+ // browsers can't decode over WebRTC. Bound the GOP to ~2s: live receivers
626
+ // that join (or lose) mid-stream must get a keyframe quickly - the x264
627
+ // default of 250 frames leaves them waiting for many seconds.
628
+ const bitrate = this.options.video.bitrate;
629
+ this.videoEncoder = await Encoder.create(encoderCodec, {
630
+ maxBFrames: 0,
631
+ options: encoderOptions,
632
+ configure: (ctx) => {
633
+ ctx.framerate = new Rational(Math.round(fps), 1);
634
+ ctx.gopSize = Math.max(1, Math.round(fps * 2));
635
+ applyVideoBitrate(ctx, bitrate);
636
+ },
637
+ });
638
+ // Source frames may sit on another device or in a format the encoder rejects
639
+ this.videoFilter = createVideoTranscodeFilter({ width: -1, height: -1, encoder: encoderCodec, hardware: this.hardwareContext });
640
+ this.videoOutput = await this.createVideoOutput();
641
+ this.throwIfStopRequested();
642
+ // a frame source has no demuxer for stop() to interrupt, a silent one would block teardown
643
+ const video = interruptibleFrameSource(source.video, this.stopSignal.promise);
644
+ controls.push(pipeline(video, this.videoFilter, this.videoEncoder, this.videoOutput, opts));
645
+ }
646
+ // Audio
647
+ if (source.audio) {
648
+ const targetCodecId = this.options.supportedAudioCodecs[0] ?? FF_ENCODER_LIBOPUS;
649
+ let encoderCodec = null;
650
+ if (typeof targetCodecId === 'string') {
651
+ encoderCodec = Codec.findEncoderByName(targetCodecId);
652
+ }
653
+ else {
654
+ encoderCodec = Codec.findEncoder(targetCodecId);
655
+ }
656
+ if (!encoderCodec) {
657
+ throw new Error(`No encoder found for audio codec ${String(targetCodecId)}`);
658
+ }
659
+ // There is no input stream to derive formats from - resample to the desired
660
+ // (or codec-preferred) sample format/rate/layout via an aformat filter.
661
+ const desiredSampleFormat = this.options.audio?.sampleFormat ?? AV_SAMPLE_FMT_FLTP;
662
+ const desiredSampleRate = this.options.audio?.sampleRate ?? 48000;
663
+ const desiredChannels = this.options.audio?.channels ?? 2;
664
+ const targetSampleFormat = this.selectSampleFormat(encoderCodec, desiredSampleFormat);
665
+ const targetSampleRate = this.selectSampleRate(encoderCodec, desiredSampleRate);
666
+ const channelLayoutStr = this.selectChannelLayout(encoderCodec, desiredChannels);
667
+ this.audioFilter = FilterAPI.create(FilterPreset.chain().aformat(targetSampleFormat, targetSampleRate, channelLayoutStr).build());
668
+ let encoderOptions = {};
669
+ if (encoderCodec.name === FF_ENCODER_LIBOPUS) {
670
+ encoderOptions.application = 'lowdelay';
671
+ encoderOptions.frame_duration = 20;
672
+ }
673
+ encoderOptions = { ...encoderOptions, ...this.options.audio.encoderOptions };
674
+ this.audioEncoder = await Encoder.create(encoderCodec, {
675
+ filter: this.audioFilter,
676
+ options: encoderOptions,
677
+ audioFrameBuffer: { maxGapFill: 0 },
678
+ });
679
+ this.audioOutput = await this.createAudioOutput();
680
+ this.throwIfStopRequested();
681
+ const audio = interruptibleFrameSource(source.audio, this.stopSignal.promise);
682
+ controls.push(pipeline(audio, this.audioFilter, this.audioEncoder, this.audioOutput, opts));
683
+ }
684
+ this.pipeline = new CombinedPipelineControl(controls);
685
+ this.attachCompletion(this.pipeline.completion);
686
+ }
687
+ /**
688
+ * Create the RTP video output muxer.
689
+ *
690
+ * Rewrites RTP sequence numbers (continuous) and rebases the PTS-derived
691
+ * timestamps (90 kHz) onto a random initial value in the write callback, then
692
+ * forwards each packet to `onVideoPacket`. Uses `this.input` when present
693
+ * (demuxer path) and no input for the frame-source path.
694
+ *
695
+ * @returns The configured video muxer
696
+ *
697
+ * @internal
698
+ */
699
+ async createVideoOutput() {
700
+ let videoSequenceNumber = Math.floor(Math.random() * 0xffff);
701
+ const videoTimestampBase = Math.floor(Math.random() * 0xffffffff) >>> 0; // unsigned 32-bit
702
+ let videoTimestampOffset;
703
+ const videoMuxerOptions = {};
704
+ if (this.options.video.ssrc !== undefined) {
705
+ videoMuxerOptions.ssrc = this.options.video.ssrc;
706
+ }
707
+ if (this.options.video.payloadType !== undefined) {
708
+ videoMuxerOptions.payload_type = this.options.video.payloadType;
709
+ }
710
+ return await Muxer.open({
711
+ write: (buffer) => {
712
+ if (isRtcp(buffer)) {
713
+ // Ignore RTCP packets
714
+ return buffer.length;
715
+ }
716
+ const rtpPacket = RtpPacket.deSerialize(buffer);
717
+ // Fix sequence number - ensure continuous sequence
718
+ rtpPacket.header.sequenceNumber = videoSequenceNumber;
719
+ videoSequenceNumber = (videoSequenceNumber + 1) & 0xffff; // Wrap at 16-bit
720
+ // Rebase the timestamp onto our own random initial value. FFmpeg's RTP
721
+ // muxer derives it from the packet PTS in the 90 kHz clock (plus its own
722
+ // random base), so the deltas carry the real presentation times -
723
+ // including B-frame reordering and VFR cadence. Only the base is swapped;
724
+ // modular uint32 arithmetic keeps wrap-around intact, and packets of the
725
+ // same frame keep sharing a timestamp.
726
+ videoTimestampOffset ??= (videoTimestampBase - rtpPacket.header.timestamp) >>> 0;
727
+ rtpPacket.header.timestamp = (rtpPacket.header.timestamp + videoTimestampOffset) >>> 0;
728
+ this.options.onVideoPacket(rtpPacket);
729
+ return buffer.length;
730
+ },
731
+ }, {
732
+ input: this.input,
733
+ copyInitialNonkeyframes: true,
734
+ format: 'rtp',
735
+ maxPacketSize: this.options.video.mtu,
736
+ options: videoMuxerOptions,
737
+ });
738
+ }
739
+ /**
740
+ * Create the RTP audio output muxer.
741
+ *
742
+ * Rewrites RTP sequence numbers (continuous) in the write callback and forwards
743
+ * each packet to `onAudioPacket`. Uses `this.input` when present (demuxer path)
744
+ * and no input for the frame-source path.
745
+ *
746
+ * @returns The configured audio muxer
747
+ *
748
+ * @internal
749
+ */
750
+ async createAudioOutput() {
751
+ let audioSequenceNumber = Math.floor(Math.random() * 0xffff);
752
+ const audioMuxerOptions = {};
753
+ if (this.options.audio.ssrc !== undefined) {
754
+ audioMuxerOptions.ssrc = this.options.audio.ssrc;
755
+ }
756
+ if (this.options.audio.payloadType !== undefined) {
757
+ audioMuxerOptions.payload_type = this.options.audio.payloadType;
758
+ }
759
+ return await Muxer.open({
760
+ write: (buffer) => {
761
+ if (isRtcp(buffer)) {
762
+ // Ignore RTCP packets
763
+ return buffer.length;
764
+ }
765
+ const rtpPacket = RtpPacket.deSerialize(buffer);
766
+ // Fix sequence number - ensure continuous sequence
767
+ rtpPacket.header.sequenceNumber = audioSequenceNumber;
768
+ audioSequenceNumber = (audioSequenceNumber + 1) & 0xffff; // Wrap at 16-bit
769
+ this.options.onAudioPacket(rtpPacket);
770
+ return buffer.length;
771
+ },
772
+ }, {
773
+ input: this.input,
774
+ copyInitialNonkeyframes: true,
775
+ format: 'rtp',
776
+ maxPacketSize: this.options.audio.mtu,
777
+ options: audioMuxerOptions,
778
+ });
779
+ }
780
+ /**
781
+ * Run the streaming pipeline until completion or stopped.
782
+ *
783
+ * @internal
784
+ */
785
+ async runPipeline() {
786
+ if (!this.input) {
787
+ return;
788
+ }
789
+ const hasVideo = this.input.video() !== undefined && this.videoOutput !== undefined;
790
+ const hasAudio = this.input.audio() !== undefined && this.audioOutput !== undefined;
791
+ const opts = this.signal ? { signal: this.signal } : undefined;
792
+ if (hasAudio && hasVideo) {
793
+ this.pipeline = pipeline(this.input, {
794
+ video: [this.videoDecoder, this.videoFilter, this.videoEncoder, this.videoBitstreamFilter],
795
+ audio: [this.audioDecoder, this.audioFilter, this.audioEncoder],
796
+ }, {
797
+ video: this.videoOutput,
798
+ audio: this.audioOutput,
799
+ }, opts);
800
+ }
801
+ else if (hasVideo) {
802
+ this.pipeline = pipeline(this.input, {
803
+ video: [this.videoDecoder, this.videoFilter, this.videoEncoder, this.videoBitstreamFilter],
804
+ }, this.videoOutput, opts);
805
+ }
806
+ else if (hasAudio) {
807
+ this.pipeline = pipeline(this.input, {
808
+ audio: [this.audioDecoder, this.audioFilter, this.audioEncoder],
809
+ }, this.audioOutput, opts);
810
+ }
811
+ else {
812
+ throw new Error('No audio or video streams found in input');
813
+ }
814
+ await this.pipeline.completion;
815
+ this.pipeline = undefined;
816
+ }
817
+ /**
818
+ * Stop streaming gracefully and clean up all resources.
819
+ *
820
+ * Stops the pipeline, closes output, and releases all FFmpeg resources.
821
+ * Safe to call multiple times. After stopping, you can call start() again
822
+ * to restart the stream. Resolves even if the pipeline had failed; that
823
+ * error is reported through `onClose(error)`.
824
+ *
825
+ * @example
826
+ * ```typescript
827
+ * const stream = RTPStream.create('input.mp4', {
828
+ * onVideoPacket: (rtp) => sendRtp(rtp)
829
+ * });
830
+ * await stream.start();
831
+ *
832
+ * // Stop after 10 seconds
833
+ * setTimeout(async () => await stream.stop(), 10000);
834
+ * ```
835
+ */
836
+ async stop() {
837
+ // Memoized: concurrent stop() calls share one teardown instead of
838
+ // double-closing native resources.
839
+ this.stopPromise ??= this.doStop().finally(() => {
840
+ this.stopPromise = undefined;
841
+ });
842
+ await this.stopPromise;
843
+ }
844
+ /**
845
+ * Perform the actual teardown (see {@link stop}).
846
+ *
847
+ * @internal
848
+ */
849
+ async doStop() {
850
+ // An in-flight start() must unwind before teardown - otherwise it would
851
+ // keep building the pipeline on top of freed resources and leave an
852
+ // orphaned transcode running forever. The flag makes doStart() bail at its
853
+ // next checkpoint, the abort unblocks a startup stuck in the input open.
854
+ this.stopRequested = true;
855
+ this.startAbort?.abort();
856
+ this.startAbort = undefined;
857
+ // Unblocks a frame source that went silent, so the completion barrier below
858
+ // cannot wait on a pull that will never settle.
859
+ this.stopSignal.resolve();
860
+ const inflightStart = this.startPromise;
861
+ // Allow a fresh start() after stopping.
862
+ this.startPromise = undefined;
863
+ if (inflightStart) {
864
+ try {
865
+ await inflightStart;
866
+ }
867
+ catch {
868
+ // Aborted or failed startup - resources are torn down below either way.
869
+ }
870
+ }
871
+ try {
872
+ // Detach the abort listener so it cannot fire for a torn-down session and
873
+ // does not accumulate across start()/stop() cycles.
874
+ if (this.abortHandler) {
875
+ this.signal?.removeEventListener('abort', this.abortHandler);
876
+ this.abortHandler = undefined;
877
+ }
878
+ // Stop pipeline if running and wait for completion. pipeline.stop() interrupts
879
+ // the source demuxer's read (frame-source pipelines unwind via iterator.return
880
+ // instead). Swallow a rejected completion here - the error is already surfaced
881
+ // to the caller via the onClose(error) path; we just need cleanup to proceed.
882
+ const pipeline = this.pipeline;
883
+ if (pipeline) {
884
+ if (!pipeline.isStopped()) {
885
+ pipeline.stop();
886
+ }
887
+ try {
888
+ await pipeline.completion;
889
+ }
890
+ catch {
891
+ // Pipeline errored - proceed with teardown regardless.
892
+ }
893
+ if (this.pipeline === pipeline) {
894
+ this.pipeline = undefined;
895
+ }
896
+ }
897
+ // Close all resources. close() rethrows a failed background write after it
898
+ // freed the muxer. That error already ended the pipeline and reaches the
899
+ // owner through onClose(error), so it must not abort the teardown here and
900
+ // leak the other output, the codecs and the input (an RTSP connection).
901
+ try {
902
+ await this.videoOutput?.close();
903
+ }
904
+ catch {
905
+ // Surfaced through the pipeline completion.
906
+ }
907
+ this.videoOutput = undefined;
908
+ try {
909
+ await this.audioOutput?.close();
910
+ }
911
+ catch {
912
+ // Surfaced through the pipeline completion.
913
+ }
914
+ this.audioOutput = undefined;
915
+ // A codec that refuses to close (still busy) must not leave the input,
916
+ // an RTSP connection, open.
917
+ try {
918
+ this.videoEncoder?.close();
919
+ this.videoEncoder = undefined;
920
+ this.videoFilter?.close();
921
+ this.videoFilter = undefined;
922
+ this.videoDecoder?.close();
923
+ this.videoDecoder = undefined;
924
+ this.audioEncoder?.close();
925
+ this.audioEncoder = undefined;
926
+ this.audioFilter?.close();
927
+ this.audioFilter = undefined;
928
+ this.audioDecoder?.close();
929
+ this.audioDecoder = undefined;
930
+ this.hardwareContext?.dispose();
931
+ this.hardwareContext = undefined;
932
+ }
933
+ finally {
934
+ await this.input?.close();
935
+ this.input = undefined;
936
+ }
937
+ }
938
+ finally {
939
+ this.stopRequested = false;
940
+ }
941
+ }
942
+ /**
943
+ * Check if the given audio codec is supported.
944
+ *
945
+ * @param codecId - The AVCodecID to check
946
+ *
947
+ * @returns True if the codec is supported, false otherwise
948
+ *
949
+ * @internal
950
+ */
951
+ isAudioCodecSupported(codecId) {
952
+ // Empty set means all codecs are supported (passthrough only)
953
+ if (this.supportedAudioCodecs.size === 0) {
954
+ return true;
955
+ }
956
+ const isSupported = this.supportedAudioCodecs.has(codecId);
957
+ if (!isSupported) {
958
+ try {
959
+ const ffEncoderCodecs = Array.from(this.supportedAudioCodecs).filter((c) => typeof c === 'string');
960
+ for (const encoderName of ffEncoderCodecs) {
961
+ const encoderCodec = Codec.findEncoderByName(encoderName);
962
+ if (encoderCodec?.id === codecId) {
963
+ return true;
964
+ }
965
+ }
966
+ }
967
+ catch {
968
+ // Ignore errors
969
+ }
970
+ }
971
+ return isSupported;
972
+ }
973
+ /**
974
+ * Find the encoder for a target video codec, on the device when it works there.
975
+ *
976
+ * @param target - Codec ID, or the name of a specific encoder to use as it is
977
+ *
978
+ * @param size - The output size, when known before the first frame
979
+ *
980
+ * @param size.width - Width
981
+ *
982
+ * @param size.height - Height
983
+ *
984
+ * @returns The encoder, or null if none exists
985
+ *
986
+ * @internal
987
+ */
988
+ async findVideoEncoder(target, size) {
989
+ if (typeof target === 'string') {
990
+ return Codec.findEncoderByName(target);
991
+ }
992
+ return (await findUsableHardwareEncoder(this.hardwareContext, target, this.options.video.encoderOptions, size)) ?? findSoftwareEncoder(target);
993
+ }
994
+ /**
995
+ * Check if the given video codec is supported.
996
+ *
997
+ * @param codecId - The AVCodecID to check
998
+ *
999
+ * @returns True if the codec is supported, false otherwise
1000
+ *
1001
+ * @internal
1002
+ */
1003
+ isVideoCodecSupported(codecId) {
1004
+ // Empty set means all codecs are supported (passthrough only)
1005
+ if (this.supportedVideoCodecs.size === 0) {
1006
+ return true;
1007
+ }
1008
+ const isSupported = this.supportedVideoCodecs.has(codecId);
1009
+ if (!isSupported) {
1010
+ try {
1011
+ const ffEncoderCodecs = Array.from(this.supportedVideoCodecs).filter((c) => typeof c === 'string');
1012
+ for (const encoderName of ffEncoderCodecs) {
1013
+ const encoderCodec = Codec.findEncoderByName(encoderName);
1014
+ if (encoderCodec?.id === codecId) {
1015
+ return true;
1016
+ }
1017
+ }
1018
+ }
1019
+ catch {
1020
+ // Ignore errors
1021
+ }
1022
+ }
1023
+ return isSupported;
1024
+ }
1025
+ /**
1026
+ * Select a codec-supported sample format for the desired one.
1027
+ *
1028
+ * Delegates to the shared {@link pickSupportedSampleFormat} so the RTP path and
1029
+ * the encoder negotiate formats identically.
1030
+ *
1031
+ * @param codec - Audio encoder codec
1032
+ *
1033
+ * @param desiredFormat - Desired sample format
1034
+ *
1035
+ * @returns A sample format the codec accepts
1036
+ *
1037
+ * @internal
1038
+ */
1039
+ selectSampleFormat(codec, desiredFormat) {
1040
+ return pickSupportedSampleFormat(desiredFormat, codec.sampleFormats);
1041
+ }
1042
+ /**
1043
+ * Select a codec-supported sample rate for the desired one.
1044
+ *
1045
+ * Delegates to the shared {@link pickSupportedRate} (keeps the desired rate when
1046
+ * accepted, otherwise the nearest supported one).
1047
+ *
1048
+ * @param codec - Audio encoder codec
1049
+ *
1050
+ * @param desiredRate - Desired sample rate
1051
+ *
1052
+ * @returns A sample rate the codec accepts
1053
+ *
1054
+ * @internal
1055
+ */
1056
+ selectSampleRate(codec, desiredRate) {
1057
+ return pickSupportedRate(desiredRate, codec.supportedSamplerates);
1058
+ }
1059
+ /**
1060
+ * Select a codec-supported channel layout for the desired channel count, as an
1061
+ * SDP layout string.
1062
+ *
1063
+ * Delegates the choice to the shared {@link pickSupportedLayout} (nearest channel
1064
+ * count when the exact count is unsupported), then renders it as `'mono'`,
1065
+ * `'stereo'`, or the layout mask.
1066
+ *
1067
+ * @param codec - Audio encoder codec
1068
+ *
1069
+ * @param desiredChannels - Desired number of channels
1070
+ *
1071
+ * @returns Channel layout string the codec accepts
1072
+ *
1073
+ * @internal
1074
+ */
1075
+ selectChannelLayout(codec, desiredChannels) {
1076
+ const supportedLayouts = codec.channelLayouts;
1077
+ if (!supportedLayouts || supportedLayouts.length === 0) {
1078
+ return desiredChannels === 1 ? 'mono' : 'stereo'; // should normally not happen
1079
+ }
1080
+ const picked = pickSupportedLayout(avChannelLayoutDefault(desiredChannels), supportedLayouts);
1081
+ if (picked.nbChannels === 1)
1082
+ return 'mono';
1083
+ if (picked.nbChannels === 2)
1084
+ return 'stereo';
1085
+ return picked.mask.toString();
1086
+ }
1087
+ }
1088
+ /**
1089
+ * Encoder options for live video: low-latency presets for x264/x265 and an IDR
1090
+ * for every requested keyframe, so a receiver that lost packets can resync.
1091
+ *
1092
+ * @param name - The encoder's name
1093
+ *
1094
+ * @param overrides - Caller options, applied last
1095
+ *
1096
+ * @returns The encoder options
1097
+ *
1098
+ * @internal
1099
+ */
1100
+ function liveVideoEncoderOptions(name, overrides) {
1101
+ const options = {};
1102
+ if (name === FF_ENCODER_LIBX264 || name === FF_ENCODER_LIBX265) {
1103
+ options.preset = 'ultrafast';
1104
+ options.tune = 'zerolatency';
1105
+ }
1106
+ // VAAPI, D3D12 and VideoToolbox make a forced I frame an IDR without an option
1107
+ const forcedIdr = forcedIdrOption(name);
1108
+ if (forcedIdr) {
1109
+ options[forcedIdr] = true;
1110
+ }
1111
+ return { ...options, ...overrides };
1112
+ }
1113
+ /**
1114
+ * The option that makes an encoder emit a forced I frame (frame.pictType = I) as IDR.
1115
+ *
1116
+ * @param name - The encoder's name
1117
+ *
1118
+ * @returns The option, or null when the encoder needs none or has none
1119
+ *
1120
+ * @internal
1121
+ */
1122
+ function forcedIdrOption(name) {
1123
+ if (name === FF_ENCODER_LIBX264 || name === FF_ENCODER_LIBX265 || name?.endsWith('_nvenc')) {
1124
+ return 'forced-idr';
1125
+ }
1126
+ if (name?.endsWith('_qsv') || name?.endsWith('_amf')) {
1127
+ return 'forced_idr';
1128
+ }
1129
+ return null;
1130
+ }
1131
+ //# sourceMappingURL=rtp-stream.js.map