mediabunny 1.0.2 → 1.0.4

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 (188) hide show
  1. package/README.md +4 -4
  2. package/dist/mediabunny.d.ts +4 -2
  3. package/dist/modules/codec-data.d.ts +95 -0
  4. package/dist/modules/codec-data.d.ts.map +1 -0
  5. package/dist/modules/codec-data.js +1103 -0
  6. package/dist/modules/codec.d.ts +250 -0
  7. package/dist/modules/codec.d.ts.map +1 -0
  8. package/dist/modules/codec.js +1100 -0
  9. package/dist/modules/conversion.d.ts +158 -0
  10. package/dist/modules/conversion.d.ts.map +1 -0
  11. package/dist/modules/conversion.js +866 -0
  12. package/dist/modules/custom-coder.d.ts +119 -0
  13. package/dist/modules/custom-coder.d.ts.map +1 -0
  14. package/dist/modules/custom-coder.js +91 -0
  15. package/dist/modules/demuxer.d.ts +17 -0
  16. package/dist/modules/demuxer.d.ts.map +1 -0
  17. package/dist/modules/demuxer.js +12 -0
  18. package/dist/modules/index.d.ts +25 -0
  19. package/dist/modules/index.d.ts.map +1 -0
  20. package/dist/modules/index.js +24 -0
  21. package/dist/modules/input-format.d.ts +121 -0
  22. package/dist/modules/input-format.d.ts.map +1 -0
  23. package/dist/modules/input-format.js +333 -0
  24. package/dist/modules/input-track.d.ts +138 -0
  25. package/dist/modules/input-track.d.ts.map +1 -0
  26. package/dist/modules/input-track.js +230 -0
  27. package/dist/modules/input.d.ts +55 -0
  28. package/dist/modules/input.d.ts.map +1 -0
  29. package/dist/modules/input.js +104 -0
  30. package/dist/modules/isobmff/isobmff-boxes.d.ts +202 -0
  31. package/dist/modules/isobmff/isobmff-boxes.d.ts.map +1 -0
  32. package/dist/modules/isobmff/isobmff-boxes.js +1111 -0
  33. package/dist/modules/isobmff/isobmff-demuxer.d.ts +163 -0
  34. package/dist/modules/isobmff/isobmff-demuxer.d.ts.map +1 -0
  35. package/dist/modules/isobmff/isobmff-demuxer.js +2080 -0
  36. package/dist/modules/isobmff/isobmff-misc.d.ts +14 -0
  37. package/dist/modules/isobmff/isobmff-misc.d.ts.map +1 -0
  38. package/dist/modules/isobmff/isobmff-misc.js +20 -0
  39. package/dist/modules/isobmff/isobmff-muxer.d.ts +142 -0
  40. package/dist/modules/isobmff/isobmff-muxer.d.ts.map +1 -0
  41. package/dist/modules/isobmff/isobmff-muxer.js +858 -0
  42. package/dist/modules/isobmff/isobmff-reader.d.ts +36 -0
  43. package/dist/modules/isobmff/isobmff-reader.d.ts.map +1 -0
  44. package/dist/modules/isobmff/isobmff-reader.js +105 -0
  45. package/dist/modules/matroska/ebml.d.ts +156 -0
  46. package/dist/modules/matroska/ebml.d.ts.map +1 -0
  47. package/dist/modules/matroska/ebml.js +529 -0
  48. package/dist/modules/matroska/matroska-demuxer.d.ts +119 -0
  49. package/dist/modules/matroska/matroska-demuxer.d.ts.map +1 -0
  50. package/dist/modules/matroska/matroska-demuxer.js +1330 -0
  51. package/dist/modules/matroska/matroska-misc.d.ts +14 -0
  52. package/dist/modules/matroska/matroska-misc.d.ts.map +1 -0
  53. package/dist/modules/matroska/matroska-misc.js +20 -0
  54. package/dist/modules/matroska/matroska-muxer.d.ts +71 -0
  55. package/dist/modules/matroska/matroska-muxer.d.ts.map +1 -0
  56. package/dist/modules/matroska/matroska-muxer.js +729 -0
  57. package/dist/modules/media-sink.d.ts +272 -0
  58. package/dist/modules/media-sink.d.ts.map +1 -0
  59. package/dist/modules/media-sink.js +1264 -0
  60. package/dist/modules/media-source.d.ts +230 -0
  61. package/dist/modules/media-source.d.ts.map +1 -0
  62. package/dist/modules/media-source.js +1095 -0
  63. package/dist/modules/misc.d.ts +127 -0
  64. package/dist/modules/misc.d.ts.map +1 -0
  65. package/dist/modules/misc.js +462 -0
  66. package/dist/modules/mp3/mp3-demuxer.d.ts +32 -0
  67. package/dist/modules/mp3/mp3-demuxer.d.ts.map +1 -0
  68. package/dist/modules/mp3/mp3-demuxer.js +160 -0
  69. package/dist/modules/mp3/mp3-misc.d.ts +37 -0
  70. package/dist/modules/mp3/mp3-misc.d.ts.map +1 -0
  71. package/dist/modules/mp3/mp3-misc.js +122 -0
  72. package/dist/modules/mp3/mp3-muxer.d.ts +27 -0
  73. package/dist/modules/mp3/mp3-muxer.d.ts.map +1 -0
  74. package/dist/modules/mp3/mp3-muxer.js +112 -0
  75. package/dist/modules/mp3/mp3-reader.d.ts +25 -0
  76. package/dist/modules/mp3/mp3-reader.d.ts.map +1 -0
  77. package/dist/modules/mp3/mp3-reader.js +73 -0
  78. package/dist/modules/mp3/mp3-writer.d.ts +30 -0
  79. package/dist/modules/mp3/mp3-writer.d.ts.map +1 -0
  80. package/dist/modules/mp3/mp3-writer.js +72 -0
  81. package/dist/modules/muxer.d.ts +32 -0
  82. package/dist/modules/muxer.d.ts.map +1 -0
  83. package/dist/modules/muxer.js +50 -0
  84. package/dist/modules/ogg/ogg-demuxer.d.ts +53 -0
  85. package/dist/modules/ogg/ogg-demuxer.d.ts.map +1 -0
  86. package/dist/modules/ogg/ogg-demuxer.js +701 -0
  87. package/dist/modules/ogg/ogg-misc.d.ts +27 -0
  88. package/dist/modules/ogg/ogg-misc.d.ts.map +1 -0
  89. package/dist/modules/ogg/ogg-misc.js +78 -0
  90. package/dist/modules/ogg/ogg-muxer.d.ts +58 -0
  91. package/dist/modules/ogg/ogg-muxer.d.ts.map +1 -0
  92. package/dist/modules/ogg/ogg-muxer.js +345 -0
  93. package/dist/modules/ogg/ogg-reader.d.ts +37 -0
  94. package/dist/modules/ogg/ogg-reader.d.ts.map +1 -0
  95. package/dist/modules/ogg/ogg-reader.js +104 -0
  96. package/dist/modules/output-format.d.ts +285 -0
  97. package/dist/modules/output-format.d.ts.map +1 -0
  98. package/dist/modules/output-format.js +385 -0
  99. package/dist/modules/output.d.ts +134 -0
  100. package/dist/modules/output.d.ts.map +1 -0
  101. package/dist/modules/output.js +269 -0
  102. package/dist/modules/packet.d.ts +86 -0
  103. package/dist/modules/packet.d.ts.map +1 -0
  104. package/dist/modules/packet.js +133 -0
  105. package/dist/modules/pcm.d.ts +12 -0
  106. package/dist/modules/pcm.d.ts.map +1 -0
  107. package/dist/modules/pcm.js +85 -0
  108. package/dist/modules/reader.d.ts +40 -0
  109. package/dist/modules/reader.d.ts.map +1 -0
  110. package/dist/modules/reader.js +153 -0
  111. package/dist/modules/sample.d.ts +186 -0
  112. package/dist/modules/sample.d.ts.map +1 -0
  113. package/dist/modules/sample.js +798 -0
  114. package/dist/modules/source.d.ts +76 -0
  115. package/dist/modules/source.d.ts.map +1 -0
  116. package/dist/modules/source.js +183 -0
  117. package/dist/modules/subtitles.d.ts +37 -0
  118. package/dist/modules/subtitles.d.ts.map +1 -0
  119. package/dist/modules/subtitles.js +89 -0
  120. package/dist/modules/target.d.ts +58 -0
  121. package/dist/modules/target.d.ts.map +1 -0
  122. package/dist/modules/target.js +63 -0
  123. package/dist/modules/wave/riff-reader.d.ts +19 -0
  124. package/dist/modules/wave/riff-reader.d.ts.map +1 -0
  125. package/dist/modules/wave/riff-reader.js +38 -0
  126. package/dist/modules/wave/riff-writer.d.ts +18 -0
  127. package/dist/modules/wave/riff-writer.d.ts.map +1 -0
  128. package/dist/modules/wave/riff-writer.js +25 -0
  129. package/dist/modules/wave/wave-demuxer.d.ts +42 -0
  130. package/dist/modules/wave/wave-demuxer.d.ts.map +1 -0
  131. package/dist/modules/wave/wave-demuxer.js +239 -0
  132. package/dist/modules/wave/wave-muxer.d.ts +27 -0
  133. package/dist/modules/wave/wave-muxer.d.ts.map +1 -0
  134. package/dist/modules/wave/wave-muxer.js +108 -0
  135. package/dist/modules/writer.d.ts +85 -0
  136. package/dist/modules/writer.d.ts.map +1 -0
  137. package/dist/modules/writer.js +347 -0
  138. package/package.json +14 -10
  139. package/src/codec-data.ts +1394 -0
  140. package/src/codec.ts +1392 -0
  141. package/src/conversion.ts +1199 -0
  142. package/src/custom-coder.ts +162 -0
  143. package/src/demuxer.ts +22 -0
  144. package/src/index.ts +148 -0
  145. package/src/input-format.ts +386 -0
  146. package/src/input-track.ts +341 -0
  147. package/src/input.ts +138 -0
  148. package/src/isobmff/isobmff-boxes.ts +1365 -0
  149. package/src/isobmff/isobmff-demuxer.ts +2646 -0
  150. package/src/isobmff/isobmff-misc.ts +29 -0
  151. package/src/isobmff/isobmff-muxer.ts +1210 -0
  152. package/src/isobmff/isobmff-reader.ts +137 -0
  153. package/src/matroska/ebml.ts +595 -0
  154. package/src/matroska/matroska-demuxer.ts +1677 -0
  155. package/src/matroska/matroska-misc.ts +29 -0
  156. package/src/matroska/matroska-muxer.ts +999 -0
  157. package/src/media-sink.ts +1577 -0
  158. package/src/media-source.ts +1406 -0
  159. package/src/misc.ts +584 -0
  160. package/src/mp3/mp3-demuxer.ts +228 -0
  161. package/src/mp3/mp3-misc.ts +154 -0
  162. package/src/mp3/mp3-muxer.ts +153 -0
  163. package/src/mp3/mp3-reader.ts +93 -0
  164. package/src/mp3/mp3-writer.ts +112 -0
  165. package/src/muxer.ts +88 -0
  166. package/src/ogg/ogg-demuxer.ts +985 -0
  167. package/src/ogg/ogg-misc.ts +116 -0
  168. package/src/ogg/ogg-muxer.ts +480 -0
  169. package/src/ogg/ogg-reader.ts +143 -0
  170. package/src/output-format.ts +689 -0
  171. package/src/output.ts +427 -0
  172. package/src/packet.ts +180 -0
  173. package/src/pcm.ts +112 -0
  174. package/src/reader.ts +205 -0
  175. package/src/sample.ts +1029 -0
  176. package/src/source.ts +279 -0
  177. package/src/subtitles.ts +131 -0
  178. package/src/target.ts +106 -0
  179. package/src/tsconfig.json +13 -0
  180. package/src/wave/riff-reader.ts +48 -0
  181. package/src/wave/riff-writer.ts +30 -0
  182. package/src/wave/wave-demuxer.ts +317 -0
  183. package/src/wave/wave-muxer.ts +145 -0
  184. package/src/writer.ts +461 -0
  185. /package/dist/{mediabunny.js → bundles/mediabunny.cjs} +0 -0
  186. /package/dist/{mediabunny.min.js → bundles/mediabunny.min.cjs} +0 -0
  187. /package/dist/{mediabunny.min.mjs → bundles/mediabunny.min.mjs} +0 -0
  188. /package/dist/{mediabunny.mjs → bundles/mediabunny.mjs} +0 -0
