@kenzuya/mediabunny 1.26.0 → 1.28.6

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