@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.
- package/README.md +1 -1
- package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21390
- package/dist/bundles/mediabunny.min.js +490 -0
- package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-demuxer.d.ts +6 -6
- package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-muxer.d.ts +4 -4
- package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-reader.d.ts +1 -1
- package/dist/modules/src/adts/adts-reader.d.ts.map +1 -1
- package/dist/modules/src/avi/avi-demuxer.d.ts +44 -0
- package/dist/modules/src/avi/avi-demuxer.d.ts.map +1 -0
- package/dist/modules/src/avi/avi-misc.d.ts +88 -0
- package/dist/modules/src/avi/avi-misc.d.ts.map +1 -0
- package/dist/modules/src/avi/avi-muxer.d.ts +45 -0
- package/dist/modules/src/avi/avi-muxer.d.ts.map +1 -0
- package/dist/modules/src/avi/riff-writer.d.ts +26 -0
- package/dist/modules/src/avi/riff-writer.d.ts.map +1 -0
- package/dist/modules/src/codec-data.d.ts +8 -3
- package/dist/modules/src/codec-data.d.ts.map +1 -1
- package/dist/modules/src/codec.d.ts +10 -10
- package/dist/modules/src/codec.d.ts.map +1 -1
- package/dist/modules/src/conversion.d.ts +33 -16
- package/dist/modules/src/conversion.d.ts.map +1 -1
- package/dist/modules/src/custom-coder.d.ts +8 -8
- package/dist/modules/src/custom-coder.d.ts.map +1 -1
- package/dist/modules/src/demuxer.d.ts +3 -3
- package/dist/modules/src/demuxer.d.ts.map +1 -1
- package/dist/modules/src/encode.d.ts +8 -8
- package/dist/modules/src/encode.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-demuxer.d.ts +7 -7
- package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-misc.d.ts +3 -3
- package/dist/modules/src/flac/flac-misc.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-muxer.d.ts +5 -5
- package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
- package/dist/modules/src/id3.d.ts +3 -3
- package/dist/modules/src/id3.d.ts.map +1 -1
- package/dist/modules/src/index.d.ts +20 -20
- package/dist/modules/src/index.d.ts.map +1 -1
- package/dist/modules/src/input-format.d.ts +22 -0
- package/dist/modules/src/input-format.d.ts.map +1 -1
- package/dist/modules/src/input-track.d.ts +8 -8
- package/dist/modules/src/input-track.d.ts.map +1 -1
- package/dist/modules/src/input.d.ts +12 -12
- package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -2
- package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +12 -12
- package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-misc.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-muxer.d.ts +11 -11
- package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-reader.d.ts +2 -2
- package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
- package/dist/modules/src/matroska/ebml.d.ts +3 -3
- package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-demuxer.d.ts +13 -13
- package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-input.d.ts +33 -0
- package/dist/modules/src/matroska/matroska-input.d.ts.map +1 -0
- package/dist/modules/src/matroska/matroska-misc.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -5
- package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
- package/dist/modules/src/media-sink.d.ts +5 -5
- package/dist/modules/src/media-sink.d.ts.map +1 -1
- package/dist/modules/src/media-source.d.ts +22 -4
- package/dist/modules/src/media-source.d.ts.map +1 -1
- package/dist/modules/src/metadata.d.ts +2 -2
- package/dist/modules/src/metadata.d.ts.map +1 -1
- package/dist/modules/src/misc.d.ts +5 -4
- package/dist/modules/src/misc.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-demuxer.d.ts +7 -7
- package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-muxer.d.ts +4 -4
- package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-reader.d.ts +2 -2
- package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-writer.d.ts +1 -1
- package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
- package/dist/modules/src/muxer.d.ts +4 -4
- package/dist/modules/src/muxer.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-demuxer.d.ts +7 -7
- package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-misc.d.ts +1 -1
- package/dist/modules/src/ogg/ogg-misc.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-muxer.d.ts +5 -5
- package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-reader.d.ts +1 -1
- package/dist/modules/src/ogg/ogg-reader.d.ts.map +1 -1
- package/dist/modules/src/output-format.d.ts +51 -6
- package/dist/modules/src/output-format.d.ts.map +1 -1
- package/dist/modules/src/output.d.ts +13 -13
- package/dist/modules/src/output.d.ts.map +1 -1
- package/dist/modules/src/packet.d.ts +1 -1
- package/dist/modules/src/packet.d.ts.map +1 -1
- package/dist/modules/src/pcm.d.ts.map +1 -1
- package/dist/modules/src/reader.d.ts +2 -2
- package/dist/modules/src/reader.d.ts.map +1 -1
- package/dist/modules/src/sample.d.ts +57 -15
- package/dist/modules/src/sample.d.ts.map +1 -1
- package/dist/modules/src/source.d.ts +3 -3
- package/dist/modules/src/source.d.ts.map +1 -1
- package/dist/modules/src/subtitles.d.ts +1 -1
- package/dist/modules/src/subtitles.d.ts.map +1 -1
- package/dist/modules/src/target.d.ts +2 -2
- package/dist/modules/src/target.d.ts.map +1 -1
- package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
- package/dist/modules/src/wave/riff-writer.d.ts +1 -1
- package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
- package/dist/modules/src/wave/wave-demuxer.d.ts +6 -6
- package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
- package/dist/modules/src/wave/wave-muxer.d.ts +4 -4
- package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
- package/dist/modules/src/writer.d.ts +1 -1
- package/dist/modules/src/writer.d.ts.map +1 -1
- package/dist/packages/eac3/eac3.wasm +0 -0
- package/dist/packages/eac3/mediabunny-eac3.js +1058 -0
- package/dist/packages/eac3/mediabunny-eac3.min.js +44 -0
- package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.js +694 -0
- package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.min.js +58 -0
- package/dist/packages/mpeg4/mediabunny-mpeg4.js +1198 -0
- package/dist/packages/mpeg4/mediabunny-mpeg4.min.js +44 -0
- package/dist/packages/mpeg4/xvid.wasm +0 -0
- package/package.json +18 -57
- package/dist/bundles/mediabunny.cjs +0 -26140
- package/dist/bundles/mediabunny.min.cjs +0 -147
- package/dist/bundles/mediabunny.min.mjs +0 -146
- package/dist/mediabunny.d.ts +0 -3319
- package/dist/modules/shared/mp3-misc.js +0 -147
- package/dist/modules/src/adts/adts-demuxer.js +0 -239
- package/dist/modules/src/adts/adts-muxer.js +0 -80
- package/dist/modules/src/adts/adts-reader.js +0 -63
- package/dist/modules/src/codec-data.js +0 -1730
- package/dist/modules/src/codec.js +0 -869
- package/dist/modules/src/conversion.js +0 -1459
- package/dist/modules/src/custom-coder.js +0 -117
- package/dist/modules/src/demuxer.js +0 -12
- package/dist/modules/src/encode.js +0 -442
- package/dist/modules/src/flac/flac-demuxer.js +0 -504
- package/dist/modules/src/flac/flac-misc.js +0 -135
- package/dist/modules/src/flac/flac-muxer.js +0 -222
- package/dist/modules/src/id3.js +0 -848
- package/dist/modules/src/index.js +0 -28
- package/dist/modules/src/input-format.js +0 -480
- package/dist/modules/src/input-track.js +0 -372
- package/dist/modules/src/input.js +0 -188
- package/dist/modules/src/isobmff/isobmff-boxes.js +0 -1480
- package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -2618
- package/dist/modules/src/isobmff/isobmff-misc.js +0 -20
- package/dist/modules/src/isobmff/isobmff-muxer.js +0 -966
- package/dist/modules/src/isobmff/isobmff-reader.js +0 -72
- package/dist/modules/src/matroska/ebml.js +0 -653
- package/dist/modules/src/matroska/matroska-demuxer.js +0 -2133
- package/dist/modules/src/matroska/matroska-misc.js +0 -20
- package/dist/modules/src/matroska/matroska-muxer.js +0 -1017
- package/dist/modules/src/media-sink.js +0 -1736
- package/dist/modules/src/media-source.js +0 -1825
- package/dist/modules/src/metadata.js +0 -193
- package/dist/modules/src/misc.js +0 -623
- package/dist/modules/src/mp3/mp3-demuxer.js +0 -285
- package/dist/modules/src/mp3/mp3-muxer.js +0 -123
- package/dist/modules/src/mp3/mp3-reader.js +0 -26
- package/dist/modules/src/mp3/mp3-writer.js +0 -78
- package/dist/modules/src/muxer.js +0 -50
- package/dist/modules/src/node.d.ts +0 -9
- package/dist/modules/src/node.d.ts.map +0 -1
- package/dist/modules/src/node.js +0 -9
- package/dist/modules/src/ogg/ogg-demuxer.js +0 -763
- package/dist/modules/src/ogg/ogg-misc.js +0 -78
- package/dist/modules/src/ogg/ogg-muxer.js +0 -353
- package/dist/modules/src/ogg/ogg-reader.js +0 -65
- package/dist/modules/src/output-format.js +0 -527
- package/dist/modules/src/output.js +0 -300
- package/dist/modules/src/packet.js +0 -182
- package/dist/modules/src/pcm.js +0 -85
- package/dist/modules/src/reader.js +0 -236
- package/dist/modules/src/sample.js +0 -1056
- package/dist/modules/src/source.js +0 -1182
- package/dist/modules/src/subtitles.js +0 -575
- package/dist/modules/src/target.js +0 -140
- package/dist/modules/src/wave/riff-writer.js +0 -30
- package/dist/modules/src/wave/wave-demuxer.js +0 -447
- package/dist/modules/src/wave/wave-muxer.js +0 -318
- package/dist/modules/src/writer.js +0 -370
- package/src/adts/adts-demuxer.ts +0 -331
- package/src/adts/adts-muxer.ts +0 -111
- package/src/adts/adts-reader.ts +0 -85
- package/src/codec-data.ts +0 -2078
- package/src/codec.ts +0 -1092
- package/src/conversion.ts +0 -2112
- package/src/custom-coder.ts +0 -197
- package/src/demuxer.ts +0 -24
- package/src/encode.ts +0 -739
- package/src/flac/flac-demuxer.ts +0 -730
- package/src/flac/flac-misc.ts +0 -164
- package/src/flac/flac-muxer.ts +0 -320
- package/src/id3.ts +0 -925
- package/src/index.ts +0 -221
- package/src/input-format.ts +0 -541
- package/src/input-track.ts +0 -529
- package/src/input.ts +0 -235
- package/src/isobmff/isobmff-boxes.ts +0 -1719
- package/src/isobmff/isobmff-demuxer.ts +0 -3190
- package/src/isobmff/isobmff-misc.ts +0 -29
- package/src/isobmff/isobmff-muxer.ts +0 -1348
- package/src/isobmff/isobmff-reader.ts +0 -91
- package/src/matroska/ebml.ts +0 -730
- package/src/matroska/matroska-demuxer.ts +0 -2481
- package/src/matroska/matroska-misc.ts +0 -29
- package/src/matroska/matroska-muxer.ts +0 -1276
- package/src/media-sink.ts +0 -2179
- package/src/media-source.ts +0 -2243
- package/src/metadata.ts +0 -320
- package/src/misc.ts +0 -798
- package/src/mp3/mp3-demuxer.ts +0 -383
- package/src/mp3/mp3-muxer.ts +0 -166
- package/src/mp3/mp3-reader.ts +0 -34
- package/src/mp3/mp3-writer.ts +0 -120
- package/src/muxer.ts +0 -88
- package/src/node.ts +0 -11
- package/src/ogg/ogg-demuxer.ts +0 -1053
- package/src/ogg/ogg-misc.ts +0 -116
- package/src/ogg/ogg-muxer.ts +0 -497
- package/src/ogg/ogg-reader.ts +0 -93
- package/src/output-format.ts +0 -945
- package/src/output.ts +0 -488
- package/src/packet.ts +0 -263
- package/src/pcm.ts +0 -112
- package/src/reader.ts +0 -323
- package/src/sample.ts +0 -1461
- package/src/source.ts +0 -1688
- package/src/subtitles.ts +0 -711
- package/src/target.ts +0 -204
- package/src/tsconfig.json +0 -16
- package/src/wave/riff-writer.ts +0 -36
- package/src/wave/wave-demuxer.ts +0 -529
- package/src/wave/wave-muxer.ts +0 -371
- 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
|
-
};
|