@@ -0,0 +1,228 @@
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
+
9
+ import { AudioCodec } from '../codec';
10
+ import { Demuxer } from '../demuxer';
11
+ import { Input } from '../input';
12
+ import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
13
+ import { PacketRetrievalOptions } from '../media-sink';
14
+ import { assert, binarySearchExact, binarySearchLessOrEqual, last, UNDETERMINED_LANGUAGE } from '../misc';
15
+ import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
16
+ import { FrameHeader, getXingOffset, INFO, XING } from './mp3-misc';
17
+ import { Mp3Reader } from './mp3-reader';
18
+
19
+ type Sample = {
20
+ timestamp: number;
21
+ duration: number;
22
+ dataStart: number;
23
+ dataSize: number;
24
+ };
25
+
26
+ export class Mp3Demuxer extends Demuxer {
27
+ reader: Mp3Reader;
28
+
29
+ metadataPromise: Promise<void> | null = null;
30
+ firstFrameHeader: FrameHeader | null = null;
31
+ allSamples: Sample[] = [];
32
+
33
+ tracks: InputAudioTrack[] = [];
34
+
35
+ constructor(input: Input) {
36
+ super(input);
37
+
38
+ this.reader = new Mp3Reader(input._mainReader);
39
+ }
40
+
41
+ async readMetadata() {
42
+ return this.metadataPromise ??= (async () => {
43
+ const fileSize = await this.input.source.getSize();
44
+ this.reader.fileSize = fileSize;
45
+
46
+ // Just load the entire file. Primitive, but the only way to actually ensure 100% correct timestamps.
47
+ // Random access in MP3 can be flaky and unreliable.
48
+ await this.reader.reader.loadRange(0, fileSize);
49
+
50
+ const id3Tag = this.reader.readId3();
51
+ if (id3Tag) {
52
+ this.reader.pos += id3Tag.size;
53
+ }
54
+
55
+ let nextTimestampInSamples = 0;
56
+
57
+ // Let's read all samples
58
+ while (true) {
59
+ const header = this.reader.readNextFrameHeader();
60
+ if (!header) {
61
+ break;
62
+ }
63
+
64
+ const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
65
+ this.reader.pos = header.startPos + xingOffset;
66
+ const word = this.reader.readU32();
67
+ const isXing = word === XING || word === INFO;
68
+
69
+ this.reader.pos = header.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short
70
+
71
+ if (isXing) {
72
+ // There's no actual audio data in this frame, so let's skip it
73
+ continue;
74
+ }
75
+
76
+ if (!this.firstFrameHeader) {
77
+ this.firstFrameHeader = header;
78
+ }
79
+
80
+ const sampleDuration = header.audioSamplesInFrame / header.sampleRate;
81
+ const sample: Sample = {
82
+ timestamp: nextTimestampInSamples / header.sampleRate,
83
+ duration: sampleDuration,
84
+ dataStart: header.startPos,
85
+ dataSize: header.totalSize,
86
+ };
87
+
88
+ this.allSamples.push(sample);
89
+ nextTimestampInSamples += header.audioSamplesInFrame;
90
+ }
91
+
92
+ if (!this.firstFrameHeader) {
93
+ throw new Error('No MP3 frames found.');
94
+ }
95
+
96
+ this.tracks = [new InputAudioTrack(new Mp3AudioTrackBacking(this))];
97
+ })();
98
+ }
99
+
100
+ async getMimeType() {
101
+ return 'audio/mpeg';
102
+ }
103
+
104
+ async getTracks() {
105
+ await this.readMetadata();
106
+ return this.tracks;
107
+ }
108
+
109
+ async computeDuration() {
110
+ await this.readMetadata();
111
+
112
+ const lastSample = last(this.allSamples);
113
+ assert(lastSample);
114
+
115
+ return lastSample.timestamp + lastSample.duration;
116
+ }
117
+ }
118
+
119
+ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
120
+ constructor(public demuxer: Mp3Demuxer) {}
121
+
122
+ getId() {
123
+ return 1;
124
+ }
125
+
126
+ async getFirstTimestamp() {
127
+ return 0;
128
+ }
129
+
130
+ getTimeResolution() {
131
+ assert(this.demuxer.firstFrameHeader);
132
+ return this.demuxer.firstFrameHeader.sampleRate / this.demuxer.firstFrameHeader.audioSamplesInFrame;
133
+ }
134
+
135
+ computeDuration() {
136
+ return this.demuxer.computeDuration();
137
+ }
138
+
139
+ getLanguageCode() {
140
+ return UNDETERMINED_LANGUAGE;
141
+ }
142
+
143
+ getCodec(): AudioCodec {
144
+ return 'mp3';
145
+ }
146
+
147
+ getNumberOfChannels() {
148
+ assert(this.demuxer.firstFrameHeader);
149
+ return this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2;
150
+ }
151
+
152
+ getSampleRate() {
153
+ assert(this.demuxer.firstFrameHeader);
154
+ return this.demuxer.firstFrameHeader.sampleRate;
155
+ }
156
+
157
+ async getDecoderConfig(): Promise<AudioDecoderConfig> {
158
+ assert(this.demuxer.firstFrameHeader);
159
+
160
+ return {
161
+ codec: 'mp3',
162
+ numberOfChannels: this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2,
163
+ sampleRate: this.demuxer.firstFrameHeader.sampleRate,
164
+ };
165
+ }
166
+
167
+ getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) {
168
+ if (sampleIndex === -1) {
169
+ return null;
170
+ }
171
+
172
+ const rawSample = this.demuxer.allSamples[sampleIndex];
173
+ if (!rawSample) {
174
+ return null;
175
+ }
176
+
177
+ let data: Uint8Array;
178
+ if (options.metadataOnly) {
179
+ data = PLACEHOLDER_DATA;
180
+ } else {
181
+ this.demuxer.reader.pos = rawSample.dataStart;
182
+ data = this.demuxer.reader.readBytes(rawSample.dataSize);
183
+ }
184
+
185
+ return new EncodedPacket(
186
+ data,
187
+ 'key',
188
+ rawSample.timestamp,
189
+ rawSample.duration,
190
+ sampleIndex,
191
+ rawSample.dataSize,
192
+ );
193
+ }
194
+
195
+ async getFirstPacket(options: PacketRetrievalOptions) {
196
+ return this.getPacketAtIndex(0, options);
197
+ }
198
+
199
+ async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
200
+ const sampleIndex = binarySearchExact(
201
+ this.demuxer.allSamples,
202
+ packet.timestamp,
203
+ x => x.timestamp,
204
+ );
205
+ if (sampleIndex === -1) {
206
+ throw new Error('Packet was not created from this track.');
207
+ }
208
+
209
+ return this.getPacketAtIndex(sampleIndex + 1, options);
210
+ }
211
+
212
+ async getPacket(timestamp: number, options: PacketRetrievalOptions) {
213
+ const index = binarySearchLessOrEqual(
214
+ this.demuxer.allSamples,
215
+ timestamp,
216
+ x => x.timestamp,
217
+ );
218
+ return this.getPacketAtIndex(index, options);
219
+ }
220
+
221
+ getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
222
+ return this.getPacket(timestamp, options);
223
+ }
224
+
225
+ getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
226
+ return this.getNextPacket(packet, options);
227
+ }
228
+ }
@@ -0,0 +1,154 @@
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
+
9
+ export const FRAME_HEADER_SIZE = 4;
10
+
11
+ // These are in kbps:
12
+ export const MPEG_V1_BITRATES: Record<number, number[]> = {
13
+ // Layer 3
14
+ 1: [-1, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1],
15
+ // Layer 2
16
+ 2: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1],
17
+ // Layer 1
18
+ 3: [-1, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1],
19
+ };
20
+ export const MPEG_V2_BITRATES: Record<number, number[]> = {
21
+ // Layer 3
22
+ 1: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1],
23
+ // Layer 2
24
+ 2: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
25
+ // Layer 1
26
+ 3: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
27
+ };
28
+ export const SAMPLING_RATES: Record<number, number[]> = {
29
+ // MPEG Version 2.5
30
+ 0: [11025, 12000, 8000, -1],
31
+ // MPEG Version 2 (ISO/IEC 13818-3)
32
+ 2: [22050, 24000, 16000, -1],
33
+ // MPEG Version 1 (ISO/IEC 11172-3)
34
+ 3: [44100, 48000, 32000, -1],
35
+ };
36
+
37
+ /** 'Xing' */
38
+ export const XING = 0x58696e67;
39
+ /** 'Info' */
40
+ export const INFO = 0x496e666f;
41
+
42
+ export type FrameHeader = {
43
+ startPos: number;
44
+ totalSize: number;
45
+ mpegVersionId: number;
46
+ layer: number;
47
+ bitrate: number;
48
+ frequencyIndex: number;
49
+ sampleRate: number;
50
+ channel: number;
51
+ modeExtension: number;
52
+ copyright: number;
53
+ original: number;
54
+ emphasis: number;
55
+ audioSamplesInFrame: number;
56
+ };
57
+
58
+ export const computeMp3FrameSize = (layer: number, bitrate: number, sampleRate: number, padding: number) => {
59
+ if (layer === 3) {
60
+ // Layer 1
61
+ return Math.floor((12 * bitrate / sampleRate + padding) * 4);
62
+ } else {
63
+ return Math.floor((144 * bitrate / sampleRate) + padding);
64
+ }
65
+ };
66
+
67
+ export const getXingOffset = (mpegVersionId: number, channel: number) => {
68
+ return mpegVersionId === 3
69
+ ? (channel === 3 ? 21 : 36)
70
+ : (channel === 3 ? 13 : 21);
71
+ };
72
+
73
+ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: number | null }): FrameHeader | null => {
74
+ const startPos = reader.pos;
75
+
76
+ const firstByte = word >>> 24;
77
+ const secondByte = (word >>> 16) & 0xff;
78
+ const thirdByte = (word >>> 8) & 0xff;
79
+ const fourthByte = word & 0xff;
80
+
81
+ if (firstByte !== 0xff && secondByte !== 0xff && thirdByte !== 0xff && fourthByte !== 0xff) {
82
+ reader.pos += 4;
83
+ return null;
84
+ }
85
+
86
+ reader.pos += 1;
87
+
88
+ if ((secondByte & 0xe0) !== 0xe0) {
89
+ return null;
90
+ }
91
+
92
+ const mpegVersionId = (secondByte >> 3) & 0x3;
93
+ const layer = (secondByte >> 1) & 0x3;
94
+
95
+ const bitrateIndex = (thirdByte >> 4) & 0xf;
96
+ const frequencyIndex = (thirdByte >> 2) & 0x3;
97
+ const padding = (thirdByte >> 1) & 0x1;
98
+
99
+ const channel = (fourthByte >> 6) & 0x3;
100
+ const modeExtension = (fourthByte >> 4) & 0x3;
101
+ const copyright = (fourthByte >> 3) & 0x1;
102
+ const original = (fourthByte >> 2) & 0x1;
103
+ const emphasis = fourthByte & 0x3;
104
+
105
+ const kilobitRate = mpegVersionId === 3
106
+ ? MPEG_V1_BITRATES[layer]?.[bitrateIndex]
107
+ : MPEG_V2_BITRATES[layer]?.[bitrateIndex];
108
+ if (!kilobitRate || kilobitRate === -1) {
109
+ return null;
110
+ }
111
+
112
+ const bitrate = kilobitRate * 1000;
113
+
114
+ const sampleRate = SAMPLING_RATES[mpegVersionId]?.[frequencyIndex];
115
+ if (!sampleRate || sampleRate === -1) {
116
+ return null;
117
+ }
118
+
119
+ const frameLength = computeMp3FrameSize(layer, bitrate, sampleRate, padding);
120
+
121
+ if (reader.fileSize !== null && reader.fileSize - startPos < frameLength) {
122
+ // The frame doesn't fit into the rest of the file
123
+ return null;
124
+ }
125
+
126
+ let audioSamplesInFrame: number;
127
+ if (mpegVersionId === 3) {
128
+ audioSamplesInFrame = layer === 3 ? 384 : 1152;
129
+ } else {
130
+ if (layer === 3) {
131
+ audioSamplesInFrame = 384;
132
+ } else if (layer === 2) {
133
+ audioSamplesInFrame = 1152;
134
+ } else {
135
+ audioSamplesInFrame = 576;
136
+ }
137
+ }
138
+
139
+ return {
140
+ startPos: startPos,
141
+ totalSize: frameLength,
142
+ mpegVersionId,
143
+ layer,
144
+ bitrate,
145
+ frequencyIndex,
146
+ sampleRate,
147
+ channel,
148
+ modeExtension,
149
+ copyright,
150
+ original,
151
+ emphasis,
152
+ audioSamplesInFrame,
153
+ };
154
+ };
@@ -0,0 +1,153 @@
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
+
9
+ import { assert, toDataView } from '../misc';
10
+ import { Muxer } from '../muxer';
11
+ import { Output, OutputAudioTrack } from '../output';
12
+ import { Mp3OutputFormat } from '../output-format';
13
+ import { EncodedPacket } from '../packet';
14
+ import { Writer } from '../writer';
15
+ import { getXingOffset, INFO, readFrameHeader, XING } from './mp3-misc';
16
+ import { Mp3Writer, XingFrameData } from './mp3-writer';
17
+
18
+ export class Mp3Muxer extends Muxer {
19
+ private format: Mp3OutputFormat;
20
+ private writer: Writer;
21
+ private mp3Writer: Mp3Writer;
22
+ private xingFrameData: XingFrameData | null = null;
23
+ private frameCount = 0;
24
+ private framePositions: number[] = [];
25
+
26
+ constructor(output: Output, format: Mp3OutputFormat) {
27
+ super(output);
28
+
29
+ this.format = format;
30
+ this.writer = output._writer;
31
+ this.mp3Writer = new Mp3Writer(output._writer);
32
+ }
33
+
34
+ async start() {
35
+ // Nothing needed here
36
+ }
37
+
38
+ async getMimeType() {
39
+ return 'audio/mpeg';
40
+ }
41
+
42
+ async addEncodedVideoPacket() {
43
+ throw new Error('MP3 does not support video.');
44
+ }
45
+
46
+ async addEncodedAudioPacket(
47
+ track: OutputAudioTrack,
48
+ packet: EncodedPacket,
49
+ ) {
50
+ const release = await this.mutex.acquire();
51
+
52
+ try {
53
+ if (!this.xingFrameData) {
54
+ const view = toDataView(packet.data);
55
+ if (view.byteLength < 4) {
56
+ throw new Error('Invalid MP3 header in sample.');
57
+ }
58
+
59
+ const word = view.getUint32(0, false);
60
+ const header = readFrameHeader(word, { pos: 0, fileSize: null });
61
+ if (!header) {
62
+ throw new Error('Invalid MP3 header in sample.');
63
+ }
64
+
65
+ const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
66
+ if (view.byteLength >= xingOffset + 4) {
67
+ const word = view.getUint32(xingOffset, false);
68
+ const isXing = word === XING || word === INFO;
69
+
70
+ if (isXing) {
71
+ // This is not a data frame, so let's completely ignore this sample
72
+ return;
73
+ }
74
+ }
75
+
76
+ this.xingFrameData = {
77
+ mpegVersionId: header.mpegVersionId,
78
+ layer: header.layer,
79
+ frequencyIndex: header.frequencyIndex,
80
+ channel: header.channel,
81
+ modeExtension: header.modeExtension,
82
+ copyright: header.copyright,
83
+ original: header.original,
84
+ emphasis: header.emphasis,
85
+
86
+ frameCount: null,
87
+ fileSize: null,
88
+ toc: null,
89
+ };
90
+
91
+ // Write a Xing frame because this muxer doesn't make any bitrate constraints, meaning we don't know if
92
+ // this will be a constant or variable bitrate file. Therefore, always write the Xing frame.
93
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
94
+
95
+ this.frameCount++;
96
+ }
97
+
98
+ this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
99
+
100
+ this.framePositions.push(this.writer.getPos());
101
+ this.writer.write(packet.data);
102
+ this.frameCount++;
103
+
104
+ await this.writer.flush();
105
+ } finally {
106
+ release();
107
+ }
108
+ }
109
+
110
+ async addSubtitleCue() {
111
+ throw new Error('MP3 does not support subtitles.');
112
+ }
113
+
114
+ async finalize() {
115
+ if (!this.xingFrameData) {
116
+ return;
117
+ }
118
+
119
+ const release = await this.mutex.acquire();
120
+
121
+ const endPos = this.writer.getPos();
122
+
123
+ this.writer.seek(0);
124
+
125
+ const toc = new Uint8Array(100);
126
+ for (let i = 0; i < 100; i++) {
127
+ const index = Math.floor(this.framePositions.length * (i / 100));
128
+ assert(index !== -1 && index < this.framePositions.length);
129
+
130
+ const byteOffset = this.framePositions[index]!;
131
+ toc[i] = 256 * (byteOffset / endPos);
132
+ }
133
+
134
+ this.xingFrameData.frameCount = this.frameCount;
135
+ this.xingFrameData.fileSize = endPos;
136
+ this.xingFrameData.toc = toc;
137
+
138
+ if (this.format._options.onXingFrame) {
139
+ this.writer.startTrackingWrites();
140
+ }
141
+
142
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
143
+
144
+ if (this.format._options.onXingFrame) {
145
+ const { data, start } = this.writer.stopTrackingWrites();
146
+ this.format._options.onXingFrame(data, start);
147
+ }
148
+
149
+ this.writer.seek(endPos);
150
+
151
+ release();
152
+ }
153
+ }
@@ -0,0 +1,93 @@
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
+
9
+ import { assert } from '../misc';
10
+ import { Reader } from '../reader';
11
+ import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from './mp3-misc';
12
+
13
+ export class Mp3Reader {
14
+ pos = 0;
15
+ fileSize: number | null = null;
16
+
17
+ constructor(public reader: Reader) {}
18
+
19
+ readBytes(length: number) {
20
+ const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
21
+ this.pos += length;
22
+
23
+ return new Uint8Array(view.buffer, offset, length);
24
+ }
25
+
26
+ readU16() {
27
+ const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2);
28
+ this.pos += 2;
29
+
30
+ return view.getUint16(offset, false);
31
+ }
32
+
33
+ readU32() {
34
+ const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4);
35
+ this.pos += 4;
36
+
37
+ return view.getUint32(offset, false);
38
+ }
39
+
40
+ readAscii(length: number) {
41
+ const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
42
+ this.pos += length;
43
+
44
+ let str = '';
45
+ for (let i = 0; i < length; i++) {
46
+ str += String.fromCharCode(view.getUint8(offset + i));
47
+ }
48
+ return str;
49
+ }
50
+
51
+ readId3() {
52
+ const tag = this.readAscii(3);
53
+ if (tag !== 'ID3') {
54
+ this.pos -= 3;
55
+ return null;
56
+ }
57
+
58
+ this.pos += 3;
59
+
60
+ const size = decodeSynchsafe(this.readU32());
61
+ return { size };
62
+ }
63
+
64
+ readNextFrameHeader(until?: number): FrameHeader | null {
65
+ assert(this.fileSize);
66
+ until ??= this.fileSize;
67
+
68
+ while (this.pos < until - FRAME_HEADER_SIZE) {
69
+ const word = this.readU32();
70
+ this.pos -= 4;
71
+
72
+ const header = readFrameHeader(word, this);
73
+ if (header) {
74
+ return header;
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }
80
+ }
81
+
82
+ export const decodeSynchsafe = (synchsafed: number) => {
83
+ let mask = 0x7f000000;
84
+ let unsynchsafed = 0;
85
+
86
+ while (mask !== 0) {
87
+ unsynchsafed >>= 1;
88
+ unsynchsafed |= synchsafed & mask;
89
+ mask >>= 8;
90
+ }
91
+
92
+ return unsynchsafed;
93
+ };