@kenzuya/mediabunny 1.26.0 → 1.28.5

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 (237) hide show
  1. package/README.md +1 -1
  2. package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21388
  3. package/dist/bundles/mediabunny.min.js +490 -0
  4. package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
  5. package/dist/modules/src/adts/adts-demuxer.d.ts +6 -6
  6. package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
  7. package/dist/modules/src/adts/adts-muxer.d.ts +4 -4
  8. package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
  9. package/dist/modules/src/adts/adts-reader.d.ts +1 -1
  10. package/dist/modules/src/adts/adts-reader.d.ts.map +1 -1
  11. package/dist/modules/src/avi/avi-demuxer.d.ts +44 -0
  12. package/dist/modules/src/avi/avi-demuxer.d.ts.map +1 -0
  13. package/dist/modules/src/avi/avi-misc.d.ts +88 -0
  14. package/dist/modules/src/avi/avi-misc.d.ts.map +1 -0
  15. package/dist/modules/src/avi/avi-muxer.d.ts +45 -0
  16. package/dist/modules/src/avi/avi-muxer.d.ts.map +1 -0
  17. package/dist/modules/src/avi/riff-writer.d.ts +26 -0
  18. package/dist/modules/src/avi/riff-writer.d.ts.map +1 -0
  19. package/dist/modules/src/codec-data.d.ts +8 -3
  20. package/dist/modules/src/codec-data.d.ts.map +1 -1
  21. package/dist/modules/src/codec.d.ts +10 -10
  22. package/dist/modules/src/codec.d.ts.map +1 -1
  23. package/dist/modules/src/conversion.d.ts +33 -16
  24. package/dist/modules/src/conversion.d.ts.map +1 -1
  25. package/dist/modules/src/custom-coder.d.ts +8 -8
  26. package/dist/modules/src/custom-coder.d.ts.map +1 -1
  27. package/dist/modules/src/demuxer.d.ts +3 -3
  28. package/dist/modules/src/demuxer.d.ts.map +1 -1
  29. package/dist/modules/src/encode.d.ts +8 -8
  30. package/dist/modules/src/encode.d.ts.map +1 -1
  31. package/dist/modules/src/flac/flac-demuxer.d.ts +7 -7
  32. package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
  33. package/dist/modules/src/flac/flac-misc.d.ts +3 -3
  34. package/dist/modules/src/flac/flac-misc.d.ts.map +1 -1
  35. package/dist/modules/src/flac/flac-muxer.d.ts +5 -5
  36. package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
  37. package/dist/modules/src/id3.d.ts +3 -3
  38. package/dist/modules/src/id3.d.ts.map +1 -1
  39. package/dist/modules/src/index.d.ts +20 -20
  40. package/dist/modules/src/index.d.ts.map +1 -1
  41. package/dist/modules/src/input-format.d.ts +22 -0
  42. package/dist/modules/src/input-format.d.ts.map +1 -1
  43. package/dist/modules/src/input-track.d.ts +8 -8
  44. package/dist/modules/src/input-track.d.ts.map +1 -1
  45. package/dist/modules/src/input.d.ts +12 -12
  46. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -2
  47. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  48. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +12 -12
  49. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  50. package/dist/modules/src/isobmff/isobmff-misc.d.ts.map +1 -1
  51. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +11 -11
  52. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  53. package/dist/modules/src/isobmff/isobmff-reader.d.ts +2 -2
  54. package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
  55. package/dist/modules/src/matroska/ebml.d.ts +3 -3
  56. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  57. package/dist/modules/src/matroska/matroska-demuxer.d.ts +13 -13
  58. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  59. package/dist/modules/src/matroska/matroska-input.d.ts +33 -0
  60. package/dist/modules/src/matroska/matroska-input.d.ts.map +1 -0
  61. package/dist/modules/src/matroska/matroska-misc.d.ts.map +1 -1
  62. package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -5
  63. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  64. package/dist/modules/src/media-sink.d.ts +5 -5
  65. package/dist/modules/src/media-sink.d.ts.map +1 -1
  66. package/dist/modules/src/media-source.d.ts +22 -4
  67. package/dist/modules/src/media-source.d.ts.map +1 -1
  68. package/dist/modules/src/metadata.d.ts +2 -2
  69. package/dist/modules/src/metadata.d.ts.map +1 -1
  70. package/dist/modules/src/misc.d.ts +5 -4
  71. package/dist/modules/src/misc.d.ts.map +1 -1
  72. package/dist/modules/src/mp3/mp3-demuxer.d.ts +7 -7
  73. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  74. package/dist/modules/src/mp3/mp3-muxer.d.ts +4 -4
  75. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  76. package/dist/modules/src/mp3/mp3-reader.d.ts +2 -2
  77. package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
  78. package/dist/modules/src/mp3/mp3-writer.d.ts +1 -1
  79. package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
  80. package/dist/modules/src/muxer.d.ts +4 -4
  81. package/dist/modules/src/muxer.d.ts.map +1 -1
  82. package/dist/modules/src/node.d.ts +1 -1
  83. package/dist/modules/src/ogg/ogg-demuxer.d.ts +7 -7
  84. package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
  85. package/dist/modules/src/ogg/ogg-misc.d.ts +1 -1
  86. package/dist/modules/src/ogg/ogg-misc.d.ts.map +1 -1
  87. package/dist/modules/src/ogg/ogg-muxer.d.ts +5 -5
  88. package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
  89. package/dist/modules/src/ogg/ogg-reader.d.ts +1 -1
  90. package/dist/modules/src/ogg/ogg-reader.d.ts.map +1 -1
  91. package/dist/modules/src/output-format.d.ts +51 -6
  92. package/dist/modules/src/output-format.d.ts.map +1 -1
  93. package/dist/modules/src/output.d.ts +13 -13
  94. package/dist/modules/src/output.d.ts.map +1 -1
  95. package/dist/modules/src/packet.d.ts +1 -1
  96. package/dist/modules/src/packet.d.ts.map +1 -1
  97. package/dist/modules/src/pcm.d.ts.map +1 -1
  98. package/dist/modules/src/reader.d.ts +2 -2
  99. package/dist/modules/src/reader.d.ts.map +1 -1
  100. package/dist/modules/src/sample.d.ts +57 -15
  101. package/dist/modules/src/sample.d.ts.map +1 -1
  102. package/dist/modules/src/source.d.ts +3 -3
  103. package/dist/modules/src/source.d.ts.map +1 -1
  104. package/dist/modules/src/subtitles.d.ts +1 -1
  105. package/dist/modules/src/subtitles.d.ts.map +1 -1
  106. package/dist/modules/src/target.d.ts +2 -2
  107. package/dist/modules/src/target.d.ts.map +1 -1
  108. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  109. package/dist/modules/src/wave/riff-writer.d.ts +1 -1
  110. package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
  111. package/dist/modules/src/wave/wave-demuxer.d.ts +6 -6
  112. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  113. package/dist/modules/src/wave/wave-muxer.d.ts +4 -4
  114. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  115. package/dist/modules/src/writer.d.ts +1 -1
  116. package/dist/modules/src/writer.d.ts.map +1 -1
  117. package/dist/packages/eac3/eac3.wasm +0 -0
  118. package/dist/packages/eac3/mediabunny-eac3.js +1058 -0
  119. package/dist/packages/eac3/mediabunny-eac3.min.js +44 -0
  120. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.js +694 -0
  121. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.min.js +58 -0
  122. package/dist/packages/mpeg4/mediabunny-mpeg4.js +1198 -0
  123. package/dist/packages/mpeg4/mediabunny-mpeg4.min.js +44 -0
  124. package/dist/packages/mpeg4/xvid.wasm +0 -0
  125. package/package.json +18 -57
  126. package/dist/bundles/mediabunny.cjs +0 -26140
  127. package/dist/bundles/mediabunny.min.cjs +0 -147
  128. package/dist/bundles/mediabunny.min.mjs +0 -146
  129. package/dist/mediabunny.d.ts +0 -3319
  130. package/dist/modules/shared/mp3-misc.js +0 -147
  131. package/dist/modules/src/adts/adts-demuxer.js +0 -239
  132. package/dist/modules/src/adts/adts-muxer.js +0 -80
  133. package/dist/modules/src/adts/adts-reader.js +0 -63
  134. package/dist/modules/src/codec-data.js +0 -1730
  135. package/dist/modules/src/codec.js +0 -869
  136. package/dist/modules/src/conversion.js +0 -1459
  137. package/dist/modules/src/custom-coder.js +0 -117
  138. package/dist/modules/src/demuxer.js +0 -12
  139. package/dist/modules/src/encode.js +0 -442
  140. package/dist/modules/src/flac/flac-demuxer.js +0 -504
  141. package/dist/modules/src/flac/flac-misc.js +0 -135
  142. package/dist/modules/src/flac/flac-muxer.js +0 -222
  143. package/dist/modules/src/id3.js +0 -848
  144. package/dist/modules/src/index.js +0 -28
  145. package/dist/modules/src/input-format.js +0 -480
  146. package/dist/modules/src/input-track.js +0 -372
  147. package/dist/modules/src/input.js +0 -188
  148. package/dist/modules/src/isobmff/isobmff-boxes.js +0 -1480
  149. package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -2618
  150. package/dist/modules/src/isobmff/isobmff-misc.js +0 -20
  151. package/dist/modules/src/isobmff/isobmff-muxer.js +0 -966
  152. package/dist/modules/src/isobmff/isobmff-reader.js +0 -72
  153. package/dist/modules/src/matroska/ebml.js +0 -653
  154. package/dist/modules/src/matroska/matroska-demuxer.js +0 -2133
  155. package/dist/modules/src/matroska/matroska-misc.js +0 -20
  156. package/dist/modules/src/matroska/matroska-muxer.js +0 -1017
  157. package/dist/modules/src/media-sink.js +0 -1736
  158. package/dist/modules/src/media-source.js +0 -1825
  159. package/dist/modules/src/metadata.js +0 -193
  160. package/dist/modules/src/misc.js +0 -623
  161. package/dist/modules/src/mp3/mp3-demuxer.js +0 -285
  162. package/dist/modules/src/mp3/mp3-muxer.js +0 -123
  163. package/dist/modules/src/mp3/mp3-reader.js +0 -26
  164. package/dist/modules/src/mp3/mp3-writer.js +0 -78
  165. package/dist/modules/src/muxer.js +0 -50
  166. package/dist/modules/src/node.js +0 -9
  167. package/dist/modules/src/ogg/ogg-demuxer.js +0 -763
  168. package/dist/modules/src/ogg/ogg-misc.js +0 -78
  169. package/dist/modules/src/ogg/ogg-muxer.js +0 -353
  170. package/dist/modules/src/ogg/ogg-reader.js +0 -65
  171. package/dist/modules/src/output-format.js +0 -527
  172. package/dist/modules/src/output.js +0 -300
  173. package/dist/modules/src/packet.js +0 -182
  174. package/dist/modules/src/pcm.js +0 -85
  175. package/dist/modules/src/reader.js +0 -236
  176. package/dist/modules/src/sample.js +0 -1056
  177. package/dist/modules/src/source.js +0 -1182
  178. package/dist/modules/src/subtitles.js +0 -575
  179. package/dist/modules/src/target.js +0 -140
  180. package/dist/modules/src/wave/riff-writer.js +0 -30
  181. package/dist/modules/src/wave/wave-demuxer.js +0 -447
  182. package/dist/modules/src/wave/wave-muxer.js +0 -318
  183. package/dist/modules/src/writer.js +0 -370
  184. package/src/adts/adts-demuxer.ts +0 -331
  185. package/src/adts/adts-muxer.ts +0 -111
  186. package/src/adts/adts-reader.ts +0 -85
  187. package/src/codec-data.ts +0 -2078
  188. package/src/codec.ts +0 -1092
  189. package/src/conversion.ts +0 -2112
  190. package/src/custom-coder.ts +0 -197
  191. package/src/demuxer.ts +0 -24
  192. package/src/encode.ts +0 -739
  193. package/src/flac/flac-demuxer.ts +0 -730
  194. package/src/flac/flac-misc.ts +0 -164
  195. package/src/flac/flac-muxer.ts +0 -320
  196. package/src/id3.ts +0 -925
  197. package/src/index.ts +0 -221
  198. package/src/input-format.ts +0 -541
  199. package/src/input-track.ts +0 -529
  200. package/src/input.ts +0 -235
  201. package/src/isobmff/isobmff-boxes.ts +0 -1719
  202. package/src/isobmff/isobmff-demuxer.ts +0 -3190
  203. package/src/isobmff/isobmff-misc.ts +0 -29
  204. package/src/isobmff/isobmff-muxer.ts +0 -1348
  205. package/src/isobmff/isobmff-reader.ts +0 -91
  206. package/src/matroska/ebml.ts +0 -730
  207. package/src/matroska/matroska-demuxer.ts +0 -2481
  208. package/src/matroska/matroska-misc.ts +0 -29
  209. package/src/matroska/matroska-muxer.ts +0 -1276
  210. package/src/media-sink.ts +0 -2179
  211. package/src/media-source.ts +0 -2243
  212. package/src/metadata.ts +0 -320
  213. package/src/misc.ts +0 -798
  214. package/src/mp3/mp3-demuxer.ts +0 -383
  215. package/src/mp3/mp3-muxer.ts +0 -166
  216. package/src/mp3/mp3-reader.ts +0 -34
  217. package/src/mp3/mp3-writer.ts +0 -120
  218. package/src/muxer.ts +0 -88
  219. package/src/node.ts +0 -11
  220. package/src/ogg/ogg-demuxer.ts +0 -1053
  221. package/src/ogg/ogg-misc.ts +0 -116
  222. package/src/ogg/ogg-muxer.ts +0 -497
  223. package/src/ogg/ogg-reader.ts +0 -93
  224. package/src/output-format.ts +0 -945
  225. package/src/output.ts +0 -488
  226. package/src/packet.ts +0 -263
  227. package/src/pcm.ts +0 -112
  228. package/src/reader.ts +0 -323
  229. package/src/sample.ts +0 -1461
  230. package/src/source.ts +0 -1688
  231. package/src/subtitles.ts +0 -711
  232. package/src/target.ts +0 -204
  233. package/src/tsconfig.json +0 -16
  234. package/src/wave/riff-writer.ts +0 -36
  235. package/src/wave/wave-demuxer.ts +0 -529
  236. package/src/wave/wave-muxer.ts +0 -371
  237. package/src/writer.ts +0 -490
