@revizly/node-av 5.2.2-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 (254) hide show
  1. package/BUILD_LINUX.md +61 -0
  2. package/LICENSE.md +22 -0
  3. package/README.md +662 -0
  4. package/build_mac_local.sh +69 -0
  5. package/dist/api/audio-frame-buffer.d.ts +205 -0
  6. package/dist/api/audio-frame-buffer.js +287 -0
  7. package/dist/api/audio-frame-buffer.js.map +1 -0
  8. package/dist/api/bitstream-filter.d.ts +820 -0
  9. package/dist/api/bitstream-filter.js +1242 -0
  10. package/dist/api/bitstream-filter.js.map +1 -0
  11. package/dist/api/constants.d.ts +44 -0
  12. package/dist/api/constants.js +45 -0
  13. package/dist/api/constants.js.map +1 -0
  14. package/dist/api/data/test_av1.ivf +0 -0
  15. package/dist/api/data/test_h264.h264 +0 -0
  16. package/dist/api/data/test_hevc.h265 +0 -0
  17. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  18. package/dist/api/data/test_vp8.ivf +0 -0
  19. package/dist/api/data/test_vp9.ivf +0 -0
  20. package/dist/api/decoder.d.ts +1088 -0
  21. package/dist/api/decoder.js +1775 -0
  22. package/dist/api/decoder.js.map +1 -0
  23. package/dist/api/demuxer.d.ts +1219 -0
  24. package/dist/api/demuxer.js +2081 -0
  25. package/dist/api/demuxer.js.map +1 -0
  26. package/dist/api/device.d.ts +586 -0
  27. package/dist/api/device.js +961 -0
  28. package/dist/api/device.js.map +1 -0
  29. package/dist/api/encoder.d.ts +1132 -0
  30. package/dist/api/encoder.js +1988 -0
  31. package/dist/api/encoder.js.map +1 -0
  32. package/dist/api/filter-complex.d.ts +821 -0
  33. package/dist/api/filter-complex.js +1604 -0
  34. package/dist/api/filter-complex.js.map +1 -0
  35. package/dist/api/filter-presets.d.ts +1286 -0
  36. package/dist/api/filter-presets.js +2152 -0
  37. package/dist/api/filter-presets.js.map +1 -0
  38. package/dist/api/filter.d.ts +1234 -0
  39. package/dist/api/filter.js +1976 -0
  40. package/dist/api/filter.js.map +1 -0
  41. package/dist/api/fmp4-stream.d.ts +426 -0
  42. package/dist/api/fmp4-stream.js +739 -0
  43. package/dist/api/fmp4-stream.js.map +1 -0
  44. package/dist/api/hardware.d.ts +651 -0
  45. package/dist/api/hardware.js +1260 -0
  46. package/dist/api/hardware.js.map +1 -0
  47. package/dist/api/index.d.ts +17 -0
  48. package/dist/api/index.js +32 -0
  49. package/dist/api/index.js.map +1 -0
  50. package/dist/api/io-stream.d.ts +307 -0
  51. package/dist/api/io-stream.js +282 -0
  52. package/dist/api/io-stream.js.map +1 -0
  53. package/dist/api/muxer.d.ts +957 -0
  54. package/dist/api/muxer.js +2002 -0
  55. package/dist/api/muxer.js.map +1 -0
  56. package/dist/api/pipeline.d.ts +607 -0
  57. package/dist/api/pipeline.js +1145 -0
  58. package/dist/api/pipeline.js.map +1 -0
  59. package/dist/api/utilities/async-queue.d.ts +120 -0
  60. package/dist/api/utilities/async-queue.js +211 -0
  61. package/dist/api/utilities/async-queue.js.map +1 -0
  62. package/dist/api/utilities/audio-sample.d.ts +117 -0
  63. package/dist/api/utilities/audio-sample.js +112 -0
  64. package/dist/api/utilities/audio-sample.js.map +1 -0
  65. package/dist/api/utilities/channel-layout.d.ts +76 -0
  66. package/dist/api/utilities/channel-layout.js +80 -0
  67. package/dist/api/utilities/channel-layout.js.map +1 -0
  68. package/dist/api/utilities/electron-shared-texture.d.ts +328 -0
  69. package/dist/api/utilities/electron-shared-texture.js +503 -0
  70. package/dist/api/utilities/electron-shared-texture.js.map +1 -0
  71. package/dist/api/utilities/image.d.ts +207 -0
  72. package/dist/api/utilities/image.js +213 -0
  73. package/dist/api/utilities/image.js.map +1 -0
  74. package/dist/api/utilities/index.d.ts +12 -0
  75. package/dist/api/utilities/index.js +25 -0
  76. package/dist/api/utilities/index.js.map +1 -0
  77. package/dist/api/utilities/media-type.d.ts +49 -0
  78. package/dist/api/utilities/media-type.js +53 -0
  79. package/dist/api/utilities/media-type.js.map +1 -0
  80. package/dist/api/utilities/pixel-format.d.ts +89 -0
  81. package/dist/api/utilities/pixel-format.js +97 -0
  82. package/dist/api/utilities/pixel-format.js.map +1 -0
  83. package/dist/api/utilities/sample-format.d.ts +129 -0
  84. package/dist/api/utilities/sample-format.js +141 -0
  85. package/dist/api/utilities/sample-format.js.map +1 -0
  86. package/dist/api/utilities/scheduler.d.ts +138 -0
  87. package/dist/api/utilities/scheduler.js +98 -0
  88. package/dist/api/utilities/scheduler.js.map +1 -0
  89. package/dist/api/utilities/streaming.d.ts +186 -0
  90. package/dist/api/utilities/streaming.js +309 -0
  91. package/dist/api/utilities/streaming.js.map +1 -0
  92. package/dist/api/utilities/timestamp.d.ts +193 -0
  93. package/dist/api/utilities/timestamp.js +206 -0
  94. package/dist/api/utilities/timestamp.js.map +1 -0
  95. package/dist/api/utilities/whisper-model.d.ts +310 -0
  96. package/dist/api/utilities/whisper-model.js +528 -0
  97. package/dist/api/utilities/whisper-model.js.map +1 -0
  98. package/dist/api/utils.d.ts +19 -0
  99. package/dist/api/utils.js +39 -0
  100. package/dist/api/utils.js.map +1 -0
  101. package/dist/api/whisper.d.ts +324 -0
  102. package/dist/api/whisper.js +362 -0
  103. package/dist/api/whisper.js.map +1 -0
  104. package/dist/constants/channel-layouts.d.ts +53 -0
  105. package/dist/constants/channel-layouts.js +57 -0
  106. package/dist/constants/channel-layouts.js.map +1 -0
  107. package/dist/constants/constants.d.ts +2325 -0
  108. package/dist/constants/constants.js +1887 -0
  109. package/dist/constants/constants.js.map +1 -0
  110. package/dist/constants/decoders.d.ts +633 -0
  111. package/dist/constants/decoders.js +641 -0
  112. package/dist/constants/decoders.js.map +1 -0
  113. package/dist/constants/encoders.d.ts +295 -0
  114. package/dist/constants/encoders.js +308 -0
  115. package/dist/constants/encoders.js.map +1 -0
  116. package/dist/constants/hardware.d.ts +26 -0
  117. package/dist/constants/hardware.js +27 -0
  118. package/dist/constants/hardware.js.map +1 -0
  119. package/dist/constants/index.d.ts +5 -0
  120. package/dist/constants/index.js +6 -0
  121. package/dist/constants/index.js.map +1 -0
  122. package/dist/ffmpeg/index.d.ts +99 -0
  123. package/dist/ffmpeg/index.js +115 -0
  124. package/dist/ffmpeg/index.js.map +1 -0
  125. package/dist/ffmpeg/utils.d.ts +31 -0
  126. package/dist/ffmpeg/utils.js +68 -0
  127. package/dist/ffmpeg/utils.js.map +1 -0
  128. package/dist/ffmpeg/version.d.ts +6 -0
  129. package/dist/ffmpeg/version.js +7 -0
  130. package/dist/ffmpeg/version.js.map +1 -0
  131. package/dist/index.d.ts +4 -0
  132. package/dist/index.js +9 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/lib/audio-fifo.d.ts +399 -0
  135. package/dist/lib/audio-fifo.js +431 -0
  136. package/dist/lib/audio-fifo.js.map +1 -0
  137. package/dist/lib/binding.d.ts +228 -0
  138. package/dist/lib/binding.js +60 -0
  139. package/dist/lib/binding.js.map +1 -0
  140. package/dist/lib/bitstream-filter-context.d.ts +379 -0
  141. package/dist/lib/bitstream-filter-context.js +441 -0
  142. package/dist/lib/bitstream-filter-context.js.map +1 -0
  143. package/dist/lib/bitstream-filter.d.ts +140 -0
  144. package/dist/lib/bitstream-filter.js +154 -0
  145. package/dist/lib/bitstream-filter.js.map +1 -0
  146. package/dist/lib/codec-context.d.ts +1071 -0
  147. package/dist/lib/codec-context.js +1354 -0
  148. package/dist/lib/codec-context.js.map +1 -0
  149. package/dist/lib/codec-parameters.d.ts +616 -0
  150. package/dist/lib/codec-parameters.js +761 -0
  151. package/dist/lib/codec-parameters.js.map +1 -0
  152. package/dist/lib/codec-parser.d.ts +201 -0
  153. package/dist/lib/codec-parser.js +213 -0
  154. package/dist/lib/codec-parser.js.map +1 -0
  155. package/dist/lib/codec.d.ts +586 -0
  156. package/dist/lib/codec.js +713 -0
  157. package/dist/lib/codec.js.map +1 -0
  158. package/dist/lib/device.d.ts +291 -0
  159. package/dist/lib/device.js +324 -0
  160. package/dist/lib/device.js.map +1 -0
  161. package/dist/lib/dictionary.d.ts +333 -0
  162. package/dist/lib/dictionary.js +372 -0
  163. package/dist/lib/dictionary.js.map +1 -0
  164. package/dist/lib/error.d.ts +242 -0
  165. package/dist/lib/error.js +303 -0
  166. package/dist/lib/error.js.map +1 -0
  167. package/dist/lib/fifo.d.ts +416 -0
  168. package/dist/lib/fifo.js +453 -0
  169. package/dist/lib/fifo.js.map +1 -0
  170. package/dist/lib/filter-context.d.ts +712 -0
  171. package/dist/lib/filter-context.js +789 -0
  172. package/dist/lib/filter-context.js.map +1 -0
  173. package/dist/lib/filter-graph-segment.d.ts +160 -0
  174. package/dist/lib/filter-graph-segment.js +171 -0
  175. package/dist/lib/filter-graph-segment.js.map +1 -0
  176. package/dist/lib/filter-graph.d.ts +641 -0
  177. package/dist/lib/filter-graph.js +704 -0
  178. package/dist/lib/filter-graph.js.map +1 -0
  179. package/dist/lib/filter-inout.d.ts +198 -0
  180. package/dist/lib/filter-inout.js +257 -0
  181. package/dist/lib/filter-inout.js.map +1 -0
  182. package/dist/lib/filter.d.ts +243 -0
  183. package/dist/lib/filter.js +272 -0
  184. package/dist/lib/filter.js.map +1 -0
  185. package/dist/lib/format-context.d.ts +1254 -0
  186. package/dist/lib/format-context.js +1379 -0
  187. package/dist/lib/format-context.js.map +1 -0
  188. package/dist/lib/frame-utils.d.ts +116 -0
  189. package/dist/lib/frame-utils.js +98 -0
  190. package/dist/lib/frame-utils.js.map +1 -0
  191. package/dist/lib/frame.d.ts +1222 -0
  192. package/dist/lib/frame.js +1435 -0
  193. package/dist/lib/frame.js.map +1 -0
  194. package/dist/lib/hardware-device-context.d.ts +362 -0
  195. package/dist/lib/hardware-device-context.js +383 -0
  196. package/dist/lib/hardware-device-context.js.map +1 -0
  197. package/dist/lib/hardware-frames-context.d.ts +419 -0
  198. package/dist/lib/hardware-frames-context.js +477 -0
  199. package/dist/lib/hardware-frames-context.js.map +1 -0
  200. package/dist/lib/index.d.ts +35 -0
  201. package/dist/lib/index.js +60 -0
  202. package/dist/lib/index.js.map +1 -0
  203. package/dist/lib/input-format.d.ts +249 -0
  204. package/dist/lib/input-format.js +306 -0
  205. package/dist/lib/input-format.js.map +1 -0
  206. package/dist/lib/io-context.d.ts +696 -0
  207. package/dist/lib/io-context.js +769 -0
  208. package/dist/lib/io-context.js.map +1 -0
  209. package/dist/lib/log.d.ts +174 -0
  210. package/dist/lib/log.js +184 -0
  211. package/dist/lib/log.js.map +1 -0
  212. package/dist/lib/native-types.d.ts +946 -0
  213. package/dist/lib/native-types.js +2 -0
  214. package/dist/lib/native-types.js.map +1 -0
  215. package/dist/lib/option.d.ts +927 -0
  216. package/dist/lib/option.js +1583 -0
  217. package/dist/lib/option.js.map +1 -0
  218. package/dist/lib/output-format.d.ts +180 -0
  219. package/dist/lib/output-format.js +213 -0
  220. package/dist/lib/output-format.js.map +1 -0
  221. package/dist/lib/packet.d.ts +501 -0
  222. package/dist/lib/packet.js +590 -0
  223. package/dist/lib/packet.js.map +1 -0
  224. package/dist/lib/rational.d.ts +251 -0
  225. package/dist/lib/rational.js +278 -0
  226. package/dist/lib/rational.js.map +1 -0
  227. package/dist/lib/software-resample-context.d.ts +552 -0
  228. package/dist/lib/software-resample-context.js +592 -0
  229. package/dist/lib/software-resample-context.js.map +1 -0
  230. package/dist/lib/software-scale-context.d.ts +344 -0
  231. package/dist/lib/software-scale-context.js +366 -0
  232. package/dist/lib/software-scale-context.js.map +1 -0
  233. package/dist/lib/stream.d.ts +379 -0
  234. package/dist/lib/stream.js +526 -0
  235. package/dist/lib/stream.js.map +1 -0
  236. package/dist/lib/sync-queue.d.ts +179 -0
  237. package/dist/lib/sync-queue.js +197 -0
  238. package/dist/lib/sync-queue.js.map +1 -0
  239. package/dist/lib/types.d.ts +34 -0
  240. package/dist/lib/types.js +2 -0
  241. package/dist/lib/types.js.map +1 -0
  242. package/dist/lib/utilities.d.ts +1127 -0
  243. package/dist/lib/utilities.js +1225 -0
  244. package/dist/lib/utilities.js.map +1 -0
  245. package/dist/utils/electron.d.ts +49 -0
  246. package/dist/utils/electron.js +63 -0
  247. package/dist/utils/electron.js.map +1 -0
  248. package/dist/utils/index.d.ts +4 -0
  249. package/dist/utils/index.js +5 -0
  250. package/dist/utils/index.js.map +1 -0
  251. package/install/check.js +121 -0
  252. package/install/ffmpeg.js +66 -0
  253. package/jellyfin-ffmpeg.patch +181 -0
  254. package/package.json +129 -0
