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,1365 @@
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 {
10
+ toUint8Array,
11
+ assert,
12
+ isU32,
13
+ last,
14
+ TransformationMatrix,
15
+ textEncoder,
16
+ COLOR_PRIMARIES_MAP,
17
+ TRANSFER_CHARACTERISTICS_MAP,
18
+ MATRIX_COEFFICIENTS_MAP,
19
+ colorSpaceIsComplete,
20
+ UNDETERMINED_LANGUAGE,
21
+ } from '../misc';
22
+ import {
23
+ AudioCodec,
24
+ generateAv1CodecConfigurationFromCodecString,
25
+ parsePcmCodec,
26
+ PCM_AUDIO_CODECS,
27
+ PcmAudioCodec,
28
+ SubtitleCodec,
29
+ VideoCodec,
30
+ } from '../codec';
31
+ import { formatSubtitleTimestamp } from '../subtitles';
32
+ import { Writer } from '../writer';
33
+ import {
34
+ GLOBAL_TIMESCALE,
35
+ intoTimescale,
36
+ IsobmffAudioTrackData,
37
+ IsobmffSubtitleTrackData,
38
+ IsobmffTrackData,
39
+ IsobmffVideoTrackData,
40
+ Sample,
41
+ } from './isobmff-muxer';
42
+ import { parseOpusIdentificationHeader } from '../codec-data';
43
+
44
+ export class IsobmffBoxWriter {
45
+ private helper = new Uint8Array(8);
46
+ private helperView = new DataView(this.helper.buffer);
47
+
48
+ /**
49
+ * Stores the position from the start of the file to where boxes elements have been written. This is used to
50
+ * rewrite/edit elements that were already added before, and to measure sizes of things.
51
+ */
52
+ offsets = new WeakMap<Box, number>();
53
+
54
+ constructor(private writer: Writer) {}
55
+
56
+ writeU32(value: number) {
57
+ this.helperView.setUint32(0, value, false);
58
+ this.writer.write(this.helper.subarray(0, 4));
59
+ }
60
+
61
+ writeU64(value: number) {
62
+ this.helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
63
+ this.helperView.setUint32(4, value, false);
64
+ this.writer.write(this.helper.subarray(0, 8));
65
+ }
66
+
67
+ writeAscii(text: string) {
68
+ for (let i = 0; i < text.length; i++) {
69
+ this.helperView.setUint8(i % 8, text.charCodeAt(i));
70
+ if (i % 8 === 7) this.writer.write(this.helper);
71
+ }
72
+
73
+ if (text.length % 8 !== 0) {
74
+ this.writer.write(this.helper.subarray(0, text.length % 8));
75
+ }
76
+ }
77
+
78
+ writeBox(box: Box) {
79
+ this.offsets.set(box, this.writer.getPos());
80
+
81
+ if (box.contents && !box.children) {
82
+ this.writeBoxHeader(box, box.size ?? box.contents.byteLength + 8);
83
+ this.writer.write(box.contents);
84
+ } else {
85
+ const startPos = this.writer.getPos();
86
+ this.writeBoxHeader(box, 0);
87
+
88
+ if (box.contents) this.writer.write(box.contents);
89
+ if (box.children) for (const child of box.children) if (child) this.writeBox(child);
90
+
91
+ const endPos = this.writer.getPos();
92
+ const size = box.size ?? endPos - startPos;
93
+ this.writer.seek(startPos);
94
+ this.writeBoxHeader(box, size);
95
+ this.writer.seek(endPos);
96
+ }
97
+ }
98
+
99
+ writeBoxHeader(box: Box, size: number) {
100
+ this.writeU32(box.largeSize ? 1 : size);
101
+ this.writeAscii(box.type);
102
+ if (box.largeSize) this.writeU64(size);
103
+ }
104
+
105
+ measureBoxHeader(box: Box) {
106
+ return 8 + (box.largeSize ? 8 : 0);
107
+ }
108
+
109
+ patchBox(box: Box) {
110
+ const boxOffset = this.offsets.get(box);
111
+ assert(boxOffset !== undefined);
112
+
113
+ const endPos = this.writer.getPos();
114
+ this.writer.seek(boxOffset);
115
+ this.writeBox(box);
116
+ this.writer.seek(endPos);
117
+ }
118
+
119
+ measureBox(box: Box) {
120
+ if (box.contents && !box.children) {
121
+ const headerSize = this.measureBoxHeader(box);
122
+ return headerSize + box.contents.byteLength;
123
+ } else {
124
+ let result = this.measureBoxHeader(box);
125
+ if (box.contents) result += box.contents.byteLength;
126
+ if (box.children) for (const child of box.children) if (child) result += this.measureBox(child);
127
+
128
+ return result;
129
+ }
130
+ }
131
+ }
132
+
133
+ const bytes = new Uint8Array(8);
134
+ const view = new DataView(bytes.buffer);
135
+
136
+ const u8 = (value: number) => {
137
+ return [(value % 0x100 + 0x100) % 0x100];
138
+ };
139
+
140
+ const u16 = (value: number) => {
141
+ view.setUint16(0, value, false);
142
+ return [bytes[0], bytes[1]] as number[];
143
+ };
144
+
145
+ const i16 = (value: number) => {
146
+ view.setInt16(0, value, false);
147
+ return [bytes[0], bytes[1]] as number[];
148
+ };
149
+
150
+ const u24 = (value: number) => {
151
+ view.setUint32(0, value, false);
152
+ return [bytes[1], bytes[2], bytes[3]] as number[];
153
+ };
154
+
155
+ const u32 = (value: number) => {
156
+ view.setUint32(0, value, false);
157
+ return [bytes[0], bytes[1], bytes[2], bytes[3]] as number[];
158
+ };
159
+
160
+ const i32 = (value: number) => {
161
+ view.setInt32(0, value, false);
162
+ return [bytes[0], bytes[1], bytes[2], bytes[3]] as number[];
163
+ };
164
+
165
+ const u64 = (value: number) => {
166
+ view.setUint32(0, Math.floor(value / 2 ** 32), false);
167
+ view.setUint32(4, value, false);
168
+ return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]] as number[];
169
+ };
170
+
171
+ const fixed_8_8 = (value: number) => {
172
+ view.setInt16(0, 2 ** 8 * value, false);
173
+ return [bytes[0], bytes[1]] as number[];
174
+ };
175
+
176
+ const fixed_16_16 = (value: number) => {
177
+ view.setInt32(0, 2 ** 16 * value, false);
178
+ return [bytes[0], bytes[1], bytes[2], bytes[3]] as number[];
179
+ };
180
+
181
+ const fixed_2_30 = (value: number) => {
182
+ view.setInt32(0, 2 ** 30 * value, false);
183
+ return [bytes[0], bytes[1], bytes[2], bytes[3]] as number[];
184
+ };
185
+
186
+ const variableUnsignedInt = (value: number, byteLength?: number) => {
187
+ const bytes: number[] = [];
188
+ let remaining = value;
189
+
190
+ do {
191
+ let byte = remaining & 0x7f;
192
+ remaining >>= 7;
193
+
194
+ // If this isn't the first byte we're adding (meaning there will be more bytes after it
195
+ // when we reverse the array), set the continuation bit
196
+ if (bytes.length > 0) {
197
+ byte |= 0x80;
198
+ }
199
+
200
+ bytes.push(byte);
201
+
202
+ if (byteLength !== undefined) {
203
+ byteLength--;
204
+ }
205
+ } while (remaining > 0 || byteLength);
206
+
207
+ // Reverse the array since we built it backwards
208
+ return bytes.reverse();
209
+ };
210
+
211
+ const ascii = (text: string, nullTerminated = false) => {
212
+ const bytes = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
213
+ if (nullTerminated) bytes.push(0x00);
214
+ return bytes;
215
+ };
216
+
217
+ const lastPresentedSample = (samples: Sample[]) => {
218
+ let result: Sample | null = null;
219
+
220
+ for (const sample of samples) {
221
+ if (!result || sample.timestamp > result.timestamp) {
222
+ result = sample;
223
+ }
224
+ }
225
+
226
+ return result;
227
+ };
228
+
229
+ const rotationMatrix = (rotationInDegrees: number): TransformationMatrix => {
230
+ const theta = rotationInDegrees * (Math.PI / 180);
231
+ const cosTheta = Math.round(Math.cos(theta));
232
+ const sinTheta = Math.round(Math.sin(theta));
233
+
234
+ // Matrices are post-multiplied in ISOBMFF, meaning this is the transpose of your typical rotation matrix
235
+ return [
236
+ cosTheta, sinTheta, 0,
237
+ -sinTheta, cosTheta, 0,
238
+ 0, 0, 1,
239
+ ];
240
+ };
241
+ const IDENTITY_MATRIX = rotationMatrix(0);
242
+
243
+ const matrixToBytes = (matrix: TransformationMatrix) => {
244
+ return [
245
+ fixed_16_16(matrix[0]), fixed_16_16(matrix[1]), fixed_2_30(matrix[2]),
246
+ fixed_16_16(matrix[3]), fixed_16_16(matrix[4]), fixed_2_30(matrix[5]),
247
+ fixed_16_16(matrix[6]), fixed_16_16(matrix[7]), fixed_2_30(matrix[8]),
248
+ ];
249
+ };
250
+
251
+ export interface Box {
252
+ type: string;
253
+ contents?: Uint8Array;
254
+ children?: (Box | null)[];
255
+ size?: number;
256
+ largeSize?: boolean;
257
+ }
258
+
259
+ type NestedNumberArray = (number | NestedNumberArray)[];
260
+
261
+ export const box = (type: string, contents?: NestedNumberArray, children?: (Box | null)[]): Box => ({
262
+ type,
263
+ contents: contents && new Uint8Array(contents.flat(10) as number[]),
264
+ children,
265
+ });
266
+
267
+ /** A FullBox always starts with a version byte, followed by three flag bytes. */
268
+ export const fullBox = (
269
+ type: string,
270
+ version: number,
271
+ flags: number,
272
+ contents?: NestedNumberArray,
273
+ children?: Box[],
274
+ ) => box(
275
+ type,
276
+ [u8(version), u24(flags), contents ?? []],
277
+ children,
278
+ );
279
+
280
+ /**
281
+ * File Type Compatibility Box: Allows the reader to determine whether this is a type of file that the
282
+ * reader understands.
283
+ */
284
+ export const ftyp = (details: {
285
+ isQuickTime: boolean;
286
+ holdsAvc: boolean;
287
+ fragmented: boolean;
288
+ }) => {
289
+ // You can find the full logic for this at
290
+ // https://github.com/FFmpeg/FFmpeg/blob/de2fb43e785773738c660cdafb9309b1ef1bc80d/libavformat/movenc.c#L5518
291
+ // Obviously, this lib only needs a small subset of that logic.
292
+
293
+ const minorVersion = 0x200;
294
+
295
+ if (details.isQuickTime) {
296
+ return box('ftyp', [
297
+ ascii('qt '), // Major brand
298
+ u32(minorVersion), // Minor version
299
+ // Compatible brands
300
+ ascii('qt '),
301
+ ]);
302
+ }
303
+
304
+ if (details.fragmented) {
305
+ return box('ftyp', [
306
+ ascii('iso5'), // Major brand
307
+ u32(minorVersion), // Minor version
308
+ // Compatible brands
309
+ ascii('iso5'),
310
+ ascii('iso6'),
311
+ ascii('mp41'),
312
+ ]);
313
+ }
314
+
315
+ return box('ftyp', [
316
+ ascii('isom'), // Major brand
317
+ u32(minorVersion), // Minor version
318
+ // Compatible brands
319
+ ascii('isom'),
320
+ details.holdsAvc ? ascii('avc1') : [],
321
+ ascii('mp41'),
322
+ ]);
323
+ };
324
+
325
+ /** Movie Sample Data Box. Contains the actual frames/samples of the media. */
326
+ export const mdat = (reserveLargeSize: boolean): Box => ({ type: 'mdat', largeSize: reserveLargeSize });
327
+
328
+ /**
329
+ * Movie Box: Used to specify the information that defines a movie - that is, the information that allows
330
+ * an application to interpret the sample data that is stored elsewhere.
331
+ */
332
+ export const moov = (
333
+ trackDatas: IsobmffTrackData[],
334
+ creationTime: number,
335
+ fragmented = false,
336
+ ) => box('moov', undefined, [
337
+ mvhd(creationTime, trackDatas),
338
+ ...trackDatas.map(x => trak(x, creationTime)),
339
+ fragmented ? mvex(trackDatas) : null,
340
+ ]);
341
+
342
+ /** Movie Header Box: Used to specify the characteristics of the entire movie, such as timescale and duration. */
343
+ export const mvhd = (
344
+ creationTime: number,
345
+ trackDatas: IsobmffTrackData[],
346
+ ) => {
347
+ const duration = intoTimescale(Math.max(
348
+ 0,
349
+ ...trackDatas
350
+ .filter(x => x.samples.length > 0)
351
+ .map((x) => {
352
+ const lastSample = lastPresentedSample(x.samples)!;
353
+ return lastSample.timestamp + lastSample.duration;
354
+ }),
355
+ ), GLOBAL_TIMESCALE);
356
+ const nextTrackId = Math.max(0, ...trackDatas.map(x => x.track.id)) + 1;
357
+
358
+ // Conditionally use u64 if u32 isn't enough
359
+ const needsU64 = !isU32(creationTime) || !isU32(duration);
360
+ const u32OrU64 = needsU64 ? u64 : u32;
361
+
362
+ return fullBox('mvhd', +needsU64, 0, [
363
+ u32OrU64(creationTime), // Creation time
364
+ u32OrU64(creationTime), // Modification time
365
+ u32(GLOBAL_TIMESCALE), // Timescale
366
+ u32OrU64(duration), // Duration
367
+ fixed_16_16(1), // Preferred rate
368
+ fixed_8_8(1), // Preferred volume
369
+ Array(10).fill(0), // Reserved
370
+ matrixToBytes(IDENTITY_MATRIX), // Matrix
371
+ Array(24).fill(0), // Pre-defined
372
+ u32(nextTrackId), // Next track ID
373
+ ]);
374
+ };
375
+
376
+ /**
377
+ * Track Box: Defines a single track of a movie. A movie may consist of one or more tracks. Each track is
378
+ * independent of the other tracks in the movie and carries its own temporal and spatial information. Each Track Box
379
+ * contains its associated Media Box.
380
+ */
381
+ export const trak = (trackData: IsobmffTrackData, creationTime: number) => box('trak', undefined, [
382
+ tkhd(trackData, creationTime),
383
+ mdia(trackData, creationTime),
384
+ ]);
385
+
386
+ /** Track Header Box: Specifies the characteristics of a single track within a movie. */
387
+ export const tkhd = (
388
+ trackData: IsobmffTrackData,
389
+ creationTime: number,
390
+ ) => {
391
+ const lastSample = lastPresentedSample(trackData.samples);
392
+ const durationInGlobalTimescale = intoTimescale(
393
+ lastSample ? lastSample.timestamp + lastSample.duration : 0,
394
+ GLOBAL_TIMESCALE,
395
+ );
396
+
397
+ const needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
398
+ const u32OrU64 = needsU64 ? u64 : u32;
399
+
400
+ let matrix: TransformationMatrix;
401
+ if (trackData.type === 'video') {
402
+ const rotation = trackData.track.metadata.rotation;
403
+ matrix = rotationMatrix(rotation ?? 0);
404
+ } else {
405
+ matrix = IDENTITY_MATRIX;
406
+ }
407
+
408
+ return fullBox('tkhd', +needsU64, 3, [
409
+ u32OrU64(creationTime), // Creation time
410
+ u32OrU64(creationTime), // Modification time
411
+ u32(trackData.track.id), // Track ID
412
+ u32(0), // Reserved
413
+ u32OrU64(durationInGlobalTimescale), // Duration
414
+ Array(8).fill(0), // Reserved
415
+ u16(0), // Layer
416
+ u16(trackData.track.id), // Alternate group
417
+ fixed_8_8(trackData.type === 'audio' ? 1 : 0), // Volume
418
+ u16(0), // Reserved
419
+ matrixToBytes(matrix), // Matrix
420
+ fixed_16_16(trackData.type === 'video' ? trackData.info.width : 0), // Track width
421
+ fixed_16_16(trackData.type === 'video' ? trackData.info.height : 0), // Track height
422
+ ]);
423
+ };
424
+
425
+ /** Media Box: Describes and define a track's media type and sample data. */
426
+ export const mdia = (trackData: IsobmffTrackData, creationTime: number) => box('mdia', undefined, [
427
+ mdhd(trackData, creationTime),
428
+ hdlr(trackData),
429
+ minf(trackData),
430
+ ]);
431
+
432
+ /** Media Header Box: Specifies the characteristics of a media, including timescale and duration. */
433
+ export const mdhd = (
434
+ trackData: IsobmffTrackData,
435
+ creationTime: number,
436
+ ) => {
437
+ const lastSample = lastPresentedSample(trackData.samples);
438
+ const localDuration = intoTimescale(
439
+ lastSample ? lastSample.timestamp + lastSample.duration : 0,
440
+ trackData.timescale,
441
+ );
442
+
443
+ const needsU64 = !isU32(creationTime) || !isU32(localDuration);
444
+ const u32OrU64 = needsU64 ? u64 : u32;
445
+
446
+ let language = 0;
447
+ for (const character of (trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE)) {
448
+ language <<= 5;
449
+ language += character.charCodeAt(0) - 0x60;
450
+ }
451
+
452
+ return fullBox('mdhd', +needsU64, 0, [
453
+ u32OrU64(creationTime), // Creation time
454
+ u32OrU64(creationTime), // Modification time
455
+ u32(trackData.timescale), // Timescale
456
+ u32OrU64(localDuration), // Duration
457
+ u16(language), // Language
458
+ u16(0), // Quality
459
+ ]);
460
+ };
461
+
462
+ const TRACK_TYPE_TO_COMPONENT_SUBTYPE: Record<IsobmffTrackData['type'], string> = {
463
+ video: 'vide',
464
+ audio: 'soun',
465
+ subtitle: 'text',
466
+ };
467
+
468
+ const TRACK_TYPE_TO_HANDLER_NAME: Record<IsobmffTrackData['type'], string> = {
469
+ video: 'MediabunnyVideoHandler',
470
+ audio: 'MediabunnySoundHandler',
471
+ subtitle: 'MediabunnyTextHandler',
472
+ };
473
+
474
+ /** Handler Reference Box: Specifies the media handler component that is to be used to interpret the media's data. */
475
+ export const hdlr = (trackData: IsobmffTrackData) => fullBox('hdlr', 0, 0, [
476
+ ascii('mhlr'), // Component type
477
+ ascii(TRACK_TYPE_TO_COMPONENT_SUBTYPE[trackData.type]), // Component subtype
478
+ u32(0), // Component manufacturer
479
+ u32(0), // Component flags
480
+ u32(0), // Component flags mask
481
+ ascii(TRACK_TYPE_TO_HANDLER_NAME[trackData.type], true), // Component name
482
+ ]);
483
+
484
+ /**
485
+ * Media Information Box: Stores handler-specific information for a track's media data. The media handler uses this
486
+ * information to map from media time to media data and to process the media data.
487
+ */
488
+ export const minf = (trackData: IsobmffTrackData) => box('minf', undefined, [
489
+ TRACK_TYPE_TO_HEADER_BOX[trackData.type](),
490
+ dinf(),
491
+ stbl(trackData),
492
+ ]);
493
+
494
+ /** Video Media Information Header Box: Defines specific color and graphics mode information. */
495
+ export const vmhd = () => fullBox('vmhd', 0, 1, [
496
+ u16(0), // Graphics mode
497
+ u16(0), // Opcolor R
498
+ u16(0), // Opcolor G
499
+ u16(0), // Opcolor B
500
+ ]);
501
+
502
+ /** Sound Media Information Header Box: Stores the sound media's control information, such as balance. */
503
+ export const smhd = () => fullBox('smhd', 0, 0, [
504
+ u16(0), // Balance
505
+ u16(0), // Reserved
506
+ ]);
507
+
508
+ /** Null Media Header Box. */
509
+ export const nmhd = () => fullBox('nmhd', 0, 0);
510
+
511
+ const TRACK_TYPE_TO_HEADER_BOX: Record<IsobmffTrackData['type'], () => Box> = {
512
+ video: vmhd,
513
+ audio: smhd,
514
+ subtitle: nmhd,
515
+ };
516
+
517
+ /**
518
+ * Data Information Box: Contains information specifying the data handler component that provides access to the
519
+ * media data. The data handler component uses the Data Information Box to interpret the media's data.
520
+ */
521
+ export const dinf = () => box('dinf', undefined, [
522
+ dref(),
523
+ ]);
524
+
525
+ /**
526
+ * Data Reference Box: Contains tabular data that instructs the data handler component how to access the media's data.
527
+ */
528
+ export const dref = () => fullBox('dref', 0, 0, [
529
+ u32(1), // Entry count
530
+ ], [
531
+ url(),
532
+ ]);
533
+
534
+ export const url = () => fullBox('url ', 0, 1); // Self-reference flag enabled
535
+
536
+ /**
537
+ * Sample Table Box: Contains information for converting from media time to sample number to sample location. This box
538
+ * also indicates how to interpret the sample (for example, whether to decompress the video data and, if so, how).
539
+ */
540
+ export const stbl = (trackData: IsobmffTrackData) => {
541
+ const needsCtts = trackData.compositionTimeOffsetTable.length > 1
542
+ || trackData.compositionTimeOffsetTable.some(x => x.sampleCompositionTimeOffset !== 0);
543
+
544
+ return box('stbl', undefined, [
545
+ stsd(trackData),
546
+ stts(trackData),
547
+ needsCtts ? ctts(trackData) : null,
548
+ needsCtts ? cslg(trackData) : null,
549
+ stsc(trackData),
550
+ stsz(trackData),
551
+ stco(trackData),
552
+ stss(trackData),
553
+ ]);
554
+ };
555
+
556
+ /**
557
+ * Sample Description Box: Stores information that allows you to decode samples in the media. The data stored in the
558
+ * sample description varies, depending on the media type.
559
+ */
560
+ export const stsd = (trackData: IsobmffTrackData) => {
561
+ let sampleDescription: Box;
562
+
563
+ if (trackData.type === 'video') {
564
+ sampleDescription = videoSampleDescription(
565
+ VIDEO_CODEC_TO_BOX_NAME[trackData.track.source._codec],
566
+ trackData,
567
+ );
568
+ } else if (trackData.type === 'audio') {
569
+ const boxName = audioCodecToBoxName(trackData.track.source._codec, trackData.muxer.isQuickTime);
570
+ assert(boxName);
571
+
572
+ sampleDescription = soundSampleDescription(
573
+ boxName,
574
+ trackData,
575
+ );
576
+ } else if (trackData.type === 'subtitle') {
577
+ sampleDescription = subtitleSampleDescription(
578
+ SUBTITLE_CODEC_TO_BOX_NAME[trackData.track.source._codec],
579
+ trackData,
580
+ );
581
+ }
582
+
583
+ assert(sampleDescription!);
584
+
585
+ return fullBox('stsd', 0, 0, [
586
+ u32(1), // Entry count
587
+ ], [
588
+ sampleDescription,
589
+ ]);
590
+ };
591
+
592
+ /** Video Sample Description Box: Contains information that defines how to interpret video media data. */
593
+ export const videoSampleDescription = (
594
+ compressionType: string,
595
+ trackData: IsobmffVideoTrackData,
596
+ ) => box(compressionType, [
597
+ Array(6).fill(0), // Reserved
598
+ u16(1), // Data reference index
599
+ u16(0), // Pre-defined
600
+ u16(0), // Reserved
601
+ Array(12).fill(0), // Pre-defined
602
+ u16(trackData.info.width), // Width
603
+ u16(trackData.info.height), // Height
604
+ u32(0x00480000), // Horizontal resolution
605
+ u32(0x00480000), // Vertical resolution
606
+ u32(0), // Reserved
607
+ u16(1), // Frame count
608
+ Array(32).fill(0), // Compressor name
609
+ u16(0x0018), // Depth
610
+ i16(0xffff), // Pre-defined
611
+ ], [
612
+ VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
613
+ colorSpaceIsComplete(trackData.info.decoderConfig.colorSpace) ? colr(trackData) : null,
614
+ ]);
615
+
616
+ /** Colour Information Box: Specifies the color space of the video. */
617
+ export const colr = (trackData: IsobmffVideoTrackData) => box('colr', [
618
+ ascii('nclx'), // Colour type
619
+ u16(COLOR_PRIMARIES_MAP[trackData.info.decoderConfig.colorSpace!.primaries!]), // Colour primaries
620
+ u16(TRANSFER_CHARACTERISTICS_MAP[trackData.info.decoderConfig.colorSpace!.transfer!]), // Transfer characteristics
621
+ u16(MATRIX_COEFFICIENTS_MAP[trackData.info.decoderConfig.colorSpace!.matrix!]), // Matrix coefficients
622
+ u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7), // Full range flag
623
+ ]);
624
+
625
+ /** AVC Configuration Box: Provides additional information to the decoder. */
626
+ export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('avcC', [
627
+ // For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
628
+ ...toUint8Array(trackData.info.decoderConfig.description!),
629
+ ]);
630
+
631
+ /** HEVC Configuration Box: Provides additional information to the decoder. */
632
+ export const hvcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('hvcC', [
633
+ // For HEVC, description is an HEVCDecoderConfigurationRecord, so nothing else to do here
634
+ ...toUint8Array(trackData.info.decoderConfig.description!),
635
+ ]);
636
+
637
+ /** VP Configuration Box: Provides additional information to the decoder. */
638
+ export const vpcC = (trackData: IsobmffVideoTrackData) => {
639
+ // Reference: https://www.webmproject.org/vp9/mp4/
640
+
641
+ if (!trackData.info.decoderConfig) {
642
+ return null;
643
+ }
644
+
645
+ const decoderConfig = trackData.info.decoderConfig;
646
+
647
+ const parts = decoderConfig.codec.split('.'); // We can derive the required values from the codec string
648
+ const profile = Number(parts[1]);
649
+ const level = Number(parts[2]);
650
+
651
+ const bitDepth = Number(parts[3]);
652
+ const chromaSubsampling = parts[4] ? Number(parts[4]) : 1; // 4:2:0 colocated with luma (0,0)
653
+ const videoFullRangeFlag = parts[8] ? Number(parts[8]) : Number(decoderConfig.colorSpace?.fullRange ?? 0);
654
+ const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + videoFullRangeFlag;
655
+
656
+ const colourPrimaries = parts[5]
657
+ ? Number(parts[5])
658
+ : decoderConfig.colorSpace?.primaries
659
+ ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries]
660
+ : 2; // Default to undetermined
661
+ const transferCharacteristics = parts[6]
662
+ ? Number(parts[6])
663
+ : decoderConfig.colorSpace?.transfer
664
+ ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer]
665
+ : 2;
666
+ const matrixCoefficients = parts[7]
667
+ ? Number(parts[7])
668
+ : decoderConfig.colorSpace?.matrix
669
+ ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix]
670
+ : 2;
671
+
672
+ return fullBox('vpcC', 1, 0, [
673
+ u8(profile), // Profile
674
+ u8(level), // Level
675
+ u8(thirdByte), // Bit depth, chroma subsampling, full range
676
+ u8(colourPrimaries), // Colour primaries
677
+ u8(transferCharacteristics), // Transfer characteristics
678
+ u8(matrixCoefficients), // Matrix coefficients
679
+ u16(0), // Codec initialization data size
680
+ ]);
681
+ };
682
+
683
+ /** AV1 Configuration Box: Provides additional information to the decoder. */
684
+ export const av1C = (trackData: IsobmffVideoTrackData) => {
685
+ return box('av1C', generateAv1CodecConfigurationFromCodecString(trackData.info.decoderConfig.codec));
686
+ };
687
+
688
+ /** Sound Sample Description Box: Contains information that defines how to interpret sound media data. */
689
+ export const soundSampleDescription = (
690
+ compressionType: string,
691
+ trackData: IsobmffAudioTrackData,
692
+ ) => {
693
+ let version = 0;
694
+ let contents: NestedNumberArray;
695
+
696
+ let sampleSizeInBits = 16;
697
+ if ((PCM_AUDIO_CODECS as readonly AudioCodec[]).includes(trackData.track.source._codec)) {
698
+ const codec = trackData.track.source._codec as PcmAudioCodec;
699
+ const { sampleSize } = parsePcmCodec(codec);
700
+ sampleSizeInBits = 8 * sampleSize;
701
+
702
+ if (sampleSizeInBits > 16) {
703
+ version = 1;
704
+ }
705
+ }
706
+
707
+ if (version === 0) {
708
+ contents = [
709
+ Array(6).fill(0), // Reserved
710
+ u16(1), // Data reference index
711
+ u16(version), // Version
712
+ u16(0), // Revision level
713
+ u32(0), // Vendor
714
+ u16(trackData.info.numberOfChannels), // Number of channels
715
+ u16(sampleSizeInBits), // Sample size (bits)
716
+ u16(0), // Compression ID
717
+ u16(0), // Packet size
718
+ u16(trackData.info.sampleRate < 2 ** 16 ? trackData.info.sampleRate : 0), // Sample rate (upper)
719
+ u16(0), // Sample rate (lower)
720
+ ];
721
+ } else {
722
+ contents = [
723
+ Array(6).fill(0), // Reserved
724
+ u16(1), // Data reference index
725
+ u16(version), // Version
726
+ u16(0), // Revision level
727
+ u32(0), // Vendor
728
+ u16(trackData.info.numberOfChannels), // Number of channels
729
+ u16(Math.min(sampleSizeInBits, 16)), // Sample size (bits)
730
+ u16(0), // Compression ID
731
+ u16(0), // Packet size
732
+ u16(trackData.info.sampleRate < 2 ** 16 ? trackData.info.sampleRate : 0), // Sample rate (upper)
733
+ u16(0), // Sample rate (lower)
734
+ u32(1), // Samples per packet (must be 1 for uncompressed formats)
735
+ u32(sampleSizeInBits / 8), // Bytes per packet
736
+ u32(trackData.info.numberOfChannels * sampleSizeInBits / 8), // Bytes per frame
737
+ u32(2), // Bytes per sample (constant in FFmpeg)
738
+ ];
739
+ }
740
+
741
+ return box(compressionType, contents, [
742
+ audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null,
743
+ ]);
744
+ };
745
+
746
+ /** MPEG-4 Elementary Stream Descriptor Box. */
747
+ export const esds = (trackData: IsobmffAudioTrackData) => {
748
+ // We build up the bytes in a layered way which reflects the nested structure
749
+
750
+ let objectTypeIndication: number;
751
+ switch (trackData.track.source._codec) {
752
+ case 'aac': {
753
+ objectTypeIndication = 0x40;
754
+ }; break;
755
+ case 'mp3': {
756
+ objectTypeIndication = 0x6b;
757
+ }; break;
758
+ case 'vorbis': {
759
+ objectTypeIndication = 0xdd;
760
+ }; break;
761
+ default: throw new Error(`Unhandled audio codec: ${trackData.track.source._codec}`);
762
+ }
763
+
764
+ let bytes = [
765
+ ...u8(objectTypeIndication), // Object type indication
766
+ ...u8(0x15), // stream type(6bits)=5 audio, flags(2bits)=1
767
+ ...u24(0), // 24bit buffer size
768
+ ...u32(0), // max bitrate
769
+ ...u32(0), // avg bitrate
770
+ ];
771
+ if (trackData.info.decoderConfig.description) {
772
+ const description = toUint8Array(trackData.info.decoderConfig.description);
773
+
774
+ // Add the decoder description to the end
775
+ bytes = [
776
+ ...bytes,
777
+ ...u8(0x05), // TAG(5) = DecoderSpecificInfo
778
+ ...variableUnsignedInt(description.byteLength),
779
+ ...description,
780
+ ];
781
+ }
782
+
783
+ bytes = [
784
+ ...u16(1), // ES_ID = 1
785
+ ...u8(0x00), // flags etc = 0
786
+ ...u8(0x04), // TAG(4) = ES Descriptor
787
+ ...variableUnsignedInt(bytes.length),
788
+ ...bytes,
789
+ ...u8(0x06), // TAG(6)
790
+ ...u8(0x01), // length
791
+ ...u8(0x02), // data
792
+ ];
793
+ bytes = [
794
+ ...u8(0x03), // TAG(3) = Object Descriptor
795
+ ...variableUnsignedInt(bytes.length),
796
+ ...bytes,
797
+ ];
798
+
799
+ return fullBox('esds', 0, 0, bytes);
800
+ };
801
+
802
+ export const wave = (trackData: IsobmffAudioTrackData) => {
803
+ return box('wave', undefined, [
804
+ frma(trackData),
805
+ enda(trackData),
806
+ box('\x00\x00\x00\x00'), // NULL tag at the end
807
+ ]);
808
+ };
809
+
810
+ export const frma = (trackData: IsobmffAudioTrackData) => {
811
+ return box('frma', [
812
+ ascii(audioCodecToBoxName(trackData.track.source._codec, trackData.muxer.isQuickTime)),
813
+ ]);
814
+ };
815
+
816
+ // This box specifies PCM endianness
817
+ export const enda = (trackData: IsobmffAudioTrackData) => {
818
+ const { littleEndian } = parsePcmCodec(trackData.track.source._codec as PcmAudioCodec);
819
+
820
+ return box('enda', [
821
+ u16(+littleEndian),
822
+ ]);
823
+ };
824
+
825
+ /** Opus Specific Box. */
826
+ export const dOps = (trackData: IsobmffAudioTrackData) => {
827
+ let outputChannelCount = trackData.info.numberOfChannels;
828
+ // Default PreSkip, should be at least 80 milliseconds worth of playback, measured in 48000 Hz samples
829
+ let preSkip = 3840;
830
+ let inputSampleRate = trackData.info.sampleRate;
831
+ let outputGain = 0;
832
+ let channelMappingFamily = 0;
833
+ let channelMappingTable = new Uint8Array(0);
834
+
835
+ // Read preskip and from codec private data from the encoder
836
+ // https://www.rfc-editor.org/rfc/rfc7845#section-5
837
+ const description = trackData.info.decoderConfig?.description;
838
+ if (description) {
839
+ assert(description.byteLength >= 18);
840
+
841
+ const bytes = toUint8Array(description);
842
+ const header = parseOpusIdentificationHeader(bytes);
843
+
844
+ outputChannelCount = header.outputChannelCount;
845
+ preSkip = header.preSkip;
846
+ inputSampleRate = header.inputSampleRate;
847
+ outputGain = header.outputGain;
848
+ channelMappingFamily = header.channelMappingFamily;
849
+
850
+ if (header.channelMappingTable) {
851
+ channelMappingTable = header.channelMappingTable;
852
+ }
853
+ }
854
+
855
+ // https://www.opus-codec.org/docs/opus_in_isobmff.html
856
+ return box('dOps', [
857
+ u8(0), // Version
858
+ u8(outputChannelCount), // OutputChannelCount
859
+ u16(preSkip), // PreSkip
860
+ u32(inputSampleRate), // InputSampleRate
861
+ i16(outputGain), // OutputGain
862
+ u8(channelMappingFamily), // ChannelMappingFamily
863
+ ...channelMappingTable,
864
+ ]);
865
+ };
866
+
867
+ /** FLAC specific box. */
868
+ export const dfLa = (trackData: IsobmffAudioTrackData) => {
869
+ const description = trackData.info.decoderConfig?.description;
870
+ assert(description);
871
+
872
+ const bytes = toUint8Array(description);
873
+
874
+ return fullBox('dfLa', 0, 0, [
875
+ ...bytes.subarray(4),
876
+ ]);
877
+ };
878
+
879
+ /** PCM Configuration Box, ISO/IEC 23003-5. */
880
+ const pcmC = (trackData: IsobmffAudioTrackData) => {
881
+ const { littleEndian, sampleSize } = parsePcmCodec(trackData.track.source._codec as PcmAudioCodec);
882
+ const formatFlags = +littleEndian;
883
+
884
+ return fullBox('pcmC', 0, 0, [
885
+ u8(formatFlags),
886
+ u8(8 * sampleSize),
887
+ ]);
888
+ };
889
+
890
+ export const subtitleSampleDescription = (
891
+ compressionType: string,
892
+ trackData: IsobmffSubtitleTrackData,
893
+ ) => box(compressionType, [
894
+ Array(6).fill(0), // Reserved
895
+ u16(1), // Data reference index
896
+ ], [
897
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
898
+ ]);
899
+
900
+ export const vttC = (trackData: IsobmffSubtitleTrackData) => box('vttC', [
901
+ ...textEncoder.encode(trackData.info.config.description),
902
+ ]);
903
+
904
+ export const txtC = (textConfig: Uint8Array) => fullBox('txtC', 0, 0, [
905
+ ...textConfig, 0, // Text config (null-terminated)
906
+ ]);
907
+
908
+ /**
909
+ * Time-To-Sample Box: Stores duration information for a media's samples, providing a mapping from a time in a media
910
+ * to the corresponding data sample. The table is compact, meaning that consecutive samples with the same time delta
911
+ * will be grouped.
912
+ */
913
+ export const stts = (trackData: IsobmffTrackData) => {
914
+ return fullBox('stts', 0, 0, [
915
+ u32(trackData.timeToSampleTable.length), // Number of entries
916
+ trackData.timeToSampleTable.map(x => [ // Time-to-sample table
917
+ u32(x.sampleCount), // Sample count
918
+ u32(x.sampleDelta), // Sample duration
919
+ ]),
920
+ ]);
921
+ };
922
+
923
+ /** Sync Sample Box: Identifies the key frames in the media, marking the random access points within a stream. */
924
+ export const stss = (trackData: IsobmffTrackData) => {
925
+ if (trackData.samples.every(x => x.type === 'key')) return null; // No stss box -> every frame is a key frame
926
+
927
+ const keySamples = [...trackData.samples.entries()].filter(([, sample]) => sample.type === 'key');
928
+ return fullBox('stss', 0, 0, [
929
+ u32(keySamples.length), // Number of entries
930
+ keySamples.map(([index]) => u32(index + 1)), // Sync sample table
931
+ ]);
932
+ };
933
+
934
+ /**
935
+ * Sample-To-Chunk Box: As samples are added to a media, they are collected into chunks that allow optimized data
936
+ * access. A chunk contains one or more samples. Chunks in a media may have different sizes, and the samples within a
937
+ * chunk may have different sizes. The Sample-To-Chunk Box stores chunk information for the samples in a media, stored
938
+ * in a compactly-coded fashion.
939
+ */
940
+ export const stsc = (trackData: IsobmffTrackData) => {
941
+ return fullBox('stsc', 0, 0, [
942
+ u32(trackData.compactlyCodedChunkTable.length), // Number of entries
943
+ trackData.compactlyCodedChunkTable.map(x => [ // Sample-to-chunk table
944
+ u32(x.firstChunk), // First chunk
945
+ u32(x.samplesPerChunk), // Samples per chunk
946
+ u32(1), // Sample description index
947
+ ]),
948
+ ]);
949
+ };
950
+
951
+ /** Sample Size Box: Specifies the byte size of each sample in the media. */
952
+ export const stsz = (trackData: IsobmffTrackData) => {
953
+ if (trackData.type === 'audio' && trackData.info.requiresPcmTransformation) {
954
+ const { sampleSize } = parsePcmCodec(trackData.track.source._codec as PcmAudioCodec);
955
+
956
+ // With PCM, every sample has the same size
957
+ return fullBox('stsz', 0, 0, [
958
+ u32(sampleSize * trackData.info.numberOfChannels), // Sample size
959
+ u32(trackData.samples.reduce((acc, x) => acc + intoTimescale(x.duration, trackData.timescale), 0)),
960
+ ]);
961
+ }
962
+
963
+ return fullBox('stsz', 0, 0, [
964
+ u32(0), // Sample size (0 means non-constant size)
965
+ u32(trackData.samples.length), // Number of entries
966
+ trackData.samples.map(x => u32(x.size)), // Sample size table
967
+ ]);
968
+ };
969
+
970
+ /** Chunk Offset Box: Identifies the location of each chunk of data in the media's data stream, relative to the file. */
971
+ export const stco = (trackData: IsobmffTrackData) => {
972
+ if (trackData.finalizedChunks.length > 0 && last(trackData.finalizedChunks)!.offset! >= 2 ** 32) {
973
+ // If the file is large, use the co64 box
974
+ return fullBox('co64', 0, 0, [
975
+ u32(trackData.finalizedChunks.length), // Number of entries
976
+ trackData.finalizedChunks.map(x => u64(x.offset!)), // Chunk offset table
977
+ ]);
978
+ }
979
+
980
+ return fullBox('stco', 0, 0, [
981
+ u32(trackData.finalizedChunks.length), // Number of entries
982
+ trackData.finalizedChunks.map(x => u32(x.offset!)), // Chunk offset table
983
+ ]);
984
+ };
985
+
986
+ /**
987
+ * Composition Time to Sample Box: Stores composition time offset information (PTS-DTS) for a
988
+ * media's samples. The table is compact, meaning that consecutive samples with the same time
989
+ * composition time offset will be grouped.
990
+ */
991
+ export const ctts = (trackData: IsobmffTrackData) => {
992
+ return fullBox('ctts', 1, 0, [
993
+ u32(trackData.compositionTimeOffsetTable.length), // Number of entries
994
+ trackData.compositionTimeOffsetTable.map(x => [ // Time-to-sample table
995
+ u32(x.sampleCount), // Sample count
996
+ i32(x.sampleCompositionTimeOffset), // Sample offset
997
+ ]),
998
+ ]);
999
+ };
1000
+
1001
+ /**
1002
+ * Composition to Decode Box: Stores information about the composition and display times of the media samples.
1003
+ */
1004
+ export const cslg = (trackData: IsobmffTrackData) => {
1005
+ let leastDecodeToDisplayDelta = Infinity;
1006
+ let greatestDecodeToDisplayDelta = -Infinity;
1007
+ let compositionStartTime = Infinity;
1008
+ let compositionEndTime = -Infinity;
1009
+
1010
+ assert(trackData.compositionTimeOffsetTable.length > 0);
1011
+ assert(trackData.samples.length > 0);
1012
+
1013
+ for (let i = 0; i < trackData.compositionTimeOffsetTable.length; i++) {
1014
+ const entry = trackData.compositionTimeOffsetTable[i]!;
1015
+ leastDecodeToDisplayDelta = Math.min(leastDecodeToDisplayDelta, entry.sampleCompositionTimeOffset);
1016
+ greatestDecodeToDisplayDelta = Math.max(greatestDecodeToDisplayDelta, entry.sampleCompositionTimeOffset);
1017
+ }
1018
+
1019
+ for (let i = 0; i < trackData.samples.length; i++) {
1020
+ const sample = trackData.samples[i]!;
1021
+ compositionStartTime = Math.min(
1022
+ compositionStartTime,
1023
+ intoTimescale(sample.timestamp, trackData.timescale),
1024
+ );
1025
+ compositionEndTime = Math.max(
1026
+ compositionEndTime,
1027
+ intoTimescale(sample.timestamp + sample.duration, trackData.timescale),
1028
+ );
1029
+ }
1030
+
1031
+ const compositionToDtsShift = Math.max(-leastDecodeToDisplayDelta, 0);
1032
+
1033
+ if (compositionEndTime >= 2 ** 31) {
1034
+ // For very large files, the composition end time can't be represented in i32, so let's just scrap the box in
1035
+ // that case. QuickTime fails to read the file if there's a cslg box with version 1, so that's sadly not an
1036
+ // option.
1037
+ return null;
1038
+ }
1039
+
1040
+ return fullBox('cslg', 0, 0, [
1041
+ i32(compositionToDtsShift), // Composition to DTS shift
1042
+ i32(leastDecodeToDisplayDelta), // Least decode to display delta
1043
+ i32(greatestDecodeToDisplayDelta), // Greatest decode to display delta
1044
+ i32(compositionStartTime), // Composition start time
1045
+ i32(compositionEndTime), // Composition end time
1046
+ ]);
1047
+ };
1048
+
1049
+ /**
1050
+ * Movie Extends Box: This box signals to readers that the file is fragmented. Contains a single Track Extends Box
1051
+ * for each track in the movie.
1052
+ */
1053
+ export const mvex = (trackDatas: IsobmffTrackData[]) => {
1054
+ return box('mvex', undefined, trackDatas.map(trex));
1055
+ };
1056
+
1057
+ /** Track Extends Box: Contains the default values used by the movie fragments. */
1058
+ export const trex = (trackData: IsobmffTrackData) => {
1059
+ return fullBox('trex', 0, 0, [
1060
+ u32(trackData.track.id), // Track ID
1061
+ u32(1), // Default sample description index
1062
+ u32(0), // Default sample duration
1063
+ u32(0), // Default sample size
1064
+ u32(0), // Default sample flags
1065
+ ]);
1066
+ };
1067
+
1068
+ /**
1069
+ * Movie Fragment Box: The movie fragments extend the presentation in time. They provide the information that would
1070
+ * previously have been in the Movie Box.
1071
+ */
1072
+ export const moof = (sequenceNumber: number, trackDatas: IsobmffTrackData[]) => {
1073
+ return box('moof', undefined, [
1074
+ mfhd(sequenceNumber),
1075
+ ...trackDatas.map(traf),
1076
+ ]);
1077
+ };
1078
+
1079
+ /** Movie Fragment Header Box: Contains a sequence number as a safety check. */
1080
+ export const mfhd = (sequenceNumber: number) => {
1081
+ return fullBox('mfhd', 0, 0, [
1082
+ u32(sequenceNumber), // Sequence number
1083
+ ]);
1084
+ };
1085
+
1086
+ const fragmentSampleFlags = (sample: Sample) => {
1087
+ let byte1 = 0;
1088
+ let byte2 = 0;
1089
+ const byte3 = 0;
1090
+ const byte4 = 0;
1091
+
1092
+ const sampleIsDifferenceSample = sample.type === 'delta';
1093
+ byte2 |= +sampleIsDifferenceSample;
1094
+
1095
+ if (sampleIsDifferenceSample) {
1096
+ byte1 |= 1; // There is redundant coding in this sample
1097
+ } else {
1098
+ byte1 |= 2; // There is no redundant coding in this sample
1099
+ }
1100
+
1101
+ // Note that there are a lot of other flags to potentially set here, but most are irrelevant / non-necessary
1102
+ return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4;
1103
+ };
1104
+
1105
+ /** Track Fragment Box */
1106
+ export const traf = (trackData: IsobmffTrackData) => {
1107
+ return box('traf', undefined, [
1108
+ tfhd(trackData),
1109
+ tfdt(trackData),
1110
+ trun(trackData),
1111
+ ]);
1112
+ };
1113
+
1114
+ /** Track Fragment Header Box: Provides a reference to the extended track, and flags. */
1115
+ export const tfhd = (trackData: IsobmffTrackData) => {
1116
+ assert(trackData.currentChunk);
1117
+
1118
+ let tfFlags = 0;
1119
+ tfFlags |= 0x00008; // Default sample duration present
1120
+ tfFlags |= 0x00010; // Default sample size present
1121
+ tfFlags |= 0x00020; // Default sample flags present
1122
+ tfFlags |= 0x20000; // Default base is moof
1123
+
1124
+ // Prefer the second sample over the first one, as the first one is a sync sample and therefore the "odd one out"
1125
+ const referenceSample = trackData.currentChunk.samples[1] ?? trackData.currentChunk.samples[0]!;
1126
+ const referenceSampleInfo = {
1127
+ duration: referenceSample.timescaleUnitsToNextSample,
1128
+ size: referenceSample.size,
1129
+ flags: fragmentSampleFlags(referenceSample),
1130
+ };
1131
+
1132
+ return fullBox('tfhd', 0, tfFlags, [
1133
+ u32(trackData.track.id), // Track ID
1134
+ u32(referenceSampleInfo.duration), // Default sample duration
1135
+ u32(referenceSampleInfo.size), // Default sample size
1136
+ u32(referenceSampleInfo.flags), // Default sample flags
1137
+ ]);
1138
+ };
1139
+
1140
+ /**
1141
+ * Track Fragment Decode Time Box: Provides the absolute decode time of the first sample of the fragment. This is
1142
+ * useful for performing random access on the media file.
1143
+ */
1144
+ export const tfdt = (trackData: IsobmffTrackData) => {
1145
+ assert(trackData.currentChunk);
1146
+
1147
+ return fullBox('tfdt', 1, 0, [
1148
+ u64(intoTimescale(trackData.currentChunk.startTimestamp, trackData.timescale)), // Base Media Decode Time
1149
+ ]);
1150
+ };
1151
+
1152
+ /** Track Run Box: Specifies a run of contiguous samples for a given track. */
1153
+ export const trun = (trackData: IsobmffTrackData) => {
1154
+ assert(trackData.currentChunk);
1155
+
1156
+ const allSampleDurations = trackData.currentChunk.samples.map(x => x.timescaleUnitsToNextSample);
1157
+ const allSampleSizes = trackData.currentChunk.samples.map(x => x.size);
1158
+ const allSampleFlags = trackData.currentChunk.samples.map(fragmentSampleFlags);
1159
+ const allSampleCompositionTimeOffsets = trackData.currentChunk.samples
1160
+ .map(x => intoTimescale(x.timestamp - x.decodeTimestamp, trackData.timescale));
1161
+
1162
+ const uniqueSampleDurations = new Set(allSampleDurations);
1163
+ const uniqueSampleSizes = new Set(allSampleSizes);
1164
+ const uniqueSampleFlags = new Set(allSampleFlags);
1165
+ const uniqueSampleCompositionTimeOffsets = new Set(allSampleCompositionTimeOffsets);
1166
+
1167
+ const firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1];
1168
+ const sampleDurationPresent = uniqueSampleDurations.size > 1;
1169
+ const sampleSizePresent = uniqueSampleSizes.size > 1;
1170
+ const sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1;
1171
+ const sampleCompositionTimeOffsetsPresent
1172
+ = uniqueSampleCompositionTimeOffsets.size > 1 || [...uniqueSampleCompositionTimeOffsets].some(x => x !== 0);
1173
+
1174
+ let flags = 0;
1175
+ flags |= 0x0001; // Data offset present
1176
+ flags |= 0x0004 * +firstSampleFlagsPresent; // First sample flags present
1177
+ flags |= 0x0100 * +sampleDurationPresent; // Sample duration present
1178
+ flags |= 0x0200 * +sampleSizePresent; // Sample size present
1179
+ flags |= 0x0400 * +sampleFlagsPresent; // Sample flags present
1180
+ flags |= 0x0800 * +sampleCompositionTimeOffsetsPresent; // Sample composition time offsets present
1181
+
1182
+ return fullBox('trun', 1, flags, [
1183
+ u32(trackData.currentChunk.samples.length), // Sample count
1184
+ u32(trackData.currentChunk.offset! - trackData.currentChunk.moofOffset! || 0), // Data offset
1185
+ firstSampleFlagsPresent ? u32(allSampleFlags[0]!) : [],
1186
+ trackData.currentChunk.samples.map((_, i) => [
1187
+ sampleDurationPresent ? u32(allSampleDurations[i]!) : [], // Sample duration
1188
+ sampleSizePresent ? u32(allSampleSizes[i]!) : [], // Sample size
1189
+ sampleFlagsPresent ? u32(allSampleFlags[i]!) : [], // Sample flags
1190
+ // Sample composition time offsets
1191
+ sampleCompositionTimeOffsetsPresent ? i32(allSampleCompositionTimeOffsets[i]!) : [],
1192
+ ]),
1193
+ ]);
1194
+ };
1195
+
1196
+ /**
1197
+ * Movie Fragment Random Access Box: For each track, provides pointers to sync samples within the file
1198
+ * for random access.
1199
+ */
1200
+ export const mfra = (trackDatas: IsobmffTrackData[]) => {
1201
+ return box('mfra', undefined, [
1202
+ ...trackDatas.map(tfra),
1203
+ mfro(),
1204
+ ]);
1205
+ };
1206
+
1207
+ /** Track Fragment Random Access Box: Provides pointers to sync samples within the file for random access. */
1208
+ export const tfra = (trackData: IsobmffTrackData, trackIndex: number) => {
1209
+ const version = 1; // Using this version allows us to use 64-bit time and offset values
1210
+
1211
+ return fullBox('tfra', version, 0, [
1212
+ u32(trackData.track.id), // Track ID
1213
+ u32(0b111111), // This specifies that traf number, trun number and sample number are 32-bit ints
1214
+ u32(trackData.finalizedChunks.length), // Number of entries
1215
+ trackData.finalizedChunks.map(chunk => [
1216
+ u64(intoTimescale(chunk.samples[0]!.timestamp, trackData.timescale)), // Time (in presentation time)
1217
+ u64(chunk.moofOffset!), // moof offset
1218
+ u32(trackIndex + 1), // traf number
1219
+ u32(1), // trun number
1220
+ u32(1), // Sample number
1221
+ ]),
1222
+ ]);
1223
+ };
1224
+
1225
+ /**
1226
+ * Movie Fragment Random Access Offset Box: Provides the size of the enclosing mfra box. This box can be used by readers
1227
+ * to quickly locate the mfra box by searching from the end of the file.
1228
+ */
1229
+ export const mfro = () => {
1230
+ return fullBox('mfro', 0, 0, [
1231
+ // This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box
1232
+ // is known
1233
+ u32(0), // Size
1234
+ ]);
1235
+ };
1236
+
1237
+ /** VTT Empty Cue Box */
1238
+ export const vtte = () => box('vtte');
1239
+
1240
+ /** VTT Cue Box */
1241
+ export const vttc = (
1242
+ payload: string,
1243
+ timestamp: number | null,
1244
+ identifier: string | null,
1245
+ settings: string | null,
1246
+ sourceId: number | null,
1247
+ ) => box('vttc', undefined, [
1248
+ sourceId !== null ? box('vsid', [i32(sourceId)]) : null,
1249
+ identifier !== null ? box('iden', [...textEncoder.encode(identifier)]) : null,
1250
+ timestamp !== null ? box('ctim', [...textEncoder.encode(formatSubtitleTimestamp(timestamp))]) : null,
1251
+ settings !== null ? box('sttg', [...textEncoder.encode(settings)]) : null,
1252
+ box('payl', [...textEncoder.encode(payload)]),
1253
+ ]);
1254
+
1255
+ /** VTT Additional Text Box */
1256
+ export const vtta = (notes: string) => box('vtta', [...textEncoder.encode(notes)]);
1257
+
1258
+ const VIDEO_CODEC_TO_BOX_NAME: Record<VideoCodec, string> = {
1259
+ avc: 'avc1',
1260
+ hevc: 'hvc1',
1261
+ vp8: 'vp08',
1262
+ vp9: 'vp09',
1263
+ av1: 'av01',
1264
+ };
1265
+
1266
+ const VIDEO_CODEC_TO_CONFIGURATION_BOX: Record<VideoCodec, (trackData: IsobmffVideoTrackData) => Box | null> = {
1267
+ avc: avcC,
1268
+ hevc: hvcC,
1269
+ vp8: vpcC,
1270
+ vp9: vpcC,
1271
+ av1: av1C,
1272
+ };
1273
+
1274
+ const audioCodecToBoxName = (codec: AudioCodec, isQuickTime: boolean): string => {
1275
+ switch (codec) {
1276
+ case 'aac': return 'mp4a';
1277
+ case 'mp3': return 'mp4a';
1278
+ case 'opus': return 'Opus';
1279
+ case 'vorbis': return 'mp4a';
1280
+ case 'flac': return 'fLaC';
1281
+ case 'ulaw': return 'ulaw';
1282
+ case 'alaw': return 'alaw';
1283
+ case 'pcm-u8': return 'raw ';
1284
+ case 'pcm-s8': return 'sowt';
1285
+ }
1286
+
1287
+ // Logic diverges here
1288
+ if (isQuickTime) {
1289
+ switch (codec) {
1290
+ case 'pcm-s16': return 'sowt';
1291
+ case 'pcm-s16be': return 'twos';
1292
+ case 'pcm-s24': return 'in24';
1293
+ case 'pcm-s24be': return 'in24';
1294
+ case 'pcm-s32': return 'in32';
1295
+ case 'pcm-s32be': return 'in32';
1296
+ case 'pcm-f32': return 'fl32';
1297
+ case 'pcm-f32be': return 'fl32';
1298
+ case 'pcm-f64': return 'fl64';
1299
+ case 'pcm-f64be': return 'fl64';
1300
+ }
1301
+ } else {
1302
+ switch (codec) {
1303
+ case 'pcm-s16': return 'ipcm';
1304
+ case 'pcm-s16be': return 'ipcm';
1305
+ case 'pcm-s24': return 'ipcm';
1306
+ case 'pcm-s24be': return 'ipcm';
1307
+ case 'pcm-s32': return 'ipcm';
1308
+ case 'pcm-s32be': return 'ipcm';
1309
+ case 'pcm-f32': return 'fpcm';
1310
+ case 'pcm-f32be': return 'fpcm';
1311
+ case 'pcm-f64': return 'fpcm';
1312
+ case 'pcm-f64be': return 'fpcm';
1313
+ }
1314
+ }
1315
+ };
1316
+
1317
+ const audioCodecToConfigurationBox = (codec: AudioCodec, isQuickTime: boolean) => {
1318
+ switch (codec) {
1319
+ case 'aac': return esds;
1320
+ case 'mp3': return esds;
1321
+ case 'opus': return dOps;
1322
+ case 'vorbis': return esds;
1323
+ case 'flac': return dfLa;
1324
+ }
1325
+
1326
+ // Logic diverges here
1327
+ if (isQuickTime) {
1328
+ switch (codec) {
1329
+ case 'pcm-s24': return wave;
1330
+ case 'pcm-s24be': return wave;
1331
+ case 'pcm-s32': return wave;
1332
+ case 'pcm-s32be': return wave;
1333
+ case 'pcm-f32': return wave;
1334
+ case 'pcm-f32be': return wave;
1335
+ case 'pcm-f64': return wave;
1336
+ case 'pcm-f64be': return wave;
1337
+ }
1338
+ } else {
1339
+ switch (codec) {
1340
+ case 'pcm-s16': return pcmC;
1341
+ case 'pcm-s16be': return pcmC;
1342
+ case 'pcm-s24': return pcmC;
1343
+ case 'pcm-s24be': return pcmC;
1344
+ case 'pcm-s32': return pcmC;
1345
+ case 'pcm-s32be': return pcmC;
1346
+ case 'pcm-f32': return pcmC;
1347
+ case 'pcm-f32be': return pcmC;
1348
+ case 'pcm-f64': return pcmC;
1349
+ case 'pcm-f64be': return pcmC;
1350
+ }
1351
+ }
1352
+
1353
+ return null;
1354
+ };
1355
+
1356
+ const SUBTITLE_CODEC_TO_BOX_NAME: Record<SubtitleCodec, string> = {
1357
+ webvtt: 'wvtt',
1358
+ };
1359
+
1360
+ const SUBTITLE_CODEC_TO_CONFIGURATION_BOX: Record<
1361
+ SubtitleCodec,
1362
+ (trackData: IsobmffSubtitleTrackData) => Box | null
1363
+ > = {
1364
+ webvtt: vttC,
1365
+ };