@@ -1,1459 +0,0 @@
1
- /*!
2
- * Copyright (c) 2025-present, Vanilagy and contributors
3
- *
4
- * This Source Code Form is subject to the terms of the Mozilla Public
5
- * License, v. 2.0. If a copy of the MPL was not distributed with this
6
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
- */
8
- import { AUDIO_CODECS, NON_PCM_AUDIO_CODECS, SUBTITLE_CODECS, VIDEO_CODECS, } from './codec.js';
9
- import { getEncodableAudioCodecs, getFirstEncodableVideoCodec, Quality, QUALITY_HIGH, } from './encode.js';
10
- import { Input } from './input.js';
11
- import { AudioSampleSink, CanvasSink, EncodedPacketSink, VideoSampleSink, } from './media-sink.js';
12
- import { EncodedVideoPacketSource, EncodedAudioPacketSource, TextSubtitleSource, VideoSampleSource, AudioSampleSource, } from './media-source.js';
13
- import { assert, clamp, isIso639Dash2LanguageCode, normalizeRotation, promiseWithResolvers, } from './misc.js';
14
- import { Output } from './output.js';
15
- import { Mp4OutputFormat } from './output-format.js';
16
- import { AudioSample, clampCropRectangle, validateCropRectangle, VideoSample } from './sample.js';
17
- import { validateMetadataTags } from './metadata.js';
18
- import { formatCuesToAss, formatCuesToSrt, formatCuesToWebVTT } from './subtitles.js';
19
- import { NullTarget } from './target.js';
20
- const validateVideoOptions = (videoOptions) => {
21
- if (videoOptions !== undefined && (!videoOptions || typeof videoOptions !== 'object')) {
22
- throw new TypeError('options.video, when provided, must be an object.');
23
- }
24
- if (videoOptions?.discard !== undefined && typeof videoOptions.discard !== 'boolean') {
25
- throw new TypeError('options.video.discard, when provided, must be a boolean.');
26
- }
27
- if (videoOptions?.forceTranscode !== undefined && typeof videoOptions.forceTranscode !== 'boolean') {
28
- throw new TypeError('options.video.forceTranscode, when provided, must be a boolean.');
29
- }
30
- if (videoOptions?.codec !== undefined && !VIDEO_CODECS.includes(videoOptions.codec)) {
31
- throw new TypeError(`options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(', ')}.`);
32
- }
33
- if (videoOptions?.bitrate !== undefined
34
- && !(videoOptions.bitrate instanceof Quality)
35
- && (!Number.isInteger(videoOptions.bitrate) || videoOptions.bitrate <= 0)) {
36
- throw new TypeError('options.video.bitrate, when provided, must be a positive integer or a quality.');
37
- }
38
- if (videoOptions?.width !== undefined
39
- && (!Number.isInteger(videoOptions.width) || videoOptions.width <= 0)) {
40
- throw new TypeError('options.video.width, when provided, must be a positive integer.');
41
- }
42
- if (videoOptions?.height !== undefined
43
- && (!Number.isInteger(videoOptions.height) || videoOptions.height <= 0)) {
44
- throw new TypeError('options.video.height, when provided, must be a positive integer.');
45
- }
46
- if (videoOptions?.fit !== undefined && !['fill', 'contain', 'cover'].includes(videoOptions.fit)) {
47
- throw new TypeError('options.video.fit, when provided, must be one of \'fill\', \'contain\', or \'cover\'.');
48
- }
49
- if (videoOptions?.width !== undefined
50
- && videoOptions.height !== undefined
51
- && videoOptions.fit === undefined) {
52
- throw new TypeError('When both options.video.width and options.video.height are provided, options.video.fit must also be'
53
- + ' provided.');
54
- }
55
- if (videoOptions?.rotate !== undefined && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
56
- throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
57
- }
58
- if (videoOptions?.crop !== undefined) {
59
- validateCropRectangle(videoOptions.crop, 'options.video.');
60
- }
61
- if (videoOptions?.frameRate !== undefined
62
- && (!Number.isFinite(videoOptions.frameRate) || videoOptions.frameRate <= 0)) {
63
- throw new TypeError('options.video.frameRate, when provided, must be a finite positive number.');
64
- }
65
- if (videoOptions?.alpha !== undefined && !['discard', 'keep'].includes(videoOptions.alpha)) {
66
- throw new TypeError('options.video.alpha, when provided, must be either \'discard\' or \'keep\'.');
67
- }
68
- if (videoOptions?.keyFrameInterval !== undefined
69
- && (!Number.isFinite(videoOptions.keyFrameInterval) || videoOptions.keyFrameInterval < 0)) {
70
- throw new TypeError('options.video.keyFrameInterval, when provided, must be a non-negative number.');
71
- }
72
- if (videoOptions?.process !== undefined && typeof videoOptions.process !== 'function') {
73
- throw new TypeError('options.video.process, when provided, must be a function.');
74
- }
75
- if (videoOptions?.processedWidth !== undefined
76
- && (!Number.isInteger(videoOptions.processedWidth) || videoOptions.processedWidth <= 0)) {
77
- throw new TypeError('options.video.processedWidth, when provided, must be a positive integer.');
78
- }
79
- if (videoOptions?.processedHeight !== undefined
80
- && (!Number.isInteger(videoOptions.processedHeight) || videoOptions.processedHeight <= 0)) {
81
- throw new TypeError('options.video.processedHeight, when provided, must be a positive integer.');
82
- }
83
- };
84
- const validateAudioOptions = (audioOptions) => {
85
- if (audioOptions !== undefined && (!audioOptions || typeof audioOptions !== 'object')) {
86
- throw new TypeError('options.audio, when provided, must be an object.');
87
- }
88
- if (audioOptions?.discard !== undefined && typeof audioOptions.discard !== 'boolean') {
89
- throw new TypeError('options.audio.discard, when provided, must be a boolean.');
90
- }
91
- if (audioOptions?.forceTranscode !== undefined && typeof audioOptions.forceTranscode !== 'boolean') {
92
- throw new TypeError('options.audio.forceTranscode, when provided, must be a boolean.');
93
- }
94
- if (audioOptions?.codec !== undefined && !AUDIO_CODECS.includes(audioOptions.codec)) {
95
- throw new TypeError(`options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(', ')}.`);
96
- }
97
- if (audioOptions?.bitrate !== undefined
98
- && !(audioOptions.bitrate instanceof Quality)
99
- && (!Number.isInteger(audioOptions.bitrate) || audioOptions.bitrate <= 0)) {
100
- throw new TypeError('options.audio.bitrate, when provided, must be a positive integer or a quality.');
101
- }
102
- if (audioOptions?.numberOfChannels !== undefined
103
- && (!Number.isInteger(audioOptions.numberOfChannels) || audioOptions.numberOfChannels <= 0)) {
104
- throw new TypeError('options.audio.numberOfChannels, when provided, must be a positive integer.');
105
- }
106
- if (audioOptions?.sampleRate !== undefined
107
- && (!Number.isInteger(audioOptions.sampleRate) || audioOptions.sampleRate <= 0)) {
108
- throw new TypeError('options.audio.sampleRate, when provided, must be a positive integer.');
109
- }
110
- if (audioOptions?.process !== undefined && typeof audioOptions.process !== 'function') {
111
- throw new TypeError('options.audio.process, when provided, must be a function.');
112
- }
113
- if (audioOptions?.processedNumberOfChannels !== undefined
114
- && (!Number.isInteger(audioOptions.processedNumberOfChannels) || audioOptions.processedNumberOfChannels <= 0)) {
115
- throw new TypeError('options.audio.processedNumberOfChannels, when provided, must be a positive integer.');
116
- }
117
- if (audioOptions?.processedSampleRate !== undefined
118
- && (!Number.isInteger(audioOptions.processedSampleRate) || audioOptions.processedSampleRate <= 0)) {
119
- throw new TypeError('options.audio.processedSampleRate, when provided, must be a positive integer.');
120
- }
121
- };
122
- const validateSubtitleOptions = (subtitleOptions) => {
123
- if (subtitleOptions !== undefined && (!subtitleOptions || typeof subtitleOptions !== 'object')) {
124
- throw new TypeError('options.subtitle, when provided, must be an object.');
125
- }
126
- if (subtitleOptions?.discard !== undefined && typeof subtitleOptions.discard !== 'boolean') {
127
- throw new TypeError('options.subtitle.discard, when provided, must be a boolean.');
128
- }
129
- if (subtitleOptions?.codec !== undefined && !SUBTITLE_CODECS.includes(subtitleOptions.codec)) {
130
- throw new TypeError(`options.subtitle.codec, when provided, must be one of: ${SUBTITLE_CODECS.join(', ')}.`);
131
- }
132
- };
133
- const FALLBACK_NUMBER_OF_CHANNELS = 2;
134
- const FALLBACK_SAMPLE_RATE = 48000;
135
- /**
136
- * Represents a media file conversion process, used to convert one media file into another. In addition to conversion,
137
- * this class can be used to resize and rotate video, resample audio, drop tracks, or trim to a specific time range.
138
- * @group Conversion
139
- * @public
140
- */
141
- export class Conversion {
142
- /** Initializes a new conversion process without starting the conversion. */
143
- static async init(options) {
144
- const conversion = new Conversion(options);
145
- await conversion._init();
146
- return conversion;
147
- }
148
- /** Creates a new Conversion instance (duh). */
149
- constructor(options) {
150
- /** @internal */
151
- this._addedCounts = {
152
- video: 0,
153
- audio: 0,
154
- subtitle: 0,
155
- };
156
- /** @internal */
157
- this._totalTrackCount = 0;
158
- /** @internal */
159
- this._trackPromises = [];
160
- /** @internal */
161
- this._executed = false;
162
- /** @internal */
163
- this._synchronizer = new TrackSynchronizer();
164
- /** @internal */
165
- this._totalDuration = null;
166
- /** @internal */
167
- this._maxTimestamps = new Map(); // Track ID -> timestamp
168
- /** @internal */
169
- this._canceled = false;
170
- /** @internal */
171
- this._externalSubtitleSources = [];
172
- /**
173
- * A callback that is fired whenever the conversion progresses. Returns a number between 0 and 1, indicating the
174
- * completion of the conversion. Note that a progress of 1 doesn't necessarily mean the conversion is complete;
175
- * the conversion is complete once `execute()` resolves.
176
- *
177
- * In order for progress to be computed, this property must be set before `execute` is called.
178
- */
179
- this.onProgress = undefined;
180
- /** @internal */
181
- this._computeProgress = false;
182
- /** @internal */
183
- this._lastProgress = 0;
184
- /**
185
- * Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
186
- * the `discardedTracks` field for reasons.
187
- */
188
- this.isValid = false;
189
- /** The list of tracks that are included in the output file. */
190
- this.utilizedTracks = [];
191
- /** The list of tracks from the input file that have been discarded, alongside the discard reason. */
192
- this.discardedTracks = [];
193
- if (!options || typeof options !== 'object') {
194
- throw new TypeError('options must be an object.');
195
- }
196
- if (!(options.input instanceof Input)) {
197
- throw new TypeError('options.input must be an Input.');
198
- }
199
- if (!(options.output instanceof Output)) {
200
- throw new TypeError('options.output must be an Output.');
201
- }
202
- if (options.output._tracks.length > 0
203
- || Object.keys(options.output._metadataTags).length > 0
204
- || options.output.state !== 'pending') {
205
- throw new TypeError('options.output must be fresh: no tracks or metadata tags added and not started.');
206
- }
207
- if (typeof options.video !== 'function') {
208
- validateVideoOptions(options.video);
209
- }
210
- if (typeof options.audio !== 'function') {
211
- validateAudioOptions(options.audio);
212
- }
213
- if (typeof options.subtitle !== 'function') {
214
- validateSubtitleOptions(options.subtitle);
215
- }
216
- if (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
217
- throw new TypeError('options.trim, when provided, must be an object.');
218
- }
219
- if (options.trim?.start !== undefined && (!Number.isFinite(options.trim.start) || options.trim.start < 0)) {
220
- throw new TypeError('options.trim.start, when provided, must be a non-negative number.');
221
- }
222
- if (options.trim?.end !== undefined && (!Number.isFinite(options.trim.end) || options.trim.end < 0)) {
223
- throw new TypeError('options.trim.end, when provided, must be a non-negative number.');
224
- }
225
- if (options.trim?.start !== undefined
226
- && options.trim.end !== undefined
227
- && options.trim.start >= options.trim.end) {
228
- throw new TypeError('options.trim.start must be less than options.trim.end.');
229
- }
230
- if (options.tags !== undefined
231
- && (typeof options.tags !== 'object' || !options.tags)
232
- && typeof options.tags !== 'function') {
233
- throw new TypeError('options.tags, when provided, must be an object or a function.');
234
- }
235
- if (typeof options.tags === 'object') {
236
- validateMetadataTags(options.tags);
237
- }
238
- if (options.showWarnings !== undefined && typeof options.showWarnings !== 'boolean') {
239
- throw new TypeError('options.showWarnings, when provided, must be a boolean.');
240
- }
241
- this._options = options;
242
- this.input = options.input;
243
- this.output = options.output;
244
- this._startTimestamp = options.trim?.start ?? 0;
245
- this._endTimestamp = options.trim?.end ?? Infinity;
246
- const { promise: started, resolve: start } = promiseWithResolvers();
247
- this._started = started;
248
- this._start = start;
249
- }
250
- /** @internal */
251
- async _init() {
252
- const inputTracks = await this.input.getTracks();
253
- const outputTrackCounts = this.output.format.getSupportedTrackCounts();
254
- let nVideo = 1;
255
- let nAudio = 1;
256
- let nSubtitle = 1;
257
- for (const track of inputTracks) {
258
- let trackOptions = undefined;
259
- if (track.isVideoTrack()) {
260
- if (this._options.video) {
261
- if (typeof this._options.video === 'function') {
262
- trackOptions = await this._options.video(track, nVideo);
263
- validateVideoOptions(trackOptions);
264
- nVideo++;
265
- }
266
- else {
267
- trackOptions = this._options.video;
268
- }
269
- }
270
- }
271
- else if (track.isAudioTrack()) {
272
- if (this._options.audio) {
273
- if (typeof this._options.audio === 'function') {
274
- trackOptions = await this._options.audio(track, nAudio);
275
- validateAudioOptions(trackOptions);
276
- nAudio++;
277
- }
278
- else {
279
- trackOptions = this._options.audio;
280
- }
281
- }
282
- }
283
- else if (track.isSubtitleTrack()) {
284
- if (this._options.subtitle) {
285
- if (typeof this._options.subtitle === 'function') {
286
- trackOptions = await this._options.subtitle(track, nSubtitle);
287
- validateSubtitleOptions(trackOptions);
288
- nSubtitle++;
289
- }
290
- else {
291
- trackOptions = this._options.subtitle;
292
- }
293
- }
294
- }
295
- else {
296
- assert(false);
297
- }
298
- if (trackOptions?.discard) {
299
- this.discardedTracks.push({
300
- track,
301
- reason: 'discarded_by_user',
302
- });
303
- continue;
304
- }
305
- if (this._totalTrackCount === outputTrackCounts.total.max) {
306
- this.discardedTracks.push({
307
- track,
308
- reason: 'max_track_count_reached',
309
- });
310
- continue;
311
- }
312
- if (this._addedCounts[track.type] === outputTrackCounts[track.type].max) {
313
- this.discardedTracks.push({
314
- track,
315
- reason: 'max_track_count_of_type_reached',
316
- });
317
- continue;
318
- }
319
- if (track.isVideoTrack()) {
320
- await this._processVideoTrack(track, (trackOptions ?? {}));
321
- }
322
- else if (track.isAudioTrack()) {
323
- await this._processAudioTrack(track, (trackOptions ?? {}));
324
- }
325
- else if (track.isSubtitleTrack()) {
326
- await this._processSubtitleTrack(track, (trackOptions ?? {}));
327
- }
328
- }
329
- // Now, let's deal with metadata tags
330
- const inputTags = await this.input.getMetadataTags();
331
- let outputTags;
332
- if (this._options.tags) {
333
- const result = typeof this._options.tags === 'function'
334
- ? await this._options.tags(inputTags)
335
- : this._options.tags;
336
- validateMetadataTags(result);
337
- outputTags = result;
338
- }
339
- else {
340
- outputTags = inputTags;
341
- }
342
- // Somewhat dirty but pragmatic
343
- const inputAndOutputFormatMatch = (await this.input.getFormat()).mimeType === this.output.format.mimeType;
344
- const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
345
- if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
346
- // If the input and output formats aren't the same, copying over raw metadata tags makes no sense and only
347
- // results in junk tags, so let's cut them out.
348
- delete outputTags.raw;
349
- }
350
- this.output.setMetadataTags(outputTags);
351
- // Let's check if the conversion can actually be executed
352
- this.isValid = this._totalTrackCount >= outputTrackCounts.total.min
353
- && this._addedCounts.video >= outputTrackCounts.video.min
354
- && this._addedCounts.audio >= outputTrackCounts.audio.min
355
- && this._addedCounts.subtitle >= outputTrackCounts.subtitle.min;
356
- if (this._options.showWarnings ?? true) {
357
- const warnElements = [];
358
- const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discarded_by_user');
359
- if (unintentionallyDiscardedTracks.length > 0) {
360
- // Let's give the user a notice/warning about discarded tracks so they aren't confused
361
- warnElements.push('Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks);
362
- }
363
- if (!this.isValid) {
364
- warnElements.push('\n\n' + this._getInvalidityExplanation().join(''));
365
- }
366
- if (warnElements.length > 0) {
367
- console.warn(...warnElements);
368
- }
369
- }
370
- }
371
- /** @internal */
372
- _getInvalidityExplanation() {
373
- const elements = [];
374
- if (this.discardedTracks.length === 0) {
375
- elements.push('Due to missing tracks, this conversion cannot be executed.');
376
- }
377
- else {
378
- const encodabilityIsTheProblem = this.discardedTracks.every(x => x.reason === 'discarded_by_user' || x.reason === 'no_encodable_target_codec');
379
- elements.push('Due to discarded tracks, this conversion cannot be executed.');
380
- if (encodabilityIsTheProblem) {
381
- const codecs = this.discardedTracks.flatMap((x) => {
382
- if (x.reason === 'discarded_by_user')
383
- return [];
384
- if (x.track.type === 'video') {
385
- return this.output.format.getSupportedVideoCodecs();
386
- }
387
- else if (x.track.type === 'audio') {
388
- return this.output.format.getSupportedAudioCodecs();
389
- }
390
- else {
391
- return this.output.format.getSupportedSubtitleCodecs();
392
- }
393
- });
394
- if (codecs.length === 1) {
395
- elements.push(`\nTracks were discarded because your environment is not able to encode '${codecs[0]}'.`);
396
- }
397
- else {
398
- elements.push('\nTracks were discarded because your environment is not able to encode any of the following'
399
- + ` codecs: ${codecs.map(x => `'${x}'`).join(', ')}.`);
400
- }
401
- if (codecs.includes('mp3')) {
402
- elements.push(`\nThe @mediabunny/mp3-encoder extension package provides support for encoding MP3.`);
403
- }
404
- }
405
- else {
406
- elements.push('\nCheck the discardedTracks field for more info.');
407
- }
408
- }
409
- return elements;
410
- }
411
- /**
412
- * Adds an external subtitle track to the output. This can be called after `init()` but before `execute()`.
413
- * This is useful for adding subtitle tracks from separate files that are not part of the input video.
414
- *
415
- * @param source - The subtitle source to add
416
- * @param metadata - Optional metadata for the subtitle track
417
- * @param contentProvider - Optional async function that will be called after the output starts to add content to the subtitle source
418
- */
419
- addExternalSubtitleTrack(source, metadata = {}, contentProvider) {
420
- if (this._executed) {
421
- throw new Error('Cannot add subtitle tracks after conversion has been executed.');
422
- }
423
- if (this.output.state !== 'pending') {
424
- throw new Error('Cannot add subtitle tracks after output has been started.');
425
- }
426
- // Check track count limits
427
- const outputTrackCounts = this.output.format.getSupportedTrackCounts();
428
- const currentSubtitleCount = this._addedCounts.subtitle + this._externalSubtitleSources.length;
429
- if (currentSubtitleCount >= outputTrackCounts.subtitle.max) {
430
- throw new Error(`Cannot add more subtitle tracks. Maximum of ${outputTrackCounts.subtitle.max} subtitle track(s) allowed.`);
431
- }
432
- const totalTrackCount = this._totalTrackCount + this._externalSubtitleSources.length + 1;
433
- if (totalTrackCount > outputTrackCounts.total.max) {
434
- throw new Error(`Cannot add more tracks. Maximum of ${outputTrackCounts.total.max} total track(s) allowed.`);
435
- }
436
- this._externalSubtitleSources.push({ source, metadata, contentProvider });
437
- // Update validity check to include external subtitles
438
- this.isValid = this._totalTrackCount + this._externalSubtitleSources.length >= outputTrackCounts.total.min
439
- && this._addedCounts.video >= outputTrackCounts.video.min
440
- && this._addedCounts.audio >= outputTrackCounts.audio.min
441
- && this._addedCounts.subtitle + this._externalSubtitleSources.length >= outputTrackCounts.subtitle.min;
442
- }
443
- /**
444
- * Executes the conversion process. Resolves once conversion is complete.
445
- *
446
- * Will throw if `isValid` is `false`.
447
- */
448
- async execute() {
449
- if (!this.isValid) {
450
- throw new Error('Cannot execute this conversion because its output configuration is invalid. Make sure to always check'
451
- + ' the isValid field before executing a conversion.\n'
452
- + this._getInvalidityExplanation().join(''));
453
- }
454
- if (this._executed) {
455
- throw new Error('Conversion cannot be executed twice.');
456
- }
457
- this._executed = true;
458
- if (this.onProgress) {
459
- this._computeProgress = true;
460
- this._totalDuration = Math.min((await this.input.computeDuration()) - this._startTimestamp, this._endTimestamp - this._startTimestamp);
461
- for (const track of this.utilizedTracks) {
462
- this._maxTimestamps.set(track.id, 0);
463
- }
464
- this.onProgress?.(0);
465
- }
466
- // Add external subtitle tracks before starting the output
467
- for (const { source, metadata } of this._externalSubtitleSources) {
468
- this.output.addSubtitleTrack(source, metadata);
469
- }
470
- await this.output.start();
471
- this._start();
472
- // Now that output has started and tracks are connected, run content providers
473
- const contentProviderPromises = this._externalSubtitleSources
474
- .filter(s => s.contentProvider)
475
- .map(s => s.contentProvider());
476
- if (contentProviderPromises.length > 0) {
477
- this._trackPromises.push(...contentProviderPromises);
478
- }
479
- try {
480
- await Promise.all(this._trackPromises);
481
- }
482
- catch (error) {
483
- if (!this._canceled) {
484
- // Make sure to cancel to stop other encoding processes and clean up resources
485
- void this.cancel();
486
- }
487
- throw error;
488
- }
489
- if (this._canceled) {
490
- await new Promise(() => { }); // Never resolve
491
- }
492
- await this.output.finalize();
493
- if (this._computeProgress) {
494
- this.onProgress?.(1);
495
- }
496
- }
497
- /** Cancels the conversion process. Does nothing if the conversion is already complete. */
498
- async cancel() {
499
- if (this.output.state === 'finalizing' || this.output.state === 'finalized') {
500
- return;
501
- }
502
- if (this._canceled) {
503
- console.warn('Conversion already canceled.');
504
- return;
505
- }
506
- this._canceled = true;
507
- await this.output.cancel();
508
- }
509
- /** @internal */
510
- async _processVideoTrack(track, trackOptions) {
511
- const sourceCodec = track.codec;
512
- if (!sourceCodec) {
513
- this.discardedTracks.push({
514
- track,
515
- reason: 'unknown_source_codec',
516
- });
517
- return;
518
- }
519
- let videoSource;
520
- const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
521
- const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
522
- const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0
523
- ? [track.codedWidth, track.codedHeight]
524
- : [track.codedHeight, track.codedWidth];
525
- const crop = trackOptions.crop;
526
- if (crop) {
527
- clampCropRectangle(crop, rotatedWidth, rotatedHeight);
528
- }
529
- const [originalWidth, originalHeight] = crop
530
- ? [crop.width, crop.height]
531
- : [rotatedWidth, rotatedHeight];
532
- let width = originalWidth;
533
- let height = originalHeight;
534
- const aspectRatio = width / height;
535
- // A lot of video encoders require that the dimensions be multiples of 2
536
- const ceilToMultipleOfTwo = (value) => Math.ceil(value / 2) * 2;
537
- if (trackOptions.width !== undefined && trackOptions.height === undefined) {
538
- width = ceilToMultipleOfTwo(trackOptions.width);
539
- height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
540
- }
541
- else if (trackOptions.width === undefined && trackOptions.height !== undefined) {
542
- height = ceilToMultipleOfTwo(trackOptions.height);
543
- width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
544
- }
545
- else if (trackOptions.width !== undefined && trackOptions.height !== undefined) {
546
- width = ceilToMultipleOfTwo(trackOptions.width);
547
- height = ceilToMultipleOfTwo(trackOptions.height);
548
- }
549
- const firstTimestamp = await track.getFirstTimestamp();
550
- const needsTranscode = !!trackOptions.forceTranscode
551
- || this._startTimestamp > 0
552
- || firstTimestamp < 0
553
- || !!trackOptions.frameRate
554
- || trackOptions.keyFrameInterval !== undefined
555
- || trackOptions.process !== undefined;
556
- let needsRerender = width !== originalWidth
557
- || height !== originalHeight
558
- // TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not
559
- // performance-optimal, but right now there's no other way because we can't change the track rotation
560
- // metadata after the output has already started. Should be possible with API changes in v2, though!
561
- || (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined))
562
- || !!crop;
563
- const alpha = trackOptions.alpha ?? 'discard';
564
- let videoCodecs = this.output.format.getSupportedVideoCodecs();
565
- if (!needsTranscode
566
- && !trackOptions.bitrate
567
- && !needsRerender
568
- && videoCodecs.includes(sourceCodec)
569
- && (!trackOptions.codec || trackOptions.codec === sourceCodec)) {
570
- // Fast path, we can simply copy over the encoded packets
571
- const source = new EncodedVideoPacketSource(sourceCodec);
572
- videoSource = source;
573
- this._trackPromises.push((async () => {
574
- await this._started;
575
- const sink = new EncodedPacketSink(track);
576
- const decoderConfig = await track.getDecoderConfig();
577
- const meta = { decoderConfig: decoderConfig ?? undefined };
578
- const endPacket = Number.isFinite(this._endTimestamp)
579
- ? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
580
- : undefined;
581
- for await (const packet of sink.packets(undefined, endPacket, { verifyKeyPackets: true })) {
582
- if (this._canceled) {
583
- return;
584
- }
585
- if (alpha === 'discard') {
586
- // Feels hacky given that the rest of the packet is readonly. But, works for now.
587
- delete packet.sideData.alpha;
588
- delete packet.sideData.alphaByteLength;
589
- }
590
- this._reportProgress(track.id, packet.timestamp);
591
- await source.add(packet, meta);
592
- if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
593
- await this._synchronizer.wait(packet.timestamp);
594
- }
595
- }
596
- source.close();
597
- this._synchronizer.closeTrack(track.id);
598
- })());
599
- }
600
- else {
601
- // We need to decode & reencode the video
602
- const canDecode = await track.canDecode();
603
- if (!canDecode) {
604
- this.discardedTracks.push({
605
- track,
606
- reason: 'undecodable_source_codec',
607
- });
608
- return;
609
- }
610
- if (trackOptions.codec) {
611
- videoCodecs = videoCodecs.filter(codec => codec === trackOptions.codec);
612
- }
613
- const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
614
- const encodableCodec = await getFirstEncodableVideoCodec(videoCodecs, {
615
- width: trackOptions.process && trackOptions.processedWidth
616
- ? trackOptions.processedWidth
617
- : width,
618
- height: trackOptions.process && trackOptions.processedHeight
619
- ? trackOptions.processedHeight
620
- : height,
621
- bitrate,
622
- });
623
- if (!encodableCodec) {
624
- this.discardedTracks.push({
625
- track,
626
- reason: 'no_encodable_target_codec',
627
- });
628
- return;
629
- }
630
- const encodingConfig = {
631
- codec: encodableCodec,
632
- bitrate,
633
- keyFrameInterval: trackOptions.keyFrameInterval,
634
- sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
635
- alpha,
636
- };
637
- const source = new VideoSampleSource(encodingConfig);
638
- videoSource = source;
639
- if (!needsRerender) {
640
- // If we're directly passing decoded samples back to the encoder, sometimes the encoder may error due
641
- // to lack of support of certain video frame formats, like when HDR is at play. To check for this, we
642
- // first try to pass a single frame to the encoder to see how it behaves. If it throws, we then fall
643
- // back to the rerender path.
644
- //
645
- // Creating a new temporary Output is sort of hacky, but due to a lack of an isolated encoder API right
646
- // now, this is the simplest way. Will refactor in the future! TODO
647
- const tempOutput = new Output({
648
- format: new Mp4OutputFormat(), // Supports all video codecs
649
- target: new NullTarget(),
650
- });
651
- const tempSource = new VideoSampleSource(encodingConfig);
652
- tempOutput.addVideoTrack(tempSource);
653
- await tempOutput.start();
654
- const sink = new VideoSampleSink(track);
655
- const firstSample = await sink.getSample(firstTimestamp); // Let's just use the first sample
656
- if (firstSample) {
657
- try {
658
- await tempSource.add(firstSample);
659
- firstSample.close();
660
- await tempOutput.finalize();
661
- }
662
- catch (error) {
663
- console.info('Error when probing encoder support. Falling back to rerender path.', error);
664
- needsRerender = true;
665
- void tempOutput.cancel();
666
- }
667
- }
668
- else {
669
- await tempOutput.cancel();
670
- }
671
- }
672
- if (needsRerender) {
673
- this._trackPromises.push((async () => {
674
- await this._started;
675
- const sink = new CanvasSink(track, {
676
- width,
677
- height,
678
- fit: trackOptions.fit ?? 'fill',
679
- rotation: totalRotation, // Bake the rotation into the output
680
- crop: trackOptions.crop,
681
- poolSize: 1,
682
- alpha: alpha === 'keep',
683
- });
684
- const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
685
- const frameRate = trackOptions.frameRate;
686
- let lastCanvas = null;
687
- let lastCanvasTimestamp = null;
688
- let lastCanvasEndTimestamp = null;
689
- /** Repeats the last sample to pad out the time until the specified timestamp. */
690
- const padFrames = async (until) => {
691
- assert(lastCanvas);
692
- assert(frameRate !== undefined);
693
- const frameDifference = Math.round((until - lastCanvasTimestamp) * frameRate);
694
- for (let i = 1; i < frameDifference; i++) {
695
- const sample = new VideoSample(lastCanvas, {
696
- timestamp: lastCanvasTimestamp + i / frameRate,
697
- duration: 1 / frameRate,
698
- });
699
- await this._registerVideoSample(track, trackOptions, source, sample);
700
- sample.close();
701
- }
702
- };
703
- for await (const { canvas, timestamp, duration } of iterator) {
704
- if (this._canceled) {
705
- return;
706
- }
707
- let adjustedSampleTimestamp = Math.max(timestamp - this._startTimestamp, 0);
708
- lastCanvasEndTimestamp = adjustedSampleTimestamp + duration;
709
- if (frameRate !== undefined) {
710
- // Logic for skipping/repeating frames when a frame rate is set
711
- const alignedTimestamp = Math.floor(adjustedSampleTimestamp * frameRate) / frameRate;
712
- if (lastCanvas !== null) {
713
- if (alignedTimestamp <= lastCanvasTimestamp) {
714
- lastCanvas = canvas;
715
- lastCanvasTimestamp = alignedTimestamp;
716
- // Skip this sample, since we already added one for this frame
717
- continue;
718
- }
719
- else {
720
- // Check if we may need to repeat the previous frame
721
- await padFrames(alignedTimestamp);
722
- }
723
- }
724
- adjustedSampleTimestamp = alignedTimestamp;
725
- }
726
- const sample = new VideoSample(canvas, {
727
- timestamp: adjustedSampleTimestamp,
728
- duration: frameRate !== undefined ? 1 / frameRate : duration,
729
- });
730
- await this._registerVideoSample(track, trackOptions, source, sample);
731
- sample.close();
732
- if (frameRate !== undefined) {
733
- lastCanvas = canvas;
734
- lastCanvasTimestamp = adjustedSampleTimestamp;
735
- }
736
- }
737
- if (lastCanvas) {
738
- assert(lastCanvasEndTimestamp !== null);
739
- assert(frameRate !== undefined);
740
- // If necessary, pad until the end timestamp of the last sample
741
- await padFrames(Math.floor(lastCanvasEndTimestamp * frameRate) / frameRate);
742
- }
743
- source.close();
744
- this._synchronizer.closeTrack(track.id);
745
- })());
746
- }
747
- else {
748
- this._trackPromises.push((async () => {
749
- await this._started;
750
- const sink = new VideoSampleSink(track);
751
- const frameRate = trackOptions.frameRate;
752
- let lastSample = null;
753
- let lastSampleTimestamp = null;
754
- let lastSampleEndTimestamp = null;
755
- /** Repeats the last sample to pad out the time until the specified timestamp. */
756
- const padFrames = async (until) => {
757
- assert(lastSample);
758
- assert(frameRate !== undefined);
759
- const frameDifference = Math.round((until - lastSampleTimestamp) * frameRate);
760
- for (let i = 1; i < frameDifference; i++) {
761
- lastSample.setTimestamp(lastSampleTimestamp + i / frameRate);
762
- lastSample.setDuration(1 / frameRate);
763
- await this._registerVideoSample(track, trackOptions, source, lastSample);
764
- }
765
- lastSample.close();
766
- };
767
- for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
768
- if (this._canceled) {
769
- lastSample?.close();
770
- return;
771
- }
772
- let adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
773
- lastSampleEndTimestamp = adjustedSampleTimestamp + sample.duration;
774
- if (frameRate !== undefined) {
775
- // Logic for skipping/repeating frames when a frame rate is set
776
- const alignedTimestamp = Math.floor(adjustedSampleTimestamp * frameRate) / frameRate;
777
- if (lastSample !== null) {
778
- if (alignedTimestamp <= lastSampleTimestamp) {
779
- lastSample.close();
780
- lastSample = sample;
781
- lastSampleTimestamp = alignedTimestamp;
782
- // Skip this sample, since we already added one for this frame
783
- continue;
784
- }
785
- else {
786
- // Check if we may need to repeat the previous frame
787
- await padFrames(alignedTimestamp);
788
- }
789
- }
790
- adjustedSampleTimestamp = alignedTimestamp;
791
- sample.setDuration(1 / frameRate);
792
- }
793
- sample.setTimestamp(adjustedSampleTimestamp);
794
- await this._registerVideoSample(track, trackOptions, source, sample);
795
- if (frameRate !== undefined) {
796
- lastSample = sample;
797
- lastSampleTimestamp = adjustedSampleTimestamp;
798
- }
799
- else {
800
- sample.close();
801
- }
802
- }
803
- if (lastSample) {
804
- assert(lastSampleEndTimestamp !== null);
805
- assert(frameRate !== undefined);
806
- // If necessary, pad until the end timestamp of the last sample
807
- await padFrames(Math.floor(lastSampleEndTimestamp * frameRate) / frameRate);
808
- }
809
- source.close();
810
- this._synchronizer.closeTrack(track.id);
811
- })());
812
- }
813
- }
814
- this.output.addVideoTrack(videoSource, {
815
- frameRate: trackOptions.frameRate,
816
- // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
817
- languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
818
- name: track.name ?? undefined,
819
- disposition: track.disposition,
820
- rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
821
- });
822
- this._addedCounts.video++;
823
- this._totalTrackCount++;
824
- this.utilizedTracks.push(track);
825
- }
826
- /** @internal */
827
- async _registerVideoSample(track, trackOptions, source, sample) {
828
- if (this._canceled) {
829
- return;
830
- }
831
- this._reportProgress(track.id, sample.timestamp);
832
- let finalSamples;
833
- if (!trackOptions.process) {
834
- finalSamples = [sample];
835
- }
836
- else {
837
- let processed = trackOptions.process(sample);
838
- if (processed instanceof Promise)
839
- processed = await processed;
840
- if (!Array.isArray(processed)) {
841
- processed = processed === null ? [] : [processed];
842
- }
843
- finalSamples = processed.map((x) => {
844
- if (x instanceof VideoSample) {
845
- return x;
846
- }
847
- if (typeof VideoFrame !== 'undefined' && x instanceof VideoFrame) {
848
- return new VideoSample(x);
849
- }
850
- // Calling the VideoSample constructor here will automatically handle input validation for us
851
- // (it throws for any non-legal argument).
852
- return new VideoSample(x, {
853
- timestamp: sample.timestamp,
854
- duration: sample.duration,
855
- });
856
- });
857
- }
858
- for (const finalSample of finalSamples) {
859
- if (this._canceled) {
860
- break;
861
- }
862
- await source.add(finalSample);
863
- if (this._synchronizer.shouldWait(track.id, finalSample.timestamp)) {
864
- await this._synchronizer.wait(finalSample.timestamp);
865
- }
866
- }
867
- for (const finalSample of finalSamples) {
868
- if (finalSample !== sample) {
869
- finalSample.close();
870
- }
871
- }
872
- }
873
- /** @internal */
874
- async _processAudioTrack(track, trackOptions) {
875
- const sourceCodec = track.codec;
876
- if (!sourceCodec) {
877
- this.discardedTracks.push({
878
- track,
879
- reason: 'unknown_source_codec',
880
- });
881
- return;
882
- }
883
- let audioSource;
884
- const originalNumberOfChannels = track.numberOfChannels;
885
- const originalSampleRate = track.sampleRate;
886
- const firstTimestamp = await track.getFirstTimestamp();
887
- let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
888
- let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
889
- let needsResample = numberOfChannels !== originalNumberOfChannels
890
- || sampleRate !== originalSampleRate
891
- || this._startTimestamp > 0
892
- || firstTimestamp < 0;
893
- let audioCodecs = this.output.format.getSupportedAudioCodecs();
894
- if (!trackOptions.forceTranscode
895
- && !trackOptions.bitrate
896
- && !needsResample
897
- && audioCodecs.includes(sourceCodec)
898
- && (!trackOptions.codec || trackOptions.codec === sourceCodec)
899
- && !trackOptions.process) {
900
- // Fast path, we can simply copy over the encoded packets
901
- const source = new EncodedAudioPacketSource(sourceCodec);
902
- audioSource = source;
903
- this._trackPromises.push((async () => {
904
- await this._started;
905
- const sink = new EncodedPacketSink(track);
906
- const decoderConfig = await track.getDecoderConfig();
907
- const meta = { decoderConfig: decoderConfig ?? undefined };
908
- const endPacket = Number.isFinite(this._endTimestamp)
909
- ? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
910
- : undefined;
911
- for await (const packet of sink.packets(undefined, endPacket)) {
912
- if (this._canceled) {
913
- return;
914
- }
915
- this._reportProgress(track.id, packet.timestamp);
916
- await source.add(packet, meta);
917
- if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
918
- await this._synchronizer.wait(packet.timestamp);
919
- }
920
- }
921
- source.close();
922
- this._synchronizer.closeTrack(track.id);
923
- })());
924
- }
925
- else {
926
- // We need to decode & reencode the audio
927
- const canDecode = await track.canDecode();
928
- if (!canDecode) {
929
- this.discardedTracks.push({
930
- track,
931
- reason: 'undecodable_source_codec',
932
- });
933
- return;
934
- }
935
- let codecOfChoice = null;
936
- if (trackOptions.codec) {
937
- audioCodecs = audioCodecs.filter(codec => codec === trackOptions.codec);
938
- }
939
- const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
940
- const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
941
- numberOfChannels: trackOptions.process && trackOptions.processedNumberOfChannels
942
- ? trackOptions.processedNumberOfChannels
943
- : numberOfChannels,
944
- sampleRate: trackOptions.process && trackOptions.processedSampleRate
945
- ? trackOptions.processedSampleRate
946
- : sampleRate,
947
- bitrate,
948
- });
949
- if (!encodableCodecs.some(codec => NON_PCM_AUDIO_CODECS.includes(codec))
950
- && audioCodecs.some(codec => NON_PCM_AUDIO_CODECS.includes(codec))
951
- && (numberOfChannels !== FALLBACK_NUMBER_OF_CHANNELS || sampleRate !== FALLBACK_SAMPLE_RATE)) {
952
- // We could not find a compatible non-PCM codec despite the container supporting them. This can be
953
- // caused by strange channel count or sample rate configurations. Therefore, let's try again but with
954
- // fallback parameters.
955
- const encodableCodecsWithDefaultParams = await getEncodableAudioCodecs(audioCodecs, {
956
- numberOfChannels: FALLBACK_NUMBER_OF_CHANNELS,
957
- sampleRate: FALLBACK_SAMPLE_RATE,
958
- bitrate,
959
- });
960
- const nonPcmCodec = encodableCodecsWithDefaultParams
961
- .find(codec => NON_PCM_AUDIO_CODECS.includes(codec));
962
- if (nonPcmCodec) {
963
- // We are able to encode using a non-PCM codec, but it'll require resampling
964
- needsResample = true;
965
- codecOfChoice = nonPcmCodec;
966
- numberOfChannels = FALLBACK_NUMBER_OF_CHANNELS;
967
- sampleRate = FALLBACK_SAMPLE_RATE;
968
- }
969
- }
970
- else {
971
- codecOfChoice = encodableCodecs[0] ?? null;
972
- }
973
- if (codecOfChoice === null) {
974
- this.discardedTracks.push({
975
- track,
976
- reason: 'no_encodable_target_codec',
977
- });
978
- return;
979
- }
980
- if (needsResample) {
981
- audioSource = this._resampleAudio(track, trackOptions, codecOfChoice, numberOfChannels, sampleRate, bitrate);
982
- }
983
- else {
984
- const source = new AudioSampleSource({
985
- codec: codecOfChoice,
986
- bitrate,
987
- });
988
- audioSource = source;
989
- this._trackPromises.push((async () => {
990
- await this._started;
991
- const sink = new AudioSampleSink(track);
992
- for await (const sample of sink.samples(undefined, this._endTimestamp)) {
993
- if (this._canceled) {
994
- return;
995
- }
996
- await this._registerAudioSample(track, trackOptions, source, sample);
997
- sample.close();
998
- }
999
- source.close();
1000
- this._synchronizer.closeTrack(track.id);
1001
- })());
1002
- }
1003
- }
1004
- this.output.addAudioTrack(audioSource, {
1005
- // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
1006
- languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
1007
- name: track.name ?? undefined,
1008
- disposition: track.disposition,
1009
- });
1010
- this._addedCounts.audio++;
1011
- this._totalTrackCount++;
1012
- this.utilizedTracks.push(track);
1013
- }
1014
- /** @internal */
1015
- async _registerAudioSample(track, trackOptions, source, sample) {
1016
- if (this._canceled) {
1017
- return;
1018
- }
1019
- this._reportProgress(track.id, sample.timestamp);
1020
- let finalSamples;
1021
- if (!trackOptions.process) {
1022
- finalSamples = [sample];
1023
- }
1024
- else {
1025
- let processed = trackOptions.process(sample);
1026
- if (processed instanceof Promise)
1027
- processed = await processed;
1028
- if (!Array.isArray(processed)) {
1029
- processed = processed === null ? [] : [processed];
1030
- }
1031
- if (!processed.every(x => x instanceof AudioSample)) {
1032
- throw new TypeError('The audio process function must return an AudioSample, null, or an array of AudioSamples.');
1033
- }
1034
- finalSamples = processed;
1035
- }
1036
- for (const finalSample of finalSamples) {
1037
- if (this._canceled) {
1038
- break;
1039
- }
1040
- await source.add(finalSample);
1041
- if (this._synchronizer.shouldWait(track.id, finalSample.timestamp)) {
1042
- await this._synchronizer.wait(finalSample.timestamp);
1043
- }
1044
- }
1045
- for (const finalSample of finalSamples) {
1046
- if (finalSample !== sample) {
1047
- finalSample.close();
1048
- }
1049
- }
1050
- }
1051
- /** @internal */
1052
- async _processSubtitleTrack(track, trackOptions) {
1053
- const sourceCodec = track.codec;
1054
- if (!sourceCodec) {
1055
- this.discardedTracks.push({
1056
- track,
1057
- reason: 'unknown_source_codec',
1058
- });
1059
- return;
1060
- }
1061
- // Determine target codec
1062
- let targetCodec = trackOptions.codec ?? sourceCodec;
1063
- const supportedCodecs = this.output.format.getSupportedSubtitleCodecs();
1064
- // Check if target codec is supported by output format
1065
- if (!supportedCodecs.includes(targetCodec)) {
1066
- // Try to use source codec if no specific codec was requested
1067
- if (!trackOptions.codec && supportedCodecs.includes(sourceCodec)) {
1068
- targetCodec = sourceCodec;
1069
- }
1070
- else {
1071
- // If a specific codec was requested but not supported, or source codec not supported, discard
1072
- this.discardedTracks.push({
1073
- track,
1074
- reason: 'no_encodable_target_codec',
1075
- });
1076
- return;
1077
- }
1078
- }
1079
- // Create subtitle source
1080
- const subtitleSource = new TextSubtitleSource(targetCodec);
1081
- // Add track promise to extract and add subtitle cues
1082
- this._trackPromises.push((async () => {
1083
- await this._started;
1084
- let subtitleText;
1085
- // If no trim or codec conversion needed, use the efficient export method
1086
- if (this._startTimestamp === 0 && !Number.isFinite(this._endTimestamp) && targetCodec === sourceCodec) {
1087
- subtitleText = await track.exportToText();
1088
- }
1089
- else {
1090
- // Extract and adjust cues for trim/conversion
1091
- const cues = [];
1092
- for await (const cue of track.getCues()) {
1093
- const cueEndTime = cue.timestamp + cue.duration;
1094
- // Apply trim if needed
1095
- if (this._startTimestamp > 0 || Number.isFinite(this._endTimestamp)) {
1096
- // Skip cues completely outside trim range
1097
- if (cueEndTime <= this._startTimestamp || cue.timestamp >= this._endTimestamp) {
1098
- continue;
1099
- }
1100
- // Adjust cue timing
1101
- const adjustedTimestamp = Math.max(cue.timestamp - this._startTimestamp, 0);
1102
- const adjustedEndTime = Math.min(cueEndTime - this._startTimestamp, this._endTimestamp - this._startTimestamp);
1103
- cues.push({
1104
- ...cue,
1105
- timestamp: adjustedTimestamp,
1106
- duration: adjustedEndTime - adjustedTimestamp,
1107
- });
1108
- }
1109
- else {
1110
- cues.push(cue);
1111
- }
1112
- if (this._canceled) {
1113
- return;
1114
- }
1115
- }
1116
- // Convert to target format
1117
- if (targetCodec === 'srt') {
1118
- subtitleText = formatCuesToSrt(cues);
1119
- }
1120
- else if (targetCodec === 'webvtt') {
1121
- subtitleText = formatCuesToWebVTT(cues);
1122
- }
1123
- else if (targetCodec === 'ass' || targetCodec === 'ssa') {
1124
- subtitleText = formatCuesToAss(cues, '');
1125
- }
1126
- else {
1127
- // For other formats (tx3g, ttml), export from track
1128
- subtitleText = await track.exportToText(targetCodec);
1129
- }
1130
- }
1131
- await subtitleSource.add(subtitleText);
1132
- subtitleSource.close();
1133
- })());
1134
- this.output.addSubtitleTrack(subtitleSource, {
1135
- languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
1136
- name: track.name ?? undefined,
1137
- });
1138
- this._addedCounts.subtitle++;
1139
- this._totalTrackCount++;
1140
- this.utilizedTracks.push(track);
1141
- }
1142
- /** @internal */
1143
- _resampleAudio(track, trackOptions, codec, targetNumberOfChannels, targetSampleRate, bitrate) {
1144
- const source = new AudioSampleSource({
1145
- codec,
1146
- bitrate,
1147
- });
1148
- this._trackPromises.push((async () => {
1149
- await this._started;
1150
- const resampler = new AudioResampler({
1151
- targetNumberOfChannels,
1152
- targetSampleRate,
1153
- startTime: this._startTimestamp,
1154
- endTime: this._endTimestamp,
1155
- onSample: sample => this._registerAudioSample(track, trackOptions, source, sample),
1156
- });
1157
- const sink = new AudioSampleSink(track);
1158
- const iterator = sink.samples(this._startTimestamp, this._endTimestamp);
1159
- for await (const sample of iterator) {
1160
- if (this._canceled) {
1161
- return;
1162
- }
1163
- await resampler.add(sample);
1164
- }
1165
- await resampler.finalize();
1166
- source.close();
1167
- this._synchronizer.closeTrack(track.id);
1168
- })());
1169
- return source;
1170
- }
1171
- /** @internal */
1172
- _reportProgress(trackId, endTimestamp) {
1173
- if (!this._computeProgress) {
1174
- return;
1175
- }
1176
- assert(this._totalDuration !== null);
1177
- this._maxTimestamps.set(trackId, Math.max(endTimestamp, this._maxTimestamps.get(trackId)));
1178
- const minTimestamp = Math.min(...this._maxTimestamps.values());
1179
- const newProgress = clamp(minTimestamp / this._totalDuration, 0, 1);
1180
- if (newProgress !== this._lastProgress) {
1181
- this._lastProgress = newProgress;
1182
- this.onProgress?.(newProgress);
1183
- }
1184
- }
1185
- }
1186
- const MAX_TIMESTAMP_GAP = 5;
1187
- /**
1188
- * Utility class for synchronizing multiple track packet consumers with one another. We don't want one consumer to get
1189
- * too out-of-sync with the others, as that may lead to a large number of packets that need to be internally buffered
1190
- * before they can be written. Therefore, we use this class to slow down a consumer if it is too far ahead of the
1191
- * slowest consumer.
1192
- */
1193
- class TrackSynchronizer {
1194
- constructor() {
1195
- this.maxTimestamps = new Map(); // Track ID -> timestamp
1196
- this.resolvers = [];
1197
- }
1198
- computeMinAndMaybeResolve() {
1199
- let newMin = Infinity;
1200
- for (const [, timestamp] of this.maxTimestamps) {
1201
- newMin = Math.min(newMin, timestamp);
1202
- }
1203
- for (let i = 0; i < this.resolvers.length; i++) {
1204
- const entry = this.resolvers[i];
1205
- if (entry.timestamp - newMin < MAX_TIMESTAMP_GAP) {
1206
- // The gap has gotten small enough again, the consumer can continue again
1207
- entry.resolve();
1208
- this.resolvers.splice(i, 1);
1209
- i--;
1210
- }
1211
- }
1212
- return newMin;
1213
- }
1214
- shouldWait(trackId, timestamp) {
1215
- this.maxTimestamps.set(trackId, Math.max(timestamp, this.maxTimestamps.get(trackId) ?? -Infinity));
1216
- const newMin = this.computeMinAndMaybeResolve();
1217
- return timestamp - newMin >= MAX_TIMESTAMP_GAP; // Should wait if it is too far ahead of the slowest consumer
1218
- }
1219
- wait(timestamp) {
1220
- const { promise, resolve } = promiseWithResolvers();
1221
- this.resolvers.push({
1222
- timestamp,
1223
- resolve,
1224
- });
1225
- return promise;
1226
- }
1227
- closeTrack(trackId) {
1228
- this.maxTimestamps.delete(trackId);
1229
- this.computeMinAndMaybeResolve();
1230
- }
1231
- }
1232
- /**
1233
- * Utility class to handle audio resampling, handling both sample rate resampling as well as channel up/downmixing.
1234
- * The advantage over doing this manually rather than using OfflineAudioContext to do it for us is the artifact-free
1235
- * handling of putting multiple resampled audio samples back to back, which produces flaky results using
1236
- * OfflineAudioContext.
1237
- */
1238
- export class AudioResampler {
1239
- constructor(options) {
1240
- this.sourceSampleRate = null;
1241
- this.sourceNumberOfChannels = null;
1242
- this.targetSampleRate = options.targetSampleRate;
1243
- this.targetNumberOfChannels = options.targetNumberOfChannels;
1244
- this.startTime = options.startTime;
1245
- this.endTime = options.endTime;
1246
- this.onSample = options.onSample;
1247
- this.bufferSizeInFrames = Math.floor(this.targetSampleRate * 5.0); // 5 seconds
1248
- this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels;
1249
- this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
1250
- this.bufferStartFrame = 0;
1251
- this.maxWrittenFrame = -1;
1252
- }
1253
- /**
1254
- * Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
1255
- */
1256
- doChannelMixerSetup() {
1257
- assert(this.sourceNumberOfChannels !== null);
1258
- const sourceNum = this.sourceNumberOfChannels;
1259
- const targetNum = this.targetNumberOfChannels;
1260
- // Logic taken from
1261
- // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API
1262
- // Most of the mapping functions are branchless.
1263
- if (sourceNum === 1 && targetNum === 2) {
1264
- // Mono to Stereo: M -> L, M -> R
1265
- this.channelMixer = (sourceData, sourceFrameIndex) => {
1266
- return sourceData[sourceFrameIndex * sourceNum];
1267
- };
1268
- }
1269
- else if (sourceNum === 1 && targetNum === 4) {
1270
- // Mono to Quad: M -> L, M -> R, 0 -> SL, 0 -> SR
1271
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1272
- return sourceData[sourceFrameIndex * sourceNum] * +(targetChannelIndex < 2);
1273
- };
1274
- }
1275
- else if (sourceNum === 1 && targetNum === 6) {
1276
- // Mono to 5.1: 0 -> L, 0 -> R, M -> C, 0 -> LFE, 0 -> SL, 0 -> SR
1277
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1278
- return sourceData[sourceFrameIndex * sourceNum] * +(targetChannelIndex === 2);
1279
- };
1280
- }
1281
- else if (sourceNum === 2 && targetNum === 1) {
1282
- // Stereo to Mono: 0.5 * (L + R)
1283
- this.channelMixer = (sourceData, sourceFrameIndex) => {
1284
- const baseIdx = sourceFrameIndex * sourceNum;
1285
- return 0.5 * (sourceData[baseIdx] + sourceData[baseIdx + 1]);
1286
- };
1287
- }
1288
- else if (sourceNum === 2 && targetNum === 4) {
1289
- // Stereo to Quad: L -> L, R -> R, 0 -> SL, 0 -> SR
1290
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1291
- return sourceData[sourceFrameIndex * sourceNum + targetChannelIndex] * +(targetChannelIndex < 2);
1292
- };
1293
- }
1294
- else if (sourceNum === 2 && targetNum === 6) {
1295
- // Stereo to 5.1: L -> L, R -> R, 0 -> C, 0 -> LFE, 0 -> SL, 0 -> SR
1296
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1297
- return sourceData[sourceFrameIndex * sourceNum + targetChannelIndex] * +(targetChannelIndex < 2);
1298
- };
1299
- }
1300
- else if (sourceNum === 4 && targetNum === 1) {
1301
- // Quad to Mono: 0.25 * (L + R + SL + SR)
1302
- this.channelMixer = (sourceData, sourceFrameIndex) => {
1303
- const baseIdx = sourceFrameIndex * sourceNum;
1304
- return 0.25 * (sourceData[baseIdx] + sourceData[baseIdx + 1]
1305
- + sourceData[baseIdx + 2] + sourceData[baseIdx + 3]);
1306
- };
1307
- }
1308
- else if (sourceNum === 4 && targetNum === 2) {
1309
- // Quad to Stereo: 0.5 * (L + SL), 0.5 * (R + SR)
1310
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1311
- const baseIdx = sourceFrameIndex * sourceNum;
1312
- return 0.5 * (sourceData[baseIdx + targetChannelIndex]
1313
- + sourceData[baseIdx + targetChannelIndex + 2]);
1314
- };
1315
- }
1316
- else if (sourceNum === 4 && targetNum === 6) {
1317
- // Quad to 5.1: L -> L, R -> R, 0 -> C, 0 -> LFE, SL -> SL, SR -> SR
1318
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1319
- const baseIdx = sourceFrameIndex * sourceNum;
1320
- // It's a bit harder to do this one branchlessly
1321
- if (targetChannelIndex < 2)
1322
- return sourceData[baseIdx + targetChannelIndex]; // L, R
1323
- if (targetChannelIndex === 2 || targetChannelIndex === 3)
1324
- return 0; // C, LFE
1325
- return sourceData[baseIdx + targetChannelIndex - 2]; // SL, SR
1326
- };
1327
- }
1328
- else if (sourceNum === 6 && targetNum === 1) {
1329
- // 5.1 to Mono: sqrt(1/2) * (L + R) + C + 0.5 * (SL + SR)
1330
- this.channelMixer = (sourceData, sourceFrameIndex) => {
1331
- const baseIdx = sourceFrameIndex * sourceNum;
1332
- return Math.SQRT1_2 * (sourceData[baseIdx] + sourceData[baseIdx + 1])
1333
- + sourceData[baseIdx + 2]
1334
- + 0.5 * (sourceData[baseIdx + 4] + sourceData[baseIdx + 5]);
1335
- };
1336
- }
1337
- else if (sourceNum === 6 && targetNum === 2) {
1338
- // 5.1 to Stereo: L + sqrt(1/2) * (C + SL), R + sqrt(1/2) * (C + SR)
1339
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1340
- const baseIdx = sourceFrameIndex * sourceNum;
1341
- return sourceData[baseIdx + targetChannelIndex]
1342
- + Math.SQRT1_2 * (sourceData[baseIdx + 2] + sourceData[baseIdx + targetChannelIndex + 4]);
1343
- };
1344
- }
1345
- else if (sourceNum === 6 && targetNum === 4) {
1346
- // 5.1 to Quad: L + sqrt(1/2) * C, R + sqrt(1/2) * C, SL, SR
1347
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1348
- const baseIdx = sourceFrameIndex * sourceNum;
1349
- // It's a bit harder to do this one branchlessly
1350
- if (targetChannelIndex < 2) {
1351
- return sourceData[baseIdx + targetChannelIndex] + Math.SQRT1_2 * sourceData[baseIdx + 2];
1352
- }
1353
- return sourceData[baseIdx + targetChannelIndex + 2]; // SL, SR
1354
- };
1355
- }
1356
- else {
1357
- // Discrete fallback: direct mapping with zero-fill or drop
1358
- this.channelMixer = (sourceData, sourceFrameIndex, targetChannelIndex) => {
1359
- return targetChannelIndex < sourceNum
1360
- ? sourceData[sourceFrameIndex * sourceNum + targetChannelIndex]
1361
- : 0;
1362
- };
1363
- }
1364
- }
1365
- ensureTempBufferSize(requiredSamples) {
1366
- let length = this.tempSourceBuffer.length;
1367
- while (length < requiredSamples) {
1368
- length *= 2;
1369
- }
1370
- if (length !== this.tempSourceBuffer.length) {
1371
- const newBuffer = new Float32Array(length);
1372
- newBuffer.set(this.tempSourceBuffer);
1373
- this.tempSourceBuffer = newBuffer;
1374
- }
1375
- }
1376
- async add(audioSample) {
1377
- if (this.sourceSampleRate === null) {
1378
- // This is the first sample, so let's init the missing data. Initting the sample rate from the decoded
1379
- // sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate
1380
- // they see fit.
1381
- this.sourceSampleRate = audioSample.sampleRate;
1382
- this.sourceNumberOfChannels = audioSample.numberOfChannels;
1383
- // Pre-allocate temporary buffer for source data
1384
- this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
1385
- this.doChannelMixerSetup();
1386
- }
1387
- const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
1388
- this.ensureTempBufferSize(requiredSamples);
1389
- // Copy the audio data to the temp buffer
1390
- const sourceDataSize = audioSample.allocationSize({ planeIndex: 0, format: 'f32' });
1391
- const sourceView = new Float32Array(this.tempSourceBuffer.buffer, 0, sourceDataSize / 4);
1392
- audioSample.copyTo(sourceView, { planeIndex: 0, format: 'f32' });
1393
- const inputStartTime = audioSample.timestamp - this.startTime;
1394
- const inputDuration = audioSample.numberOfFrames / this.sourceSampleRate;
1395
- const inputEndTime = Math.min(inputStartTime + inputDuration, this.endTime - this.startTime);
1396
- // Compute which output frames are affected by this sample
1397
- const outputStartFrame = Math.floor(inputStartTime * this.targetSampleRate);
1398
- const outputEndFrame = Math.ceil(inputEndTime * this.targetSampleRate);
1399
- for (let outputFrame = outputStartFrame; outputFrame < outputEndFrame; outputFrame++) {
1400
- if (outputFrame < this.bufferStartFrame) {
1401
- continue; // Skip writes to the past
1402
- }
1403
- while (outputFrame >= this.bufferStartFrame + this.bufferSizeInFrames) {
1404
- // The write is after the current buffer, so finalize it
1405
- await this.finalizeCurrentBuffer();
1406
- this.bufferStartFrame += this.bufferSizeInFrames;
1407
- }
1408
- const bufferFrameIndex = outputFrame - this.bufferStartFrame;
1409
- assert(bufferFrameIndex < this.bufferSizeInFrames);
1410
- const outputTime = outputFrame / this.targetSampleRate;
1411
- const inputTime = outputTime - inputStartTime;
1412
- const sourcePosition = inputTime * this.sourceSampleRate;
1413
- const sourceLowerFrame = Math.floor(sourcePosition);
1414
- const sourceUpperFrame = Math.ceil(sourcePosition);
1415
- const fraction = sourcePosition - sourceLowerFrame;
1416
- // Process each output channel
1417
- for (let targetChannel = 0; targetChannel < this.targetNumberOfChannels; targetChannel++) {
1418
- let lowerSample = 0;
1419
- let upperSample = 0;
1420
- if (sourceLowerFrame >= 0 && sourceLowerFrame < audioSample.numberOfFrames) {
1421
- lowerSample = this.channelMixer(sourceView, sourceLowerFrame, targetChannel);
1422
- }
1423
- if (sourceUpperFrame >= 0 && sourceUpperFrame < audioSample.numberOfFrames) {
1424
- upperSample = this.channelMixer(sourceView, sourceUpperFrame, targetChannel);
1425
- }
1426
- // For resampling, we do naive linear interpolation to find the in-between sample. This produces
1427
- // suboptimal results especially for downsampling (for which a low-pass filter would first need to be
1428
- // applied), but AudioContext doesn't do this either, so, whatever, for now.
1429
- const outputSample = lowerSample + fraction * (upperSample - lowerSample);
1430
- // Write to output buffer (interleaved)
1431
- const outputIndex = bufferFrameIndex * this.targetNumberOfChannels + targetChannel;
1432
- this.outputBuffer[outputIndex] += outputSample; // Add in case of overlapping samples
1433
- }
1434
- this.maxWrittenFrame = Math.max(this.maxWrittenFrame, bufferFrameIndex);
1435
- }
1436
- }
1437
- async finalizeCurrentBuffer() {
1438
- if (this.maxWrittenFrame < 0) {
1439
- return; // Nothing to finalize
1440
- }
1441
- const samplesWritten = (this.maxWrittenFrame + 1) * this.targetNumberOfChannels;
1442
- const outputData = new Float32Array(samplesWritten);
1443
- outputData.set(this.outputBuffer.subarray(0, samplesWritten));
1444
- const timestampSeconds = this.bufferStartFrame / this.targetSampleRate;
1445
- const audioSample = new AudioSample({
1446
- format: 'f32',
1447
- sampleRate: this.targetSampleRate,
1448
- numberOfChannels: this.targetNumberOfChannels,
1449
- timestamp: timestampSeconds,
1450
- data: outputData,
1451
- });
1452
- await this.onSample(audioSample);
1453
- this.outputBuffer.fill(0);
1454
- this.maxWrittenFrame = -1;
1455
- }
1456
- finalize() {
1457
- return this.finalizeCurrentBuffer();
1458
- }
1459
- }