@@ -0,0 +1,739 @@
1
+ import { AV_CODEC_ID_AAC, AV_CODEC_ID_AV1, AV_CODEC_ID_FLAC, AV_CODEC_ID_H264, AV_CODEC_ID_HEVC, AV_CODEC_ID_OPUS, AV_HWDEVICE_TYPE_NONE, AV_SAMPLE_FMT_FLTP, } from '../constants/constants.js';
2
+ import { FF_ENCODER_AAC, FF_ENCODER_LIBX264 } from '../constants/encoders.js';
3
+ import { Codec } from '../lib/codec.js';
4
+ import { avGetCodecString } from '../lib/utilities.js';
5
+ import { Decoder } from './decoder.js';
6
+ import { Demuxer } from './demuxer.js';
7
+ import { Encoder } from './encoder.js';
8
+ import { FilterPreset } from './filter-presets.js';
9
+ import { FilterAPI } from './filter.js';
10
+ import { HardwareContext } from './hardware.js';
11
+ import { Muxer } from './muxer.js';
12
+ import { pipeline } from './pipeline.js';
13
+ /**
14
+ * Target codec strings for fMP4 streaming.
15
+ */
16
+ export const FMP4_CODECS = {
17
+ H264: 'avc1.640029',
18
+ H265: 'hvc1.1.6.L153.B0',
19
+ AV1: 'av01.0.00M.08',
20
+ AAC: 'mp4a.40.2',
21
+ FLAC: 'flac',
22
+ OPUS: 'opus',
23
+ };
24
+ /**
25
+ * High-level fMP4 streaming with automatic codec detection and transcoding.
26
+ *
27
+ * Provides fragmented MP4 streaming for clients.
28
+ * Automatically transcodes video to H.264 and audio to AAC if not supported by client.
29
+ * Client sends supported codecs, server transcodes accordingly.
30
+ * Essential component for building adaptive streaming servers.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { FMP4Stream } from 'node-av/api';
35
+ *
36
+ * // Client sends supported codecs
37
+ * const supportedCodecs = 'avc1.640029,hvc1.1.6.L153.B0,mp4a.40.2,flac';
38
+ *
39
+ * // Create stream with codec negotiation
40
+ * const stream = FMP4Stream.create('rtsp://camera.local/stream', {
41
+ * supportedCodecs,
42
+ * onData: (data) => ws.send(data.data)
43
+ * });
44
+ *
45
+ * // Start streaming (auto-transcodes if needed)
46
+ * await stream.start();
47
+ *
48
+ * // Stop when done
49
+ * await stream.stop();
50
+ * ```
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * // Stream with hardware acceleration
55
+ * const stream = FMP4Stream.create('input.mp4', {
56
+ * supportedCodecs: 'avc1.640029,mp4a.40.2',
57
+ * hardware: 'auto',
58
+ * fragDuration: 1,
59
+ * onData: (data) => sendToClient(data.data)
60
+ * });
61
+ *
62
+ * await stream.start();
63
+ * await stream.stop();
64
+ * ```
65
+ */
66
+ export class FMP4Stream {
67
+ options;
68
+ inputUrl;
69
+ inputOptions;
70
+ input;
71
+ output;
72
+ hardwareContext;
73
+ videoDecoder;
74
+ videoFilter;
75
+ videoEncoder;
76
+ audioDecoder;
77
+ audioFilter;
78
+ audioEncoder;
79
+ pipeline;
80
+ signal;
81
+ supportedCodecs;
82
+ incompleteBoxBuffer = null;
83
+ fragmentQueue = null;
84
+ _initSegment = null;
85
+ _initSegmentResolve = null;
86
+ _initSegmentPromise = null;
87
+ _ftypData = null;
88
+ _moovData = null;
89
+ /**
90
+ * @param input - Media input URL or pre-opened Demuxer
91
+ *
92
+ * @param options - Stream configuration options
93
+ *
94
+ * Use {@link create} factory method
95
+ *
96
+ * @internal
97
+ */
98
+ constructor(input, options) {
99
+ if (typeof input === 'string') {
100
+ this.inputUrl = input;
101
+ }
102
+ else {
103
+ this.inputUrl = null;
104
+ this.input = input;
105
+ }
106
+ const inputUrl = this.inputUrl ?? '';
107
+ this.inputOptions = {
108
+ ...options.inputOptions,
109
+ options: {
110
+ flags: 'low_delay',
111
+ fflags: 'nobuffer',
112
+ // analyzeduration: 0,
113
+ // probesize: 32,
114
+ timeout: 10000000,
115
+ rtsp_transport: inputUrl.toLowerCase().startsWith('rtsp') ? 'tcp' : undefined,
116
+ ...options.inputOptions?.options,
117
+ },
118
+ };
119
+ this.options = {
120
+ onData: options.onData ?? (() => { }),
121
+ onClose: options.onClose ?? (() => { }),
122
+ supportedCodecs: options.supportedCodecs ?? '',
123
+ fragDuration: options.fragDuration ?? 1,
124
+ hardware: options.hardware ?? { deviceType: AV_HWDEVICE_TYPE_NONE },
125
+ inputOptions: options.inputOptions,
126
+ video: {
127
+ fps: options.video?.fps,
128
+ width: options.video?.width,
129
+ height: options.video?.height,
130
+ encoderOptions: options.video?.encoderOptions ?? {},
131
+ },
132
+ audio: {
133
+ encoderOptions: options.audio?.encoderOptions ?? {},
134
+ },
135
+ bufferSize: options.bufferSize ?? 2 * 1024 * 1024,
136
+ boxMode: options.boxMode ?? false,
137
+ movFlags: options.movFlags ?? '+frag_keyframe+separate_moof+default_base_moof+empty_moov',
138
+ };
139
+ this.signal = options.signal;
140
+ // Parse supported codecs
141
+ this.supportedCodecs = new Set(this.options.supportedCodecs
142
+ .split(',')
143
+ .map((c) => c.trim())
144
+ .filter(Boolean));
145
+ }
146
+ /**
147
+ * Create a fMP4 stream from a media source.
148
+ *
149
+ * Configures the stream with input URL and options. The input is not opened
150
+ * until start() is called, allowing the stream to be reused after stop().
151
+ *
152
+ * @param input - Media source URL (RTSP, file path, HTTP, etc.) or a pre-opened {@link Demuxer}
153
+ *
154
+ * @param options - Stream configuration options with supported codecs
155
+ *
156
+ * @returns Configured fMP4 stream instance
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // Stream from file with codec negotiation
161
+ * const stream = FMP4Stream.create('video.mp4', {
162
+ * supportedCodecs: 'avc1.640029,mp4a.40.2',
163
+ * onData: (data) => ws.send(data.data)
164
+ * });
165
+ * ```
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * // Stream from RTSP with auto hardware acceleration
170
+ * const stream = FMP4Stream.create('rtsp://camera.local/stream', {
171
+ * supportedCodecs: 'avc1.640029,hvc1.1.6.L153.B0,mp4a.40.2',
172
+ * hardware: 'auto',
173
+ * fragDuration: 0.5
174
+ * });
175
+ * ```
176
+ */
177
+ static create(input, options = {}) {
178
+ return new FMP4Stream(input, options);
179
+ }
180
+ /**
181
+ * Promise that resolves with the init segment (ftyp+moov).
182
+ *
183
+ * Only available when boxMode is enabled.
184
+ * Resolves once the first ftyp and moov boxes have been received.
185
+ *
186
+ * @throws {Error} If boxMode is not enabled
187
+ */
188
+ get initSegment() {
189
+ if (!this.options.boxMode) {
190
+ throw new Error('initSegment is only available in box mode');
191
+ }
192
+ this._initSegmentPromise ??= new Promise((resolve) => {
193
+ if (this._initSegment) {
194
+ resolve(this._initSegment);
195
+ }
196
+ else {
197
+ this._initSegmentResolve = resolve;
198
+ }
199
+ });
200
+ return this._initSegmentPromise;
201
+ }
202
+ /**
203
+ * Get the demuxer instance.
204
+ *
205
+ * Used for accessing the underlying demuxer.
206
+ * Only available after start() is called.
207
+ *
208
+ * @returns Demuxer instance or undefined if not started
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const stream = FMP4Stream.create('input.mp4', {
213
+ * const input = stream.getInput();
214
+ * console.log('Bitrate:', input?.bitRate);
215
+ * ```
216
+ */
217
+ getInput() {
218
+ return this.input;
219
+ }
220
+ /**
221
+ * Get the codec string that will be used by client.
222
+ *
223
+ * Returns the MIME type codec string based on input codecs and transcoding decisions.
224
+ * Call this after start() is called to know what codec string to use for addSourceBuffer().
225
+ *
226
+ * @returns MIME type codec string (e.g., "avc1.640029,mp4a.40.2")
227
+ *
228
+ * @throws {Error} If called before start() is called
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const stream = await FMP4Stream.create('input.mp4', {
233
+ * supportedCodecs: 'avc1.640029,mp4a.40.2'
234
+ * });
235
+ *
236
+ * await stream.start(); // Must start first
237
+ * const codecString = stream.getCodecString();
238
+ * console.log(codecString); // "avc1.640029,mp4a.40.2"
239
+ * // Use this for: sourceBuffer = mediaSource.addSourceBuffer(`video/mp4; codecs="${codecString}"`);
240
+ * ```
241
+ */
242
+ getCodecString() {
243
+ if (!this.input) {
244
+ throw new Error('Input not opened. Call start() first to open the input.');
245
+ }
246
+ const videoStream = this.input.video();
247
+ const audioStream = this.input.audio();
248
+ const videoCodecId = videoStream?.codecpar.codecId;
249
+ const audioCodecId = audioStream?.codecpar.codecId;
250
+ // Determine video codec string
251
+ let videoCodec = null;
252
+ if (videoCodecId) {
253
+ const needsVideoTranscode = !this.isVideoCodecSupported(videoCodecId);
254
+ if (needsVideoTranscode) {
255
+ // Transcoding to H.264
256
+ videoCodec = FMP4_CODECS.H264;
257
+ }
258
+ else if (videoCodecId === AV_CODEC_ID_H264) {
259
+ // H.264 - use RFC 6381 codec string from input
260
+ const codecString = avGetCodecString(videoStream.codecpar);
261
+ videoCodec = codecString ?? FMP4_CODECS.H264;
262
+ }
263
+ else if (videoCodecId === AV_CODEC_ID_HEVC) {
264
+ // H.265 - use RFC 6381 codec string from input
265
+ const codecString = avGetCodecString(videoStream.codecpar);
266
+ videoCodec = codecString ?? FMP4_CODECS.H265;
267
+ }
268
+ else if (videoCodecId === AV_CODEC_ID_AV1) {
269
+ // AV1 - use RFC 6381 codec string from input
270
+ const codecString = avGetCodecString(videoStream.codecpar);
271
+ videoCodec = codecString ?? FMP4_CODECS.AV1;
272
+ }
273
+ else {
274
+ // Fallback to H.264 (should not happen as we transcode unsupported codecs)
275
+ videoCodec = FMP4_CODECS.H264;
276
+ }
277
+ }
278
+ // Determine audio codec string
279
+ let audioCodec = null;
280
+ if (audioCodecId) {
281
+ const needsAudioTranscode = !this.isAudioCodecSupported(audioCodecId);
282
+ if (needsAudioTranscode) {
283
+ // Transcoding to AAC
284
+ audioCodec = FMP4_CODECS.AAC;
285
+ }
286
+ else if (audioCodecId === AV_CODEC_ID_AAC) {
287
+ // AAC - use fixed codec string
288
+ audioCodec = FMP4_CODECS.AAC;
289
+ }
290
+ else if (audioCodecId === AV_CODEC_ID_FLAC) {
291
+ // FLAC
292
+ audioCodec = FMP4_CODECS.FLAC;
293
+ }
294
+ else if (audioCodecId === AV_CODEC_ID_OPUS) {
295
+ // Opus
296
+ audioCodec = FMP4_CODECS.OPUS;
297
+ }
298
+ else {
299
+ // Fallback to AAC (should not happen as we transcode unsupported codecs)
300
+ audioCodec = FMP4_CODECS.AAC;
301
+ }
302
+ }
303
+ // Combine video and audio codec strings
304
+ return [videoCodec, audioCodec].filter(Boolean).join(',');
305
+ }
306
+ /**
307
+ * Start streaming media to fMP4 chunks.
308
+ *
309
+ * Begins the media processing pipeline, reading packets from input,
310
+ * transcoding based on supported codecs, and generating fMP4 chunks.
311
+ * Video transcodes to H.264 if H.264/H.265 not supported.
312
+ * Audio transcodes to AAC if AAC/FLAC/Opus not supported.
313
+ * This method returns immediately after starting the pipeline.
314
+ *
315
+ * @returns Promise that resolves when pipeline is started
316
+ *
317
+ * @throws {FFmpegError} If setup fails
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * const stream = await FMP4Stream.create('input.mp4', {
322
+ * supportedCodecs: 'avc1.640029,mp4a.40.2',
323
+ * onData: (data) => sendToClient(data.data)
324
+ * });
325
+ *
326
+ * // Start streaming (returns immediately)
327
+ * await stream.start();
328
+ *
329
+ * // Later: stop streaming
330
+ * await stream.stop();
331
+ * ```
332
+ */
333
+ async start() {
334
+ if (this.pipeline) {
335
+ return;
336
+ }
337
+ this.signal?.throwIfAborted();
338
+ this.signal?.addEventListener('abort', () => this.stop(), { once: true });
339
+ // Open input if not already open
340
+ if (!this.input) {
341
+ if (!this.inputUrl) {
342
+ throw new Error('No input URL or Demuxer provided');
343
+ }
344
+ this.input = await Demuxer.open(this.inputUrl, this.inputOptions);
345
+ }
346
+ const videoStream = this.input.video();
347
+ const audioStream = this.input.audio();
348
+ // Check if video needs transcoding
349
+ const needsVideoTranscode = videoStream && !this.isVideoCodecSupported(videoStream.codecpar.codecId);
350
+ if (needsVideoTranscode) {
351
+ // Check if we need hardware acceleration
352
+ if (this.options.hardware === 'auto') {
353
+ this.hardwareContext = HardwareContext.auto();
354
+ }
355
+ else if (this.options.hardware.deviceType !== AV_HWDEVICE_TYPE_NONE) {
356
+ this.hardwareContext = HardwareContext.create(this.options.hardware.deviceType, this.options.hardware.device, this.options.hardware.options);
357
+ }
358
+ // Transcode to H.264
359
+ this.videoDecoder = await Decoder.create(videoStream, {
360
+ hardware: this.hardwareContext,
361
+ exitOnError: false,
362
+ });
363
+ // Determine if we need filters by comparing with current stream properties
364
+ const currentWidth = videoStream.codecpar.width;
365
+ const currentHeight = videoStream.codecpar.height;
366
+ const currentFps = videoStream.avgFrameRate.num / videoStream.avgFrameRate.den;
367
+ const needsScale = (this.options.video.width !== undefined && this.options.video.width !== currentWidth) ||
368
+ (this.options.video.height !== undefined && this.options.video.height !== currentHeight);
369
+ const needsFps = this.options.video.fps !== undefined && isFinite(currentFps) && this.options.video.fps !== currentFps;
370
+ // Create filter chain only if needed
371
+ if (needsScale || needsFps) {
372
+ const filterChain = FilterPreset.chain(this.hardwareContext);
373
+ // Add scale filter if dimensions differ
374
+ if (needsScale) {
375
+ const targetWidth = this.options.video.width ?? -1;
376
+ const targetHeight = this.options.video.height ?? -1;
377
+ filterChain.scale(targetWidth, targetHeight);
378
+ }
379
+ // Add fps filter if fps differs
380
+ if (needsFps) {
381
+ filterChain.fps(this.options.video.fps);
382
+ }
383
+ this.videoFilter = FilterAPI.create(filterChain.build(), {
384
+ hardware: this.hardwareContext,
385
+ });
386
+ }
387
+ const encoderCodec = this.hardwareContext?.getEncoderCodec('h264') ?? Codec.findEncoderByName(FF_ENCODER_LIBX264);
388
+ let encoderOptions = {};
389
+ if (encoderCodec.name === FF_ENCODER_LIBX264 || encoderCodec.name === FF_ENCODER_LIBX264) {
390
+ encoderOptions.preset = 'ultrafast';
391
+ encoderOptions.tune = 'zerolatency';
392
+ }
393
+ encoderOptions = {
394
+ ...encoderOptions,
395
+ ...this.options.video.encoderOptions,
396
+ };
397
+ this.videoEncoder = await Encoder.create(encoderCodec, {
398
+ decoder: this.videoDecoder,
399
+ options: encoderOptions,
400
+ });
401
+ }
402
+ // Check if audio needs transcoding
403
+ const needsAudioTranscode = audioStream && !this.isAudioCodecSupported(audioStream.codecpar.codecId);
404
+ if (needsAudioTranscode) {
405
+ // Transcode to AAC
406
+ this.audioDecoder = await Decoder.create(audioStream, {
407
+ exitOnError: false,
408
+ });
409
+ const targetSampleRate = 44100;
410
+ const filterChain = FilterPreset.chain().aformat(AV_SAMPLE_FMT_FLTP, targetSampleRate, 'stereo').build();
411
+ this.audioFilter = FilterAPI.create(filterChain);
412
+ this.audioEncoder = await Encoder.create(FF_ENCODER_AAC, {
413
+ decoder: this.audioDecoder,
414
+ filter: this.audioFilter,
415
+ options: this.options.audio.encoderOptions,
416
+ });
417
+ }
418
+ // Setup output with callback
419
+ const cb = {
420
+ write: (buffer) => {
421
+ if (this.options.boxMode) {
422
+ // Box mode: buffer until we have complete boxes
423
+ this.processBoxMode(buffer);
424
+ }
425
+ else {
426
+ // Chunk mode: send raw data immediately
427
+ const info = { isComplete: false, boxes: [] };
428
+ this.options.onData(buffer, info);
429
+ this.pushFragment(buffer, info);
430
+ }
431
+ return buffer.length;
432
+ },
433
+ };
434
+ this.output = await Muxer.open(cb, {
435
+ input: this.input,
436
+ format: 'mp4',
437
+ bufferSize: this.options.bufferSize,
438
+ exitOnError: false,
439
+ options: {
440
+ movflags: this.options.movFlags,
441
+ frag_duration: this.options.fragDuration,
442
+ },
443
+ });
444
+ this.runPipeline()
445
+ .then(() => {
446
+ this.endFragments();
447
+ this.options.onClose?.();
448
+ })
449
+ .catch(async (error) => {
450
+ this.endFragments();
451
+ await this.stop();
452
+ this.options.onClose?.(error);
453
+ });
454
+ }
455
+ /**
456
+ * Stop streaming gracefully and clean up all resources.
457
+ *
458
+ * Stops the pipeline, closes output, and releases all FFmpeg resources.
459
+ * Safe to call multiple times. After stopping, you can call start() again
460
+ * to restart the stream.
461
+ *
462
+ * @example
463
+ * ```typescript
464
+ * const stream = await FMP4Stream.create('input.mp4', {
465
+ * supportedCodecs: 'avc1.640029,mp4a.40.2'
466
+ * });
467
+ * await stream.start();
468
+ *
469
+ * // Stop after 10 seconds
470
+ * setTimeout(async () => await stream.stop(), 10000);
471
+ * ```
472
+ */
473
+ async stop() {
474
+ this.endFragments();
475
+ // Stop pipeline if running and wait for completion
476
+ if (this.pipeline && !this.pipeline.isStopped()) {
477
+ this.pipeline.stop();
478
+ await this.pipeline.completion;
479
+ this.pipeline = undefined;
480
+ }
481
+ // Close all resources
482
+ await this.input?.close();
483
+ this.input = undefined;
484
+ this.videoDecoder?.close();
485
+ this.videoDecoder = undefined;
486
+ this.videoFilter?.close();
487
+ this.videoFilter = undefined;
488
+ this.videoEncoder?.close();
489
+ this.videoEncoder = undefined;
490
+ this.audioDecoder?.close();
491
+ this.audioDecoder = undefined;
492
+ this.audioFilter?.close();
493
+ this.audioFilter = undefined;
494
+ this.audioEncoder?.close();
495
+ this.audioEncoder = undefined;
496
+ this.hardwareContext?.dispose();
497
+ this.hardwareContext = undefined;
498
+ await this.output?.close();
499
+ this.output = undefined;
500
+ this._initSegment = null;
501
+ this._initSegmentResolve = null;
502
+ this._initSegmentPromise = null;
503
+ this._ftypData = null;
504
+ this._moovData = null;
505
+ }
506
+ /**
507
+ * Async generator that yields media fragments (moof+mdat chunks).
508
+ *
509
+ * In box mode, yields only media fragments (chunks containing moof boxes),
510
+ * NOT the init segment. Use {@link initSegment} to get the ftyp+moov data.
511
+ *
512
+ * In chunk mode (boxMode disabled), yields every chunk as-is since
513
+ * box-level filtering is not possible without box parsing.
514
+ *
515
+ * The generator completes when the stream stops or the pipeline ends.
516
+ *
517
+ * @yields {FMP4Fragment} Media fragment with data buffer and box info
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * const stream = FMP4Stream.create('rtsp://camera/stream', {
522
+ * supportedCodecs: 'avc1.640029,mp4a.40.2',
523
+ * boxMode: true,
524
+ * });
525
+ *
526
+ * await stream.start();
527
+ * const init = await stream.initSegment;
528
+ *
529
+ * for await (const fragment of stream.fragments()) {
530
+ * sendToClient(fragment.data);
531
+ * }
532
+ * ```
533
+ */
534
+ async *fragments() {
535
+ this.fragmentQueue = { queue: [], resolve: null, done: false };
536
+ try {
537
+ while (true) {
538
+ const result = await new Promise((resolve) => {
539
+ if (this.fragmentQueue.queue.length > 0) {
540
+ resolve({ value: this.fragmentQueue.queue.shift(), done: false });
541
+ return;
542
+ }
543
+ if (this.fragmentQueue.done) {
544
+ resolve({ value: undefined, done: true });
545
+ return;
546
+ }
547
+ this.fragmentQueue.resolve = resolve;
548
+ });
549
+ if (result.done)
550
+ break;
551
+ yield result.value;
552
+ }
553
+ }
554
+ finally {
555
+ this.fragmentQueue = null;
556
+ }
557
+ }
558
+ /**
559
+ * Run the streaming pipeline until completion or stopped.
560
+ *
561
+ * @internal
562
+ */
563
+ async runPipeline() {
564
+ if (!this.input || !this.output) {
565
+ return;
566
+ }
567
+ const hasVideo = this.input?.video() !== undefined;
568
+ const hasAudio = this.input?.audio() !== undefined;
569
+ const opts = this.signal ? { signal: this.signal } : undefined;
570
+ if (hasAudio && hasVideo) {
571
+ this.pipeline = pipeline(this.input, {
572
+ video: [this.videoDecoder, this.videoFilter, this.videoEncoder],
573
+ audio: [this.audioDecoder, this.audioFilter, this.audioEncoder],
574
+ }, this.output, opts);
575
+ }
576
+ else if (hasVideo) {
577
+ this.pipeline = pipeline(this.input, {
578
+ video: [this.videoDecoder, this.videoFilter, this.videoEncoder],
579
+ }, this.output, opts);
580
+ }
581
+ else if (hasAudio) {
582
+ this.pipeline = pipeline(this.input, {
583
+ audio: [this.audioDecoder, this.audioFilter, this.audioEncoder],
584
+ }, this.output, opts);
585
+ }
586
+ else {
587
+ throw new Error('No audio or video streams found in input');
588
+ }
589
+ await this.pipeline.completion;
590
+ this.pipeline = undefined;
591
+ }
592
+ /**
593
+ * Check if video codec is supported.
594
+ *
595
+ * @param codecId - Codec ID
596
+ *
597
+ * @returns True if H.264, H.265, or AV1 is in supported codecs
598
+ *
599
+ * @internal
600
+ */
601
+ isVideoCodecSupported(codecId) {
602
+ if (codecId === AV_CODEC_ID_H264 && (this.supportedCodecs.has(FMP4_CODECS.H264) || this.supportedCodecs.has('avc1'))) {
603
+ return true;
604
+ }
605
+ if (codecId === AV_CODEC_ID_HEVC && (this.supportedCodecs.has(FMP4_CODECS.H265) || this.supportedCodecs.has('hvc1') || this.supportedCodecs.has('hev1'))) {
606
+ return true;
607
+ }
608
+ if (codecId === AV_CODEC_ID_AV1 && (this.supportedCodecs.has(FMP4_CODECS.AV1) || this.supportedCodecs.has('av01'))) {
609
+ return true;
610
+ }
611
+ return false;
612
+ }
613
+ /**
614
+ * Check if audio codec is supported.
615
+ *
616
+ * @param codecId - Codec ID
617
+ *
618
+ * @returns True if AAC, FLAC, or Opus is in supported codecs
619
+ *
620
+ * @internal
621
+ */
622
+ isAudioCodecSupported(codecId) {
623
+ if (codecId === AV_CODEC_ID_AAC && this.supportedCodecs.has(FMP4_CODECS.AAC)) {
624
+ return true;
625
+ }
626
+ if (codecId === AV_CODEC_ID_FLAC && this.supportedCodecs.has(FMP4_CODECS.FLAC)) {
627
+ return true;
628
+ }
629
+ if (codecId === AV_CODEC_ID_OPUS && this.supportedCodecs.has(FMP4_CODECS.OPUS)) {
630
+ return true;
631
+ }
632
+ return false;
633
+ }
634
+ /**
635
+ * Process buffer in box mode - buffers until complete boxes are available.
636
+ *
637
+ * @param chunk - Incoming data chunk from FFmpeg
638
+ *
639
+ * @internal
640
+ */
641
+ processBoxMode(chunk) {
642
+ // If we have an incomplete box from previous chunk, append to it
643
+ if (this.incompleteBoxBuffer) {
644
+ chunk = Buffer.concat([this.incompleteBoxBuffer, chunk]);
645
+ this.incompleteBoxBuffer = null;
646
+ }
647
+ let offset = 0;
648
+ const boxes = [];
649
+ while (offset + 8 <= chunk.length) {
650
+ // Read box header
651
+ const boxSize = chunk.readUInt32BE(offset);
652
+ const boxType = chunk.toString('ascii', offset + 4, offset + 8);
653
+ // Check if we have the complete box
654
+ if (offset + boxSize > chunk.length) {
655
+ // Box is incomplete - save for next chunk
656
+ this.incompleteBoxBuffer = chunk.subarray(offset);
657
+ break;
658
+ }
659
+ // We have the complete box - parse it
660
+ const box = {
661
+ type: boxType,
662
+ size: boxSize,
663
+ data: chunk.subarray(offset + 8, offset + boxSize),
664
+ offset: offset,
665
+ };
666
+ boxes.push(box);
667
+ // Move to next box
668
+ offset += boxSize;
669
+ // Safety check: invalid box size
670
+ if (boxSize < 8) {
671
+ break;
672
+ }
673
+ }
674
+ // If we have complete boxes, send them to the callback
675
+ if (boxes.length > 0) {
676
+ const boxData = chunk.subarray(0, offset);
677
+ const info = { isComplete: true, boxes };
678
+ // onData callback gets everything
679
+ this.options.onData(boxData, info);
680
+ // Init segment tracking: collect ftyp and moov data
681
+ if (!this._initSegment) {
682
+ for (const box of boxes) {
683
+ if (box.type === 'ftyp' && !this._ftypData) {
684
+ this._ftypData = chunk.subarray(box.offset, box.offset + box.size);
685
+ }
686
+ if (box.type === 'moov' && !this._moovData) {
687
+ this._moovData = chunk.subarray(box.offset, box.offset + box.size);
688
+ }
689
+ }
690
+ if (this._ftypData && this._moovData) {
691
+ this._initSegment = Buffer.concat([this._ftypData, this._moovData]);
692
+ this._initSegmentResolve?.(this._initSegment);
693
+ this._initSegmentResolve = null;
694
+ }
695
+ }
696
+ // fragments() generator gets only media fragments (moof+mdat)
697
+ const isMediaFragment = boxes.some((b) => b.type === 'moof');
698
+ if (isMediaFragment) {
699
+ this.pushFragment(boxData, info);
700
+ }
701
+ }
702
+ }
703
+ /**
704
+ * Push a fragment to the fragment queue for the async generator.
705
+ *
706
+ * @param data - fMP4 data buffer
707
+ *
708
+ * @param info - Parsed box information
709
+ *
710
+ * @internal
711
+ */
712
+ pushFragment(data, info) {
713
+ if (!this.fragmentQueue)
714
+ return;
715
+ const fragment = { data, info };
716
+ if (this.fragmentQueue.resolve) {
717
+ this.fragmentQueue.resolve({ value: fragment, done: false });
718
+ this.fragmentQueue.resolve = null;
719
+ }
720
+ else {
721
+ this.fragmentQueue.queue.push(fragment);
722
+ }
723
+ }
724
+ /**
725
+ * Signal the fragment queue that no more fragments will arrive.
726
+ *
727
+ * @internal
728
+ */
729
+ endFragments() {
730
+ if (!this.fragmentQueue)
731
+ return;
732
+ this.fragmentQueue.done = true;
733
+ if (this.fragmentQueue.resolve) {
734
+ this.fragmentQueue.resolve({ value: undefined, done: true });
735
+ this.fragmentQueue.resolve = null;
736
+ }
737
+ }
738
+ }
739
+ //# sourceMappingURL=fmp4-stream.js.map