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