@kenzuya/mediabunny 1.26.0 → 1.28.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21388
- 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/node.d.ts +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.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,3190 +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
|
-
AacCodecInfo,
|
|
11
|
-
AudioCodec,
|
|
12
|
-
extractAudioCodecString,
|
|
13
|
-
extractVideoCodecString,
|
|
14
|
-
MediaCodec,
|
|
15
|
-
OPUS_SAMPLE_RATE,
|
|
16
|
-
parseAacAudioSpecificConfig,
|
|
17
|
-
parsePcmCodec,
|
|
18
|
-
PCM_AUDIO_CODECS,
|
|
19
|
-
PcmAudioCodec,
|
|
20
|
-
SubtitleCodec,
|
|
21
|
-
VideoCodec,
|
|
22
|
-
} from '../codec';
|
|
23
|
-
import {
|
|
24
|
-
Av1CodecInfo,
|
|
25
|
-
AvcDecoderConfigurationRecord,
|
|
26
|
-
extractAv1CodecInfoFromPacket,
|
|
27
|
-
extractVp9CodecInfoFromPacket,
|
|
28
|
-
FlacBlockType,
|
|
29
|
-
HevcDecoderConfigurationRecord,
|
|
30
|
-
Vp9CodecInfo,
|
|
31
|
-
} from '../codec-data';
|
|
32
|
-
import { Demuxer } from '../demuxer';
|
|
33
|
-
import { Input } from '../input';
|
|
34
|
-
import {
|
|
35
|
-
InputAudioTrack,
|
|
36
|
-
InputAudioTrackBacking,
|
|
37
|
-
InputSubtitleTrack,
|
|
38
|
-
InputSubtitleTrackBacking,
|
|
39
|
-
InputTrack,
|
|
40
|
-
InputTrackBacking,
|
|
41
|
-
InputVideoTrack,
|
|
42
|
-
InputVideoTrackBacking,
|
|
43
|
-
} from '../input-track';
|
|
44
|
-
import { PacketRetrievalOptions } from '../media-sink';
|
|
45
|
-
import {
|
|
46
|
-
assert,
|
|
47
|
-
binarySearchExact,
|
|
48
|
-
binarySearchLessOrEqual,
|
|
49
|
-
Bitstream,
|
|
50
|
-
COLOR_PRIMARIES_MAP_INVERSE,
|
|
51
|
-
findLastIndex,
|
|
52
|
-
isIso639Dash2LanguageCode,
|
|
53
|
-
last,
|
|
54
|
-
MATRIX_COEFFICIENTS_MAP_INVERSE,
|
|
55
|
-
normalizeRotation,
|
|
56
|
-
roundToMultiple,
|
|
57
|
-
Rotation,
|
|
58
|
-
textDecoder,
|
|
59
|
-
TransformationMatrix,
|
|
60
|
-
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
|
61
|
-
UNDETERMINED_LANGUAGE,
|
|
62
|
-
toDataView,
|
|
63
|
-
roundIfAlmostInteger,
|
|
64
|
-
} from '../misc';
|
|
65
|
-
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
|
66
|
-
import { SubtitleCue } from '../subtitles';
|
|
67
|
-
import { buildIsobmffMimeType } from './isobmff-misc';
|
|
68
|
-
import {
|
|
69
|
-
MAX_BOX_HEADER_SIZE,
|
|
70
|
-
MIN_BOX_HEADER_SIZE,
|
|
71
|
-
readBoxHeader,
|
|
72
|
-
readDataBox,
|
|
73
|
-
readFixed_16_16,
|
|
74
|
-
readFixed_2_30,
|
|
75
|
-
readIsomVariableInteger,
|
|
76
|
-
readMetadataStringShort,
|
|
77
|
-
} from './isobmff-reader';
|
|
78
|
-
import {
|
|
79
|
-
FileSlice,
|
|
80
|
-
readBytes,
|
|
81
|
-
readF64Be,
|
|
82
|
-
readI16Be,
|
|
83
|
-
readI32Be,
|
|
84
|
-
readI64Be,
|
|
85
|
-
Reader,
|
|
86
|
-
readU16Be,
|
|
87
|
-
readU24Be,
|
|
88
|
-
readU32Be,
|
|
89
|
-
readU64Be,
|
|
90
|
-
readU8,
|
|
91
|
-
readAscii,
|
|
92
|
-
} from '../reader';
|
|
93
|
-
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
|
|
94
|
-
|
|
95
|
-
type InternalTrack = {
|
|
96
|
-
id: number;
|
|
97
|
-
demuxer: IsobmffDemuxer;
|
|
98
|
-
inputTrack: InputTrack | null;
|
|
99
|
-
disposition: TrackDisposition;
|
|
100
|
-
timescale: number;
|
|
101
|
-
durationInMovieTimescale: number;
|
|
102
|
-
durationInMediaTimescale: number;
|
|
103
|
-
rotation: Rotation;
|
|
104
|
-
internalCodecId: string | null;
|
|
105
|
-
name: string | null;
|
|
106
|
-
languageCode: string;
|
|
107
|
-
sampleTableByteOffset: number;
|
|
108
|
-
sampleTable: SampleTable | null;
|
|
109
|
-
fragmentLookupTable: FragmentLookupTableEntry[];
|
|
110
|
-
currentFragmentState: FragmentTrackState | null;
|
|
111
|
-
/**
|
|
112
|
-
* List of all encountered fragment offsets alongside their timestamps. This list never gets truncated, but memory
|
|
113
|
-
* consumption should be negligible.
|
|
114
|
-
*/
|
|
115
|
-
fragmentPositionCache: {
|
|
116
|
-
moofOffset: number;
|
|
117
|
-
startTimestamp: number;
|
|
118
|
-
endTimestamp: number;
|
|
119
|
-
}[];
|
|
120
|
-
/** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */
|
|
121
|
-
editListPreviousSegmentDurations: number;
|
|
122
|
-
/** The media time offset of the main edit list entry (with media time !== -1) */
|
|
123
|
-
editListOffset: number;
|
|
124
|
-
} & ({
|
|
125
|
-
info: null;
|
|
126
|
-
} | {
|
|
127
|
-
info: {
|
|
128
|
-
type: 'video';
|
|
129
|
-
width: number;
|
|
130
|
-
height: number;
|
|
131
|
-
codec: VideoCodec | null;
|
|
132
|
-
codecDescription: Uint8Array | null;
|
|
133
|
-
colorSpace: VideoColorSpaceInit | null;
|
|
134
|
-
avcType: 1 | 3 | null;
|
|
135
|
-
avcCodecInfo: AvcDecoderConfigurationRecord | null;
|
|
136
|
-
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
|
|
137
|
-
vp9CodecInfo: Vp9CodecInfo | null;
|
|
138
|
-
av1CodecInfo: Av1CodecInfo | null;
|
|
139
|
-
};
|
|
140
|
-
} | {
|
|
141
|
-
info: {
|
|
142
|
-
type: 'audio';
|
|
143
|
-
numberOfChannels: number;
|
|
144
|
-
sampleRate: number;
|
|
145
|
-
codec: AudioCodec | null;
|
|
146
|
-
codecDescription: Uint8Array | null;
|
|
147
|
-
aacCodecInfo: AacCodecInfo | null;
|
|
148
|
-
};
|
|
149
|
-
} | {
|
|
150
|
-
info: {
|
|
151
|
-
type: 'subtitle';
|
|
152
|
-
codec: SubtitleCodec | null;
|
|
153
|
-
codecPrivateText: string | null;
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
type InternalVideoTrack = InternalTrack & { info: { type: 'video' } };
|
|
158
|
-
type InternalAudioTrack = InternalTrack & { info: { type: 'audio' } };
|
|
159
|
-
type InternalSubtitleTrack = InternalTrack & { info: { type: 'subtitle' } };
|
|
160
|
-
|
|
161
|
-
type SampleTable = {
|
|
162
|
-
sampleTimingEntries: SampleTimingEntry[];
|
|
163
|
-
sampleCompositionTimeOffsets: SampleCompositionTimeOffsetEntry[];
|
|
164
|
-
sampleSizes: number[];
|
|
165
|
-
keySampleIndices: number[] | null; // Samples that are keyframes
|
|
166
|
-
chunkOffsets: number[];
|
|
167
|
-
sampleToChunk: SampleToChunkEntry[];
|
|
168
|
-
presentationTimestamps: {
|
|
169
|
-
presentationTimestamp: number;
|
|
170
|
-
sampleIndex: number;
|
|
171
|
-
}[] | null;
|
|
172
|
-
/**
|
|
173
|
-
* Provides a fast map from sample index to index in the sorted presentation timestamps array - so, a fast map from
|
|
174
|
-
* decode order to presentation order.
|
|
175
|
-
*/
|
|
176
|
-
presentationTimestampIndexMap: number[] | null;
|
|
177
|
-
};
|
|
178
|
-
type SampleTimingEntry = {
|
|
179
|
-
startIndex: number;
|
|
180
|
-
startDecodeTimestamp: number;
|
|
181
|
-
count: number;
|
|
182
|
-
delta: number;
|
|
183
|
-
};
|
|
184
|
-
type SampleCompositionTimeOffsetEntry = {
|
|
185
|
-
startIndex: number;
|
|
186
|
-
count: number;
|
|
187
|
-
offset: number;
|
|
188
|
-
};
|
|
189
|
-
type SampleToChunkEntry = {
|
|
190
|
-
startSampleIndex: number;
|
|
191
|
-
startChunkIndex: number;
|
|
192
|
-
samplesPerChunk: number;
|
|
193
|
-
sampleDescriptionIndex: number;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
type FragmentTrackDefaults = {
|
|
197
|
-
trackId: number;
|
|
198
|
-
defaultSampleDescriptionIndex: number;
|
|
199
|
-
defaultSampleDuration: number;
|
|
200
|
-
defaultSampleSize: number;
|
|
201
|
-
defaultSampleFlags: number;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
type FragmentLookupTableEntry = {
|
|
205
|
-
timestamp: number;
|
|
206
|
-
moofOffset: number;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
type FragmentTrackState = {
|
|
210
|
-
baseDataOffset: number;
|
|
211
|
-
sampleDescriptionIndex: number | null;
|
|
212
|
-
defaultSampleDuration: number | null;
|
|
213
|
-
defaultSampleSize: number | null;
|
|
214
|
-
defaultSampleFlags: number | null;
|
|
215
|
-
startTimestamp: number | null;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
type FragmentTrackData = {
|
|
219
|
-
track: InternalTrack;
|
|
220
|
-
startTimestamp: number;
|
|
221
|
-
endTimestamp: number;
|
|
222
|
-
firstKeyFrameTimestamp: number | null;
|
|
223
|
-
samples: FragmentTrackSample[];
|
|
224
|
-
presentationTimestamps: {
|
|
225
|
-
presentationTimestamp: number;
|
|
226
|
-
sampleIndex: number;
|
|
227
|
-
}[];
|
|
228
|
-
startTimestampIsFinal: boolean;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
type FragmentTrackSample = {
|
|
232
|
-
presentationTimestamp: number;
|
|
233
|
-
duration: number;
|
|
234
|
-
byteOffset: number;
|
|
235
|
-
byteSize: number;
|
|
236
|
-
isKeyFrame: boolean;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
type Fragment = {
|
|
240
|
-
moofOffset: number;
|
|
241
|
-
moofSize: number;
|
|
242
|
-
implicitBaseDataOffset: number;
|
|
243
|
-
trackData: Map<InternalTrack['id'], FragmentTrackData>;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
export class IsobmffDemuxer extends Demuxer {
|
|
247
|
-
reader: Reader;
|
|
248
|
-
moovSlice: FileSlice | null = null;
|
|
249
|
-
|
|
250
|
-
currentTrack: InternalTrack | null = null;
|
|
251
|
-
tracks: InternalTrack[] = [];
|
|
252
|
-
metadataPromise: Promise<void> | null = null;
|
|
253
|
-
movieTimescale = -1;
|
|
254
|
-
movieDurationInTimescale = -1;
|
|
255
|
-
isQuickTime = false;
|
|
256
|
-
metadataTags: MetadataTags = {};
|
|
257
|
-
currentMetadataKeys: Map<number, string> | null = null;
|
|
258
|
-
|
|
259
|
-
isFragmented = false;
|
|
260
|
-
fragmentTrackDefaults: FragmentTrackDefaults[] = [];
|
|
261
|
-
currentFragment: Fragment | null = null;
|
|
262
|
-
/**
|
|
263
|
-
* Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the
|
|
264
|
-
* same fragment in quick succession.
|
|
265
|
-
*/
|
|
266
|
-
lastReadFragment: Fragment | null = null;
|
|
267
|
-
|
|
268
|
-
constructor(input: Input) {
|
|
269
|
-
super(input);
|
|
270
|
-
|
|
271
|
-
this.reader = input._reader;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
override async computeDuration() {
|
|
275
|
-
const tracks = await this.getTracks();
|
|
276
|
-
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
|
|
277
|
-
return Math.max(0, ...trackDurations);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
override async getTracks() {
|
|
281
|
-
await this.readMetadata();
|
|
282
|
-
return this.tracks.map(track => track.inputTrack!);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
override async getMimeType() {
|
|
286
|
-
await this.readMetadata();
|
|
287
|
-
|
|
288
|
-
const codecStrings = await Promise.all(this.tracks.map(x => x.inputTrack!.getCodecParameterString()));
|
|
289
|
-
|
|
290
|
-
return buildIsobmffMimeType({
|
|
291
|
-
isQuickTime: this.isQuickTime,
|
|
292
|
-
hasVideo: this.tracks.some(x => x.info?.type === 'video'),
|
|
293
|
-
hasAudio: this.tracks.some(x => x.info?.type === 'audio'),
|
|
294
|
-
codecStrings: codecStrings.filter(Boolean) as string[],
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async getMetadataTags() {
|
|
299
|
-
await this.readMetadata();
|
|
300
|
-
return this.metadataTags;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
readMetadata() {
|
|
304
|
-
return this.metadataPromise ??= (async () => {
|
|
305
|
-
let currentPos = 0;
|
|
306
|
-
while (true) {
|
|
307
|
-
let slice = this.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
|
|
308
|
-
if (slice instanceof Promise) slice = await slice;
|
|
309
|
-
if (!slice) break;
|
|
310
|
-
|
|
311
|
-
const startPos = currentPos;
|
|
312
|
-
const boxInfo = readBoxHeader(slice);
|
|
313
|
-
if (!boxInfo) {
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (boxInfo.name === 'ftyp') {
|
|
318
|
-
const majorBrand = readAscii(slice, 4);
|
|
319
|
-
this.isQuickTime = majorBrand === 'qt ';
|
|
320
|
-
} else if (boxInfo.name === 'moov') {
|
|
321
|
-
// Found moov, load it
|
|
322
|
-
|
|
323
|
-
let moovSlice = this.reader.requestSlice(slice.filePos, boxInfo.contentSize);
|
|
324
|
-
if (moovSlice instanceof Promise) moovSlice = await moovSlice;
|
|
325
|
-
if (!moovSlice) break;
|
|
326
|
-
|
|
327
|
-
this.moovSlice = moovSlice;
|
|
328
|
-
this.readContiguousBoxes(this.moovSlice);
|
|
329
|
-
|
|
330
|
-
// Put default tracks first
|
|
331
|
-
this.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
|
|
332
|
-
|
|
333
|
-
for (const track of this.tracks) {
|
|
334
|
-
// Modify the edit list offset based on the previous segment durations. They are in different
|
|
335
|
-
// timescales, so we first convert to seconds and then into the track timescale.
|
|
336
|
-
const previousSegmentDurationsInSeconds
|
|
337
|
-
= track.editListPreviousSegmentDurations / this.movieTimescale;
|
|
338
|
-
track.editListOffset -= Math.round(previousSegmentDurationsInSeconds * track.timescale);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
currentPos = startPos + boxInfo.totalSize;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (this.isFragmented && this.reader.fileSize !== null) {
|
|
348
|
-
// The last 4 bytes may contain the size of the mfra box at the end of the file
|
|
349
|
-
let lastWordSlice = this.reader.requestSlice(this.reader.fileSize - 4, 4);
|
|
350
|
-
if (lastWordSlice instanceof Promise) lastWordSlice = await lastWordSlice;
|
|
351
|
-
assert(lastWordSlice);
|
|
352
|
-
|
|
353
|
-
const lastWord = readU32Be(lastWordSlice);
|
|
354
|
-
const potentialMfraPos = this.reader.fileSize - lastWord;
|
|
355
|
-
|
|
356
|
-
if (potentialMfraPos >= 0 && potentialMfraPos <= this.reader.fileSize - MAX_BOX_HEADER_SIZE) {
|
|
357
|
-
let mfraHeaderSlice = this.reader.requestSliceRange(
|
|
358
|
-
potentialMfraPos,
|
|
359
|
-
MIN_BOX_HEADER_SIZE,
|
|
360
|
-
MAX_BOX_HEADER_SIZE,
|
|
361
|
-
);
|
|
362
|
-
if (mfraHeaderSlice instanceof Promise) mfraHeaderSlice = await mfraHeaderSlice;
|
|
363
|
-
|
|
364
|
-
if (mfraHeaderSlice) {
|
|
365
|
-
const boxInfo = readBoxHeader(mfraHeaderSlice);
|
|
366
|
-
|
|
367
|
-
if (boxInfo && boxInfo.name === 'mfra') {
|
|
368
|
-
// We found the mfra box, allowing for much better random access. Let's parse it.
|
|
369
|
-
let mfraSlice = this.reader.requestSlice(mfraHeaderSlice.filePos, boxInfo.contentSize);
|
|
370
|
-
if (mfraSlice instanceof Promise) mfraSlice = await mfraSlice;
|
|
371
|
-
|
|
372
|
-
if (mfraSlice) {
|
|
373
|
-
this.readContiguousBoxes(mfraSlice);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
})();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
getSampleTableForTrack(internalTrack: InternalTrack) {
|
|
383
|
-
if (internalTrack.sampleTable) {
|
|
384
|
-
return internalTrack.sampleTable;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const sampleTable: SampleTable = {
|
|
388
|
-
sampleTimingEntries: [],
|
|
389
|
-
sampleCompositionTimeOffsets: [],
|
|
390
|
-
sampleSizes: [],
|
|
391
|
-
keySampleIndices: null,
|
|
392
|
-
chunkOffsets: [],
|
|
393
|
-
sampleToChunk: [],
|
|
394
|
-
presentationTimestamps: null,
|
|
395
|
-
presentationTimestampIndexMap: null,
|
|
396
|
-
};
|
|
397
|
-
internalTrack.sampleTable = sampleTable;
|
|
398
|
-
|
|
399
|
-
assert(this.moovSlice);
|
|
400
|
-
const stblContainerSlice = this.moovSlice.slice(internalTrack.sampleTableByteOffset);
|
|
401
|
-
|
|
402
|
-
this.currentTrack = internalTrack;
|
|
403
|
-
this.traverseBox(stblContainerSlice);
|
|
404
|
-
this.currentTrack = null;
|
|
405
|
-
|
|
406
|
-
const isPcmCodec = internalTrack.info?.type === 'audio'
|
|
407
|
-
&& internalTrack.info.codec
|
|
408
|
-
&& (PCM_AUDIO_CODECS as readonly string[]).includes(internalTrack.info.codec);
|
|
409
|
-
|
|
410
|
-
if (isPcmCodec && sampleTable.sampleCompositionTimeOffsets.length === 0) {
|
|
411
|
-
// If the audio has PCM samples, the way the samples are defined in the sample table is somewhat
|
|
412
|
-
// suboptimal: Each individual audio sample is its own sample, meaning we can have 48000 samples per second.
|
|
413
|
-
// Because we treat each sample as its own atomic unit that can be decoded, this would lead to a huge
|
|
414
|
-
// amount of very short samples for PCM audio. So instead, we make a transformation: If the audio is in PCM,
|
|
415
|
-
// we say that each chunk (that normally holds many samples) now is one big sample. We can this because
|
|
416
|
-
// the samples in the chunk are contiguous and the format is PCM, so the entire chunk as one thing still
|
|
417
|
-
// encodes valid audio information.
|
|
418
|
-
|
|
419
|
-
assert(internalTrack.info?.type === 'audio');
|
|
420
|
-
const pcmInfo = parsePcmCodec(internalTrack.info.codec as PcmAudioCodec);
|
|
421
|
-
|
|
422
|
-
const newSampleTimingEntries: SampleTimingEntry[] = [];
|
|
423
|
-
const newSampleSizes: number[] = [];
|
|
424
|
-
|
|
425
|
-
for (let i = 0; i < sampleTable.sampleToChunk.length; i++) {
|
|
426
|
-
const chunkEntry = sampleTable.sampleToChunk[i]!;
|
|
427
|
-
const nextEntry = sampleTable.sampleToChunk[i + 1];
|
|
428
|
-
const chunkCount = (nextEntry ? nextEntry.startChunkIndex : sampleTable.chunkOffsets.length)
|
|
429
|
-
- chunkEntry.startChunkIndex;
|
|
430
|
-
|
|
431
|
-
for (let j = 0; j < chunkCount; j++) {
|
|
432
|
-
const startSampleIndex = chunkEntry.startSampleIndex + j * chunkEntry.samplesPerChunk;
|
|
433
|
-
const endSampleIndex = startSampleIndex + chunkEntry.samplesPerChunk; // Exclusive, outside of chunk
|
|
434
|
-
|
|
435
|
-
const startTimingEntryIndex = binarySearchLessOrEqual(
|
|
436
|
-
sampleTable.sampleTimingEntries,
|
|
437
|
-
startSampleIndex,
|
|
438
|
-
x => x.startIndex,
|
|
439
|
-
);
|
|
440
|
-
const startTimingEntry = sampleTable.sampleTimingEntries[startTimingEntryIndex]!;
|
|
441
|
-
const endTimingEntryIndex = binarySearchLessOrEqual(
|
|
442
|
-
sampleTable.sampleTimingEntries,
|
|
443
|
-
endSampleIndex,
|
|
444
|
-
x => x.startIndex,
|
|
445
|
-
);
|
|
446
|
-
const endTimingEntry = sampleTable.sampleTimingEntries[endTimingEntryIndex]!;
|
|
447
|
-
|
|
448
|
-
const firstSampleTimestamp = startTimingEntry.startDecodeTimestamp
|
|
449
|
-
+ (startSampleIndex - startTimingEntry.startIndex) * startTimingEntry.delta;
|
|
450
|
-
const lastSampleTimestamp = endTimingEntry.startDecodeTimestamp
|
|
451
|
-
+ (endSampleIndex - endTimingEntry.startIndex) * endTimingEntry.delta;
|
|
452
|
-
const delta = lastSampleTimestamp - firstSampleTimestamp;
|
|
453
|
-
|
|
454
|
-
const lastSampleTimingEntry = last(newSampleTimingEntries);
|
|
455
|
-
if (lastSampleTimingEntry && lastSampleTimingEntry.delta === delta) {
|
|
456
|
-
lastSampleTimingEntry.count++;
|
|
457
|
-
} else {
|
|
458
|
-
// One sample for the entire chunk
|
|
459
|
-
newSampleTimingEntries.push({
|
|
460
|
-
startIndex: chunkEntry.startChunkIndex + j,
|
|
461
|
-
startDecodeTimestamp: firstSampleTimestamp,
|
|
462
|
-
count: 1,
|
|
463
|
-
delta,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Instead of determining the chunk's size by looping over the samples sizes in the sample table, we
|
|
468
|
-
// can directly compute it as we know how many PCM frames are in this chunk, and the size of each
|
|
469
|
-
// PCM frame. This also improves compatibility with some files which fail to write proper sample
|
|
470
|
-
// size values into their sample tables in the PCM case.
|
|
471
|
-
const chunkSize = chunkEntry.samplesPerChunk
|
|
472
|
-
* pcmInfo.sampleSize
|
|
473
|
-
* internalTrack.info.numberOfChannels;
|
|
474
|
-
|
|
475
|
-
newSampleSizes.push(chunkSize);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
chunkEntry.startSampleIndex = chunkEntry.startChunkIndex;
|
|
479
|
-
chunkEntry.samplesPerChunk = 1;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
sampleTable.sampleTimingEntries = newSampleTimingEntries;
|
|
483
|
-
sampleTable.sampleSizes = newSampleSizes;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (sampleTable.sampleCompositionTimeOffsets.length > 0) {
|
|
487
|
-
// If composition time offsets are defined, we must build a list of all presentation timestamps and then
|
|
488
|
-
// sort them
|
|
489
|
-
sampleTable.presentationTimestamps = [];
|
|
490
|
-
|
|
491
|
-
for (const entry of sampleTable.sampleTimingEntries) {
|
|
492
|
-
for (let i = 0; i < entry.count; i++) {
|
|
493
|
-
sampleTable.presentationTimestamps.push({
|
|
494
|
-
presentationTimestamp: entry.startDecodeTimestamp + i * entry.delta,
|
|
495
|
-
sampleIndex: entry.startIndex + i,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
for (const entry of sampleTable.sampleCompositionTimeOffsets) {
|
|
501
|
-
for (let i = 0; i < entry.count; i++) {
|
|
502
|
-
const sampleIndex = entry.startIndex + i;
|
|
503
|
-
const sample = sampleTable.presentationTimestamps[sampleIndex];
|
|
504
|
-
if (!sample) {
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
sample.presentationTimestamp += entry.offset;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
sampleTable.presentationTimestamps.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
|
|
513
|
-
|
|
514
|
-
sampleTable.presentationTimestampIndexMap = Array(sampleTable.presentationTimestamps.length).fill(-1);
|
|
515
|
-
for (let i = 0; i < sampleTable.presentationTimestamps.length; i++) {
|
|
516
|
-
sampleTable.presentationTimestampIndexMap[sampleTable.presentationTimestamps[i]!.sampleIndex] = i;
|
|
517
|
-
}
|
|
518
|
-
} else {
|
|
519
|
-
// If they're not defined, we can simply use the decode timestamps as presentation timestamps
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return sampleTable;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async readFragment(startPos: number): Promise<Fragment> {
|
|
526
|
-
if (this.lastReadFragment?.moofOffset === startPos) {
|
|
527
|
-
return this.lastReadFragment;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
let headerSlice = this.reader.requestSliceRange(startPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
|
|
531
|
-
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
|
532
|
-
assert(headerSlice);
|
|
533
|
-
|
|
534
|
-
const moofBoxInfo = readBoxHeader(headerSlice);
|
|
535
|
-
assert(moofBoxInfo?.name === 'moof');
|
|
536
|
-
|
|
537
|
-
let entireSlice = this.reader.requestSlice(startPos, moofBoxInfo.totalSize);
|
|
538
|
-
if (entireSlice instanceof Promise) entireSlice = await entireSlice;
|
|
539
|
-
assert(entireSlice);
|
|
540
|
-
|
|
541
|
-
this.traverseBox(entireSlice);
|
|
542
|
-
|
|
543
|
-
const fragment = this.lastReadFragment;
|
|
544
|
-
assert(fragment && fragment.moofOffset === startPos);
|
|
545
|
-
|
|
546
|
-
for (const [, trackData] of fragment.trackData) {
|
|
547
|
-
const track = trackData.track;
|
|
548
|
-
const { fragmentPositionCache } = track;
|
|
549
|
-
|
|
550
|
-
if (!trackData.startTimestampIsFinal) {
|
|
551
|
-
// It may be that some tracks don't define the base decode time, i.e. when the fragment begins. This
|
|
552
|
-
// we'll need to figure out the start timestamp another way. We'll compute the timestamp by accessing
|
|
553
|
-
// the lookup entries and fragment cache, which works out nicely with the lookup algorithm: If these
|
|
554
|
-
// exist, then the lookup will automatically start at the furthest possible point. If they don't, the
|
|
555
|
-
// lookup starts sequentially from the start, incrementally summing up all fragment durations. It's sort
|
|
556
|
-
// of implicit, but it ends up working nicely.
|
|
557
|
-
|
|
558
|
-
const lookupEntry = track.fragmentLookupTable.find(x => x.moofOffset === fragment.moofOffset);
|
|
559
|
-
if (lookupEntry) {
|
|
560
|
-
// There's a lookup entry, let's use its timestamp
|
|
561
|
-
offsetFragmentTrackDataByTimestamp(trackData, lookupEntry.timestamp);
|
|
562
|
-
} else {
|
|
563
|
-
const lastCacheIndex = binarySearchLessOrEqual(
|
|
564
|
-
fragmentPositionCache,
|
|
565
|
-
fragment.moofOffset - 1,
|
|
566
|
-
x => x.moofOffset,
|
|
567
|
-
);
|
|
568
|
-
if (lastCacheIndex !== -1) {
|
|
569
|
-
// Let's use the timestamp of the previous fragment in the cache
|
|
570
|
-
const lastCache = fragmentPositionCache[lastCacheIndex]!;
|
|
571
|
-
offsetFragmentTrackDataByTimestamp(trackData, lastCache.endTimestamp);
|
|
572
|
-
} else {
|
|
573
|
-
// We're the first fragment I guess, "offset by 0"
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
trackData.startTimestampIsFinal = true;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Let's remember that a fragment with a given timestamp is here, speeding up future lookups if no
|
|
581
|
-
// lookup table exists
|
|
582
|
-
const insertionIndex = binarySearchLessOrEqual(
|
|
583
|
-
fragmentPositionCache,
|
|
584
|
-
trackData.startTimestamp,
|
|
585
|
-
x => x.startTimestamp,
|
|
586
|
-
);
|
|
587
|
-
if (
|
|
588
|
-
insertionIndex === -1
|
|
589
|
-
|| fragmentPositionCache[insertionIndex]!.moofOffset !== fragment.moofOffset
|
|
590
|
-
) {
|
|
591
|
-
fragmentPositionCache.splice(insertionIndex + 1, 0, {
|
|
592
|
-
moofOffset: fragment.moofOffset,
|
|
593
|
-
startTimestamp: trackData.startTimestamp,
|
|
594
|
-
endTimestamp: trackData.endTimestamp,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return fragment;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
readContiguousBoxes(slice: FileSlice) {
|
|
603
|
-
const startIndex = slice.filePos;
|
|
604
|
-
|
|
605
|
-
while (slice.filePos - startIndex <= slice.length - MIN_BOX_HEADER_SIZE) {
|
|
606
|
-
const foundBox = this.traverseBox(slice);
|
|
607
|
-
|
|
608
|
-
if (!foundBox) {
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// eslint-disable-next-line @stylistic/generator-star-spacing
|
|
615
|
-
*iterateContiguousBoxes(slice: FileSlice) {
|
|
616
|
-
const startIndex = slice.filePos;
|
|
617
|
-
|
|
618
|
-
while (slice.filePos - startIndex <= slice.length - MIN_BOX_HEADER_SIZE) {
|
|
619
|
-
const startPos = slice.filePos;
|
|
620
|
-
const boxInfo = readBoxHeader(slice);
|
|
621
|
-
if (!boxInfo) {
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
yield { boxInfo, slice };
|
|
626
|
-
slice.filePos = startPos + boxInfo.totalSize;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
traverseBox(slice: FileSlice): boolean {
|
|
631
|
-
const startPos = slice.filePos;
|
|
632
|
-
const boxInfo = readBoxHeader(slice);
|
|
633
|
-
if (!boxInfo) {
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const contentStartPos = slice.filePos;
|
|
638
|
-
const boxEndPos = startPos + boxInfo.totalSize;
|
|
639
|
-
|
|
640
|
-
switch (boxInfo.name) {
|
|
641
|
-
case 'mdia':
|
|
642
|
-
case 'minf':
|
|
643
|
-
case 'dinf':
|
|
644
|
-
case 'mfra':
|
|
645
|
-
case 'edts': {
|
|
646
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
647
|
-
}; break;
|
|
648
|
-
|
|
649
|
-
case 'mvhd': {
|
|
650
|
-
const version = readU8(slice);
|
|
651
|
-
slice.skip(3); // Flags
|
|
652
|
-
|
|
653
|
-
if (version === 1) {
|
|
654
|
-
slice.skip(8 + 8);
|
|
655
|
-
this.movieTimescale = readU32Be(slice);
|
|
656
|
-
this.movieDurationInTimescale = readU64Be(slice);
|
|
657
|
-
} else {
|
|
658
|
-
slice.skip(4 + 4);
|
|
659
|
-
this.movieTimescale = readU32Be(slice);
|
|
660
|
-
this.movieDurationInTimescale = readU32Be(slice);
|
|
661
|
-
}
|
|
662
|
-
}; break;
|
|
663
|
-
|
|
664
|
-
case 'trak': {
|
|
665
|
-
const track = {
|
|
666
|
-
id: -1,
|
|
667
|
-
demuxer: this,
|
|
668
|
-
inputTrack: null,
|
|
669
|
-
disposition: {
|
|
670
|
-
...DEFAULT_TRACK_DISPOSITION,
|
|
671
|
-
},
|
|
672
|
-
info: null,
|
|
673
|
-
timescale: -1,
|
|
674
|
-
durationInMovieTimescale: -1,
|
|
675
|
-
durationInMediaTimescale: -1,
|
|
676
|
-
rotation: 0,
|
|
677
|
-
internalCodecId: null,
|
|
678
|
-
name: null,
|
|
679
|
-
languageCode: UNDETERMINED_LANGUAGE,
|
|
680
|
-
sampleTableByteOffset: -1,
|
|
681
|
-
sampleTable: null,
|
|
682
|
-
fragmentLookupTable: [],
|
|
683
|
-
currentFragmentState: null,
|
|
684
|
-
fragmentPositionCache: [],
|
|
685
|
-
editListPreviousSegmentDurations: 0,
|
|
686
|
-
editListOffset: 0,
|
|
687
|
-
} satisfies InternalTrack as InternalTrack;
|
|
688
|
-
this.currentTrack = track;
|
|
689
|
-
|
|
690
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
691
|
-
|
|
692
|
-
if (track.id !== -1 && track.timescale !== -1 && track.info !== null) {
|
|
693
|
-
if (track.info.type === 'video' && track.info.width !== -1) {
|
|
694
|
-
const videoTrack = track as InternalVideoTrack;
|
|
695
|
-
track.inputTrack = new InputVideoTrack(this.input, new IsobmffVideoTrackBacking(videoTrack));
|
|
696
|
-
this.tracks.push(track);
|
|
697
|
-
} else if (track.info.type === 'audio' && track.info.numberOfChannels !== -1) {
|
|
698
|
-
const audioTrack = track as InternalAudioTrack;
|
|
699
|
-
track.inputTrack = new InputAudioTrack(this.input, new IsobmffAudioTrackBacking(audioTrack));
|
|
700
|
-
this.tracks.push(track);
|
|
701
|
-
} else if (track.info.type === 'subtitle') {
|
|
702
|
-
const subtitleTrack = track as InternalSubtitleTrack;
|
|
703
|
-
track.inputTrack = new InputSubtitleTrack(this.input, new IsobmffSubtitleTrackBacking(subtitleTrack));
|
|
704
|
-
this.tracks.push(track);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
this.currentTrack = null;
|
|
709
|
-
}; break;
|
|
710
|
-
|
|
711
|
-
case 'tkhd': {
|
|
712
|
-
const track = this.currentTrack;
|
|
713
|
-
if (!track) {
|
|
714
|
-
break;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const version = readU8(slice);
|
|
718
|
-
const flags = readU24Be(slice);
|
|
719
|
-
|
|
720
|
-
// Spec says disabled tracks are to be treated like they don't exist, but in practice, they are treated
|
|
721
|
-
// more like non-default tracks.
|
|
722
|
-
const trackEnabled = !!(flags & 0x1);
|
|
723
|
-
track.disposition.default = trackEnabled;
|
|
724
|
-
|
|
725
|
-
// Skip over creation & modification time to reach the track ID
|
|
726
|
-
if (version === 0) {
|
|
727
|
-
slice.skip(8);
|
|
728
|
-
track.id = readU32Be(slice);
|
|
729
|
-
slice.skip(4);
|
|
730
|
-
track.durationInMovieTimescale = readU32Be(slice);
|
|
731
|
-
} else if (version === 1) {
|
|
732
|
-
slice.skip(16);
|
|
733
|
-
track.id = readU32Be(slice);
|
|
734
|
-
slice.skip(4);
|
|
735
|
-
track.durationInMovieTimescale = readU64Be(slice);
|
|
736
|
-
} else {
|
|
737
|
-
throw new Error(`Incorrect track header version ${version}.`);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
slice.skip(2 * 4 + 2 + 2 + 2 + 2);
|
|
741
|
-
const matrix: TransformationMatrix = [
|
|
742
|
-
readFixed_16_16(slice),
|
|
743
|
-
readFixed_16_16(slice),
|
|
744
|
-
readFixed_2_30(slice),
|
|
745
|
-
readFixed_16_16(slice),
|
|
746
|
-
readFixed_16_16(slice),
|
|
747
|
-
readFixed_2_30(slice),
|
|
748
|
-
readFixed_16_16(slice),
|
|
749
|
-
readFixed_16_16(slice),
|
|
750
|
-
readFixed_2_30(slice),
|
|
751
|
-
];
|
|
752
|
-
|
|
753
|
-
const rotation = normalizeRotation(roundToMultiple(extractRotationFromMatrix(matrix), 90));
|
|
754
|
-
assert(rotation === 0 || rotation === 90 || rotation === 180 || rotation === 270);
|
|
755
|
-
|
|
756
|
-
track.rotation = rotation;
|
|
757
|
-
}; break;
|
|
758
|
-
|
|
759
|
-
case 'elst': {
|
|
760
|
-
const track = this.currentTrack;
|
|
761
|
-
if (!track) {
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const version = readU8(slice);
|
|
766
|
-
slice.skip(3); // Flags
|
|
767
|
-
|
|
768
|
-
let relevantEntryFound = false;
|
|
769
|
-
let previousSegmentDurations = 0;
|
|
770
|
-
|
|
771
|
-
const entryCount = readU32Be(slice);
|
|
772
|
-
for (let i = 0; i < entryCount; i++) {
|
|
773
|
-
const segmentDuration = version === 1
|
|
774
|
-
? readU64Be(slice)
|
|
775
|
-
: readU32Be(slice);
|
|
776
|
-
const mediaTime = version === 1
|
|
777
|
-
? readI64Be(slice)
|
|
778
|
-
: readI32Be(slice);
|
|
779
|
-
const mediaRate = readFixed_16_16(slice);
|
|
780
|
-
|
|
781
|
-
if (segmentDuration === 0) {
|
|
782
|
-
// Don't care
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (relevantEntryFound) {
|
|
787
|
-
console.warn(
|
|
788
|
-
'Unsupported edit list: multiple edits are not currently supported. Only using first edit.',
|
|
789
|
-
);
|
|
790
|
-
break;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (mediaTime === -1) {
|
|
794
|
-
previousSegmentDurations += segmentDuration;
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (mediaRate !== 1) {
|
|
799
|
-
console.warn('Unsupported edit list entry: media rate must be 1.');
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
track.editListPreviousSegmentDurations = previousSegmentDurations;
|
|
804
|
-
track.editListOffset = mediaTime;
|
|
805
|
-
relevantEntryFound = true;
|
|
806
|
-
}
|
|
807
|
-
}; break;
|
|
808
|
-
|
|
809
|
-
case 'mdhd': {
|
|
810
|
-
const track = this.currentTrack;
|
|
811
|
-
if (!track) {
|
|
812
|
-
break;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const version = readU8(slice);
|
|
816
|
-
slice.skip(3); // Flags
|
|
817
|
-
|
|
818
|
-
if (version === 0) {
|
|
819
|
-
slice.skip(8);
|
|
820
|
-
track.timescale = readU32Be(slice);
|
|
821
|
-
track.durationInMediaTimescale = readU32Be(slice);
|
|
822
|
-
} else if (version === 1) {
|
|
823
|
-
slice.skip(16);
|
|
824
|
-
track.timescale = readU32Be(slice);
|
|
825
|
-
track.durationInMediaTimescale = readU64Be(slice);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
let language = readU16Be(slice);
|
|
829
|
-
|
|
830
|
-
if (language > 0) {
|
|
831
|
-
track.languageCode = '';
|
|
832
|
-
|
|
833
|
-
for (let i = 0; i < 3; i++) {
|
|
834
|
-
track.languageCode = String.fromCharCode(0x60 + (language & 0b11111)) + track.languageCode;
|
|
835
|
-
language >>= 5;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (!isIso639Dash2LanguageCode(track.languageCode)) {
|
|
839
|
-
// Sometimes the bytes are garbage
|
|
840
|
-
track.languageCode = UNDETERMINED_LANGUAGE;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}; break;
|
|
844
|
-
|
|
845
|
-
case 'hdlr': {
|
|
846
|
-
const track = this.currentTrack;
|
|
847
|
-
if (!track) {
|
|
848
|
-
break;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
slice.skip(8); // Version + flags + pre-defined
|
|
852
|
-
const handlerType = readAscii(slice, 4);
|
|
853
|
-
|
|
854
|
-
if (handlerType === 'vide') {
|
|
855
|
-
track.info = {
|
|
856
|
-
type: 'video',
|
|
857
|
-
width: -1,
|
|
858
|
-
height: -1,
|
|
859
|
-
codec: null,
|
|
860
|
-
codecDescription: null,
|
|
861
|
-
colorSpace: null,
|
|
862
|
-
avcType: null,
|
|
863
|
-
avcCodecInfo: null,
|
|
864
|
-
hevcCodecInfo: null,
|
|
865
|
-
vp9CodecInfo: null,
|
|
866
|
-
av1CodecInfo: null,
|
|
867
|
-
};
|
|
868
|
-
} else if (handlerType === 'soun') {
|
|
869
|
-
track.info = {
|
|
870
|
-
type: 'audio',
|
|
871
|
-
numberOfChannels: -1,
|
|
872
|
-
sampleRate: -1,
|
|
873
|
-
codec: null,
|
|
874
|
-
codecDescription: null,
|
|
875
|
-
aacCodecInfo: null,
|
|
876
|
-
};
|
|
877
|
-
} else if (handlerType === 'text' || handlerType === 'subt' || handlerType === 'sbtl') {
|
|
878
|
-
track.info = {
|
|
879
|
-
type: 'subtitle',
|
|
880
|
-
codec: null,
|
|
881
|
-
codecPrivateText: null,
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
}; break;
|
|
885
|
-
|
|
886
|
-
case 'stbl': {
|
|
887
|
-
const track = this.currentTrack;
|
|
888
|
-
if (!track) {
|
|
889
|
-
break;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
track.sampleTableByteOffset = startPos;
|
|
893
|
-
|
|
894
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
895
|
-
}; break;
|
|
896
|
-
|
|
897
|
-
case 'stsd': {
|
|
898
|
-
const track = this.currentTrack;
|
|
899
|
-
if (!track) {
|
|
900
|
-
break;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (track.info === null || track.sampleTable) {
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const stsdVersion = readU8(slice);
|
|
908
|
-
slice.skip(3); // Flags
|
|
909
|
-
|
|
910
|
-
const entries = readU32Be(slice);
|
|
911
|
-
|
|
912
|
-
for (let i = 0; i < entries; i++) {
|
|
913
|
-
const sampleBoxStartPos = slice.filePos;
|
|
914
|
-
const sampleBoxInfo = readBoxHeader(slice);
|
|
915
|
-
if (!sampleBoxInfo) {
|
|
916
|
-
break;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
track.internalCodecId = sampleBoxInfo.name;
|
|
920
|
-
const lowercaseBoxName = sampleBoxInfo.name.toLowerCase();
|
|
921
|
-
|
|
922
|
-
if (track.info.type === 'video') {
|
|
923
|
-
if (lowercaseBoxName === 'avc1' || lowercaseBoxName === 'avc3') {
|
|
924
|
-
track.info.codec = 'avc';
|
|
925
|
-
track.info.avcType = lowercaseBoxName === 'avc1' ? 1 : 3;
|
|
926
|
-
} else if (lowercaseBoxName === 'hvc1' || lowercaseBoxName === 'hev1') {
|
|
927
|
-
track.info.codec = 'hevc';
|
|
928
|
-
} else if (lowercaseBoxName === 'vp08') {
|
|
929
|
-
track.info.codec = 'vp8';
|
|
930
|
-
} else if (lowercaseBoxName === 'vp09') {
|
|
931
|
-
track.info.codec = 'vp9';
|
|
932
|
-
} else if (lowercaseBoxName === 'av01') {
|
|
933
|
-
track.info.codec = 'av1';
|
|
934
|
-
} else {
|
|
935
|
-
console.warn(`Unsupported video codec (sample entry type '${sampleBoxInfo.name}').`);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
slice.skip(6 * 1 + 2 + 2 + 2 + 3 * 4);
|
|
939
|
-
|
|
940
|
-
track.info.width = readU16Be(slice);
|
|
941
|
-
track.info.height = readU16Be(slice);
|
|
942
|
-
|
|
943
|
-
slice.skip(4 + 4 + 4 + 2 + 32 + 2 + 2);
|
|
944
|
-
|
|
945
|
-
this.readContiguousBoxes(
|
|
946
|
-
slice.slice(
|
|
947
|
-
slice.filePos,
|
|
948
|
-
(sampleBoxStartPos + sampleBoxInfo.totalSize) - slice.filePos,
|
|
949
|
-
),
|
|
950
|
-
);
|
|
951
|
-
} else if (track.info.type === 'subtitle') {
|
|
952
|
-
// Parse subtitle sample entries
|
|
953
|
-
slice.skip(6); // Reserved
|
|
954
|
-
const dataReferenceIndex = readU16Be(slice);
|
|
955
|
-
|
|
956
|
-
// Detect subtitle codec based on sample entry box type
|
|
957
|
-
if (lowercaseBoxName === 'wvtt') {
|
|
958
|
-
track.info.codec = 'webvtt';
|
|
959
|
-
} else if (lowercaseBoxName === 'tx3g' || lowercaseBoxName === 'text') {
|
|
960
|
-
// 3GPP Timed Text
|
|
961
|
-
track.info.codec = 'tx3g';
|
|
962
|
-
} else if (lowercaseBoxName === 'stpp') {
|
|
963
|
-
// TTML/IMSC subtitles
|
|
964
|
-
track.info.codec = 'ttml';
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
this.readContiguousBoxes(
|
|
968
|
-
slice.slice(
|
|
969
|
-
slice.filePos,
|
|
970
|
-
(sampleBoxStartPos + sampleBoxInfo.totalSize) - slice.filePos,
|
|
971
|
-
),
|
|
972
|
-
);
|
|
973
|
-
} else {
|
|
974
|
-
if (lowercaseBoxName === 'mp4a') {
|
|
975
|
-
// We don't know the codec yet (might be AAC, might be MP3), need to read the esds box
|
|
976
|
-
} else if (lowercaseBoxName === 'opus') {
|
|
977
|
-
track.info.codec = 'opus';
|
|
978
|
-
} else if (lowercaseBoxName === 'flac') {
|
|
979
|
-
track.info.codec = 'flac';
|
|
980
|
-
} else if (
|
|
981
|
-
lowercaseBoxName === 'twos'
|
|
982
|
-
|| lowercaseBoxName === 'sowt'
|
|
983
|
-
|| lowercaseBoxName === 'raw '
|
|
984
|
-
|| lowercaseBoxName === 'in24'
|
|
985
|
-
|| lowercaseBoxName === 'in32'
|
|
986
|
-
|| lowercaseBoxName === 'fl32'
|
|
987
|
-
|| lowercaseBoxName === 'fl64'
|
|
988
|
-
|| lowercaseBoxName === 'lpcm'
|
|
989
|
-
|| lowercaseBoxName === 'ipcm' // ISO/IEC 23003-5
|
|
990
|
-
|| lowercaseBoxName === 'fpcm' // "
|
|
991
|
-
) {
|
|
992
|
-
// It's PCM
|
|
993
|
-
// developer.apple.com/documentation/quicktime-file-format/sound_sample_descriptions/
|
|
994
|
-
} else if (lowercaseBoxName === 'ulaw') {
|
|
995
|
-
track.info.codec = 'ulaw';
|
|
996
|
-
} else if (lowercaseBoxName === 'alaw') {
|
|
997
|
-
track.info.codec = 'alaw';
|
|
998
|
-
} else {
|
|
999
|
-
console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
slice.skip(6 * 1 + 2);
|
|
1003
|
-
|
|
1004
|
-
const version = readU16Be(slice);
|
|
1005
|
-
slice.skip(3 * 2);
|
|
1006
|
-
|
|
1007
|
-
let channelCount = readU16Be(slice);
|
|
1008
|
-
let sampleSize = readU16Be(slice);
|
|
1009
|
-
|
|
1010
|
-
slice.skip(2 * 2);
|
|
1011
|
-
|
|
1012
|
-
// Can't use fixed16_16 as that's signed
|
|
1013
|
-
let sampleRate = readU32Be(slice) / 0x10000;
|
|
1014
|
-
|
|
1015
|
-
if (stsdVersion === 0 && version > 0) {
|
|
1016
|
-
// Additional QuickTime fields
|
|
1017
|
-
if (version === 1) {
|
|
1018
|
-
slice.skip(4);
|
|
1019
|
-
sampleSize = 8 * readU32Be(slice);
|
|
1020
|
-
slice.skip(2 * 4);
|
|
1021
|
-
} else if (version === 2) {
|
|
1022
|
-
slice.skip(4);
|
|
1023
|
-
sampleRate = readF64Be(slice);
|
|
1024
|
-
channelCount = readU32Be(slice);
|
|
1025
|
-
slice.skip(4); // Always 0x7f000000
|
|
1026
|
-
|
|
1027
|
-
sampleSize = readU32Be(slice);
|
|
1028
|
-
|
|
1029
|
-
const flags = readU32Be(slice);
|
|
1030
|
-
|
|
1031
|
-
slice.skip(2 * 4);
|
|
1032
|
-
|
|
1033
|
-
if (lowercaseBoxName === 'lpcm') {
|
|
1034
|
-
const bytesPerSample = (sampleSize + 7) >> 3;
|
|
1035
|
-
const isFloat = Boolean(flags & 1);
|
|
1036
|
-
const isBigEndian = Boolean(flags & 2);
|
|
1037
|
-
const sFlags = flags & 4 ? -1 : 0; // I guess it means "signed flags" or something?
|
|
1038
|
-
|
|
1039
|
-
if (sampleSize > 0 && sampleSize <= 64) {
|
|
1040
|
-
if (isFloat) {
|
|
1041
|
-
if (sampleSize === 32) {
|
|
1042
|
-
track.info.codec = isBigEndian ? 'pcm-f32be' : 'pcm-f32';
|
|
1043
|
-
}
|
|
1044
|
-
} else {
|
|
1045
|
-
if (sFlags & (1 << (bytesPerSample - 1))) {
|
|
1046
|
-
if (bytesPerSample === 1) {
|
|
1047
|
-
track.info.codec = 'pcm-s8';
|
|
1048
|
-
} else if (bytesPerSample === 2) {
|
|
1049
|
-
track.info.codec = isBigEndian ? 'pcm-s16be' : 'pcm-s16';
|
|
1050
|
-
} else if (bytesPerSample === 3) {
|
|
1051
|
-
track.info.codec = isBigEndian ? 'pcm-s24be' : 'pcm-s24';
|
|
1052
|
-
} else if (bytesPerSample === 4) {
|
|
1053
|
-
track.info.codec = isBigEndian ? 'pcm-s32be' : 'pcm-s32';
|
|
1054
|
-
}
|
|
1055
|
-
} else {
|
|
1056
|
-
if (bytesPerSample === 1) {
|
|
1057
|
-
track.info.codec = 'pcm-u8';
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (track.info.codec === null) {
|
|
1064
|
-
console.warn('Unsupported PCM format.');
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
if (track.info.codec === 'opus') {
|
|
1071
|
-
sampleRate = OPUS_SAMPLE_RATE; // Always the same
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
track.info.numberOfChannels = channelCount;
|
|
1075
|
-
track.info.sampleRate = sampleRate;
|
|
1076
|
-
|
|
1077
|
-
// PCM codec assignments
|
|
1078
|
-
if (lowercaseBoxName === 'twos') {
|
|
1079
|
-
if (sampleSize === 8) {
|
|
1080
|
-
track.info.codec = 'pcm-s8';
|
|
1081
|
-
} else if (sampleSize === 16) {
|
|
1082
|
-
track.info.codec = 'pcm-s16be';
|
|
1083
|
-
} else {
|
|
1084
|
-
console.warn(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
|
1085
|
-
track.info.codec = null;
|
|
1086
|
-
}
|
|
1087
|
-
} else if (lowercaseBoxName === 'sowt') {
|
|
1088
|
-
if (sampleSize === 8) {
|
|
1089
|
-
track.info.codec = 'pcm-s8';
|
|
1090
|
-
} else if (sampleSize === 16) {
|
|
1091
|
-
track.info.codec = 'pcm-s16';
|
|
1092
|
-
} else {
|
|
1093
|
-
console.warn(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
|
1094
|
-
track.info.codec = null;
|
|
1095
|
-
}
|
|
1096
|
-
} else if (lowercaseBoxName === 'raw ') {
|
|
1097
|
-
track.info.codec = 'pcm-u8';
|
|
1098
|
-
} else if (lowercaseBoxName === 'in24') {
|
|
1099
|
-
track.info.codec = 'pcm-s24be';
|
|
1100
|
-
} else if (lowercaseBoxName === 'in32') {
|
|
1101
|
-
track.info.codec = 'pcm-s32be';
|
|
1102
|
-
} else if (lowercaseBoxName === 'fl32') {
|
|
1103
|
-
track.info.codec = 'pcm-f32be';
|
|
1104
|
-
} else if (lowercaseBoxName === 'fl64') {
|
|
1105
|
-
track.info.codec = 'pcm-f64be';
|
|
1106
|
-
} else if (lowercaseBoxName === 'ipcm') {
|
|
1107
|
-
track.info.codec = 'pcm-s16be'; // Placeholder, will be adjusted by the pcmC box
|
|
1108
|
-
} else if (lowercaseBoxName === 'fpcm') {
|
|
1109
|
-
track.info.codec = 'pcm-f32be'; // Placeholder, will be adjusted by the pcmC box
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
this.readContiguousBoxes(
|
|
1113
|
-
slice.slice(
|
|
1114
|
-
slice.filePos,
|
|
1115
|
-
(sampleBoxStartPos + sampleBoxInfo.totalSize) - slice.filePos,
|
|
1116
|
-
),
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}; break;
|
|
1121
|
-
|
|
1122
|
-
case 'avcC': {
|
|
1123
|
-
const track = this.currentTrack;
|
|
1124
|
-
if (!track) {
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
assert(track.info);
|
|
1128
|
-
|
|
1129
|
-
if (track.info.type === 'video') {
|
|
1130
|
-
track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
|
|
1131
|
-
}
|
|
1132
|
-
}; break;
|
|
1133
|
-
|
|
1134
|
-
case 'hvcC': {
|
|
1135
|
-
const track = this.currentTrack;
|
|
1136
|
-
if (!track) {
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
assert(track.info);
|
|
1140
|
-
|
|
1141
|
-
if (track.info.type === 'video') {
|
|
1142
|
-
track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
|
|
1143
|
-
}
|
|
1144
|
-
}; break;
|
|
1145
|
-
|
|
1146
|
-
case 'vpcC': {
|
|
1147
|
-
const track = this.currentTrack;
|
|
1148
|
-
if (!track) {
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
assert(track.info?.type === 'video');
|
|
1152
|
-
|
|
1153
|
-
slice.skip(4); // Version + flags
|
|
1154
|
-
|
|
1155
|
-
const profile = readU8(slice);
|
|
1156
|
-
const level = readU8(slice);
|
|
1157
|
-
const thirdByte = readU8(slice);
|
|
1158
|
-
const bitDepth = thirdByte >> 4;
|
|
1159
|
-
const chromaSubsampling = (thirdByte >> 1) & 0b111;
|
|
1160
|
-
const videoFullRangeFlag = thirdByte & 1;
|
|
1161
|
-
const colourPrimaries = readU8(slice);
|
|
1162
|
-
const transferCharacteristics = readU8(slice);
|
|
1163
|
-
const matrixCoefficients = readU8(slice);
|
|
1164
|
-
|
|
1165
|
-
track.info.vp9CodecInfo = {
|
|
1166
|
-
profile,
|
|
1167
|
-
level,
|
|
1168
|
-
bitDepth,
|
|
1169
|
-
chromaSubsampling,
|
|
1170
|
-
videoFullRangeFlag,
|
|
1171
|
-
colourPrimaries,
|
|
1172
|
-
transferCharacteristics,
|
|
1173
|
-
matrixCoefficients,
|
|
1174
|
-
};
|
|
1175
|
-
}; break;
|
|
1176
|
-
|
|
1177
|
-
case 'av1C': {
|
|
1178
|
-
const track = this.currentTrack;
|
|
1179
|
-
if (!track) {
|
|
1180
|
-
break;
|
|
1181
|
-
}
|
|
1182
|
-
assert(track.info?.type === 'video');
|
|
1183
|
-
|
|
1184
|
-
slice.skip(1); // Marker + version
|
|
1185
|
-
|
|
1186
|
-
const secondByte = readU8(slice);
|
|
1187
|
-
const profile = secondByte >> 5;
|
|
1188
|
-
const level = secondByte & 0b11111;
|
|
1189
|
-
|
|
1190
|
-
const thirdByte = readU8(slice);
|
|
1191
|
-
const tier = thirdByte >> 7;
|
|
1192
|
-
const highBitDepth = (thirdByte >> 6) & 1;
|
|
1193
|
-
const twelveBit = (thirdByte >> 5) & 1;
|
|
1194
|
-
const monochrome = (thirdByte >> 4) & 1;
|
|
1195
|
-
const chromaSubsamplingX = (thirdByte >> 3) & 1;
|
|
1196
|
-
const chromaSubsamplingY = (thirdByte >> 2) & 1;
|
|
1197
|
-
const chromaSamplePosition = thirdByte & 0b11;
|
|
1198
|
-
|
|
1199
|
-
// Logic from https://aomediacodec.github.io/av1-spec/av1-spec.pdf
|
|
1200
|
-
const bitDepth = profile === 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8);
|
|
1201
|
-
|
|
1202
|
-
track.info.av1CodecInfo = {
|
|
1203
|
-
profile,
|
|
1204
|
-
level,
|
|
1205
|
-
tier,
|
|
1206
|
-
bitDepth,
|
|
1207
|
-
monochrome,
|
|
1208
|
-
chromaSubsamplingX,
|
|
1209
|
-
chromaSubsamplingY,
|
|
1210
|
-
chromaSamplePosition,
|
|
1211
|
-
};
|
|
1212
|
-
}; break;
|
|
1213
|
-
|
|
1214
|
-
case 'colr': {
|
|
1215
|
-
const track = this.currentTrack;
|
|
1216
|
-
if (!track) {
|
|
1217
|
-
break;
|
|
1218
|
-
}
|
|
1219
|
-
assert(track.info?.type === 'video');
|
|
1220
|
-
|
|
1221
|
-
const colourType = readAscii(slice, 4);
|
|
1222
|
-
if (colourType !== 'nclx') {
|
|
1223
|
-
break;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
const colourPrimaries = readU16Be(slice);
|
|
1227
|
-
const transferCharacteristics = readU16Be(slice);
|
|
1228
|
-
const matrixCoefficients = readU16Be(slice);
|
|
1229
|
-
const fullRangeFlag = Boolean(readU8(slice) & 0x80);
|
|
1230
|
-
|
|
1231
|
-
track.info.colorSpace = {
|
|
1232
|
-
primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries],
|
|
1233
|
-
transfer: TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics],
|
|
1234
|
-
matrix: MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients],
|
|
1235
|
-
fullRange: fullRangeFlag,
|
|
1236
|
-
} as VideoColorSpaceInit;
|
|
1237
|
-
}; break;
|
|
1238
|
-
|
|
1239
|
-
case 'wave': {
|
|
1240
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
1241
|
-
}; break;
|
|
1242
|
-
|
|
1243
|
-
case 'esds': {
|
|
1244
|
-
const track = this.currentTrack;
|
|
1245
|
-
if (!track) {
|
|
1246
|
-
break;
|
|
1247
|
-
}
|
|
1248
|
-
assert(track.info?.type === 'audio');
|
|
1249
|
-
|
|
1250
|
-
slice.skip(4); // Version + flags
|
|
1251
|
-
|
|
1252
|
-
const tag = readU8(slice);
|
|
1253
|
-
assert(tag === 0x03); // ES Descriptor
|
|
1254
|
-
|
|
1255
|
-
readIsomVariableInteger(slice); // Length
|
|
1256
|
-
|
|
1257
|
-
slice.skip(2); // ES ID
|
|
1258
|
-
const mixed = readU8(slice);
|
|
1259
|
-
|
|
1260
|
-
const streamDependenceFlag = (mixed & 0x80) !== 0;
|
|
1261
|
-
const urlFlag = (mixed & 0x40) !== 0;
|
|
1262
|
-
const ocrStreamFlag = (mixed & 0x20) !== 0;
|
|
1263
|
-
|
|
1264
|
-
if (streamDependenceFlag) {
|
|
1265
|
-
slice.skip(2);
|
|
1266
|
-
}
|
|
1267
|
-
if (urlFlag) {
|
|
1268
|
-
const urlLength = readU8(slice);
|
|
1269
|
-
slice.skip(urlLength);
|
|
1270
|
-
}
|
|
1271
|
-
if (ocrStreamFlag) {
|
|
1272
|
-
slice.skip(2);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const decoderConfigTag = readU8(slice);
|
|
1276
|
-
assert(decoderConfigTag === 0x04); // DecoderConfigDescriptor
|
|
1277
|
-
|
|
1278
|
-
const decoderConfigDescriptorLength = readIsomVariableInteger(slice); // Length
|
|
1279
|
-
|
|
1280
|
-
const payloadStart = slice.filePos;
|
|
1281
|
-
|
|
1282
|
-
const objectTypeIndication = readU8(slice);
|
|
1283
|
-
if (objectTypeIndication === 0x40 || objectTypeIndication === 0x67) {
|
|
1284
|
-
track.info.codec = 'aac';
|
|
1285
|
-
track.info.aacCodecInfo = { isMpeg2: objectTypeIndication === 0x67 };
|
|
1286
|
-
} else if (objectTypeIndication === 0x69 || objectTypeIndication === 0x6b) {
|
|
1287
|
-
track.info.codec = 'mp3';
|
|
1288
|
-
} else if (objectTypeIndication === 0xdd) {
|
|
1289
|
-
track.info.codec = 'vorbis'; // "nonstandard, gpac uses it" - FFmpeg
|
|
1290
|
-
} else {
|
|
1291
|
-
console.warn(
|
|
1292
|
-
`Unsupported audio codec (objectTypeIndication ${objectTypeIndication}) - discarding track.`,
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
slice.skip(1 + 3 + 4 + 4);
|
|
1297
|
-
|
|
1298
|
-
if (decoderConfigDescriptorLength > slice.filePos - payloadStart) {
|
|
1299
|
-
// There's a DecoderSpecificInfo at the end, let's read it
|
|
1300
|
-
|
|
1301
|
-
const decoderSpecificInfoTag = readU8(slice);
|
|
1302
|
-
assert(decoderSpecificInfoTag === 0x05); // DecoderSpecificInfo
|
|
1303
|
-
|
|
1304
|
-
const decoderSpecificInfoLength = readIsomVariableInteger(slice);
|
|
1305
|
-
track.info.codecDescription = readBytes(slice, decoderSpecificInfoLength);
|
|
1306
|
-
|
|
1307
|
-
if (track.info.codec === 'aac') {
|
|
1308
|
-
// Let's try to deduce more accurate values directly from the AudioSpecificConfig:
|
|
1309
|
-
const audioSpecificConfig = parseAacAudioSpecificConfig(track.info.codecDescription);
|
|
1310
|
-
if (audioSpecificConfig.numberOfChannels !== null) {
|
|
1311
|
-
track.info.numberOfChannels = audioSpecificConfig.numberOfChannels;
|
|
1312
|
-
}
|
|
1313
|
-
if (audioSpecificConfig.sampleRate !== null) {
|
|
1314
|
-
track.info.sampleRate = audioSpecificConfig.sampleRate;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}; break;
|
|
1319
|
-
|
|
1320
|
-
case 'enda': {
|
|
1321
|
-
const track = this.currentTrack;
|
|
1322
|
-
if (!track) {
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
|
-
assert(track.info?.type === 'audio');
|
|
1326
|
-
|
|
1327
|
-
const littleEndian = readU16Be(slice) & 0xff; // 0xff is from FFmpeg
|
|
1328
|
-
|
|
1329
|
-
if (littleEndian) {
|
|
1330
|
-
if (track.info.codec === 'pcm-s16be') {
|
|
1331
|
-
track.info.codec = 'pcm-s16';
|
|
1332
|
-
} else if (track.info.codec === 'pcm-s24be') {
|
|
1333
|
-
track.info.codec = 'pcm-s24';
|
|
1334
|
-
} else if (track.info.codec === 'pcm-s32be') {
|
|
1335
|
-
track.info.codec = 'pcm-s32';
|
|
1336
|
-
} else if (track.info.codec === 'pcm-f32be') {
|
|
1337
|
-
track.info.codec = 'pcm-f32';
|
|
1338
|
-
} else if (track.info.codec === 'pcm-f64be') {
|
|
1339
|
-
track.info.codec = 'pcm-f64';
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}; break;
|
|
1343
|
-
|
|
1344
|
-
case 'pcmC': {
|
|
1345
|
-
const track = this.currentTrack;
|
|
1346
|
-
if (!track) {
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
assert(track.info?.type === 'audio');
|
|
1350
|
-
|
|
1351
|
-
slice.skip(1 + 3); // Version + flags
|
|
1352
|
-
|
|
1353
|
-
// ISO/IEC 23003-5
|
|
1354
|
-
|
|
1355
|
-
const formatFlags = readU8(slice);
|
|
1356
|
-
const isLittleEndian = Boolean(formatFlags & 0x01);
|
|
1357
|
-
const pcmSampleSize = readU8(slice);
|
|
1358
|
-
|
|
1359
|
-
if (track.info.codec === 'pcm-s16be') {
|
|
1360
|
-
// ipcm
|
|
1361
|
-
|
|
1362
|
-
if (isLittleEndian) {
|
|
1363
|
-
if (pcmSampleSize === 16) {
|
|
1364
|
-
track.info.codec = 'pcm-s16';
|
|
1365
|
-
} else if (pcmSampleSize === 24) {
|
|
1366
|
-
track.info.codec = 'pcm-s24';
|
|
1367
|
-
} else if (pcmSampleSize === 32) {
|
|
1368
|
-
track.info.codec = 'pcm-s32';
|
|
1369
|
-
} else {
|
|
1370
|
-
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
|
1371
|
-
track.info.codec = null;
|
|
1372
|
-
}
|
|
1373
|
-
} else {
|
|
1374
|
-
if (pcmSampleSize === 16) {
|
|
1375
|
-
track.info.codec = 'pcm-s16be';
|
|
1376
|
-
} else if (pcmSampleSize === 24) {
|
|
1377
|
-
track.info.codec = 'pcm-s24be';
|
|
1378
|
-
} else if (pcmSampleSize === 32) {
|
|
1379
|
-
track.info.codec = 'pcm-s32be';
|
|
1380
|
-
} else {
|
|
1381
|
-
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
|
1382
|
-
track.info.codec = null;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
} else if (track.info.codec === 'pcm-f32be') {
|
|
1386
|
-
// fpcm
|
|
1387
|
-
|
|
1388
|
-
if (isLittleEndian) {
|
|
1389
|
-
if (pcmSampleSize === 32) {
|
|
1390
|
-
track.info.codec = 'pcm-f32';
|
|
1391
|
-
} else if (pcmSampleSize === 64) {
|
|
1392
|
-
track.info.codec = 'pcm-f64';
|
|
1393
|
-
} else {
|
|
1394
|
-
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
|
1395
|
-
track.info.codec = null;
|
|
1396
|
-
}
|
|
1397
|
-
} else {
|
|
1398
|
-
if (pcmSampleSize === 32) {
|
|
1399
|
-
track.info.codec = 'pcm-f32be';
|
|
1400
|
-
} else if (pcmSampleSize === 64) {
|
|
1401
|
-
track.info.codec = 'pcm-f64be';
|
|
1402
|
-
} else {
|
|
1403
|
-
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
|
1404
|
-
track.info.codec = null;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
break;
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
case 'dOps': { // Used for Opus audio
|
|
1413
|
-
const track = this.currentTrack;
|
|
1414
|
-
if (!track) {
|
|
1415
|
-
break;
|
|
1416
|
-
}
|
|
1417
|
-
assert(track.info?.type === 'audio');
|
|
1418
|
-
|
|
1419
|
-
slice.skip(1); // Version
|
|
1420
|
-
|
|
1421
|
-
// https://www.opus-codec.org/docs/opus_in_isobmff.html
|
|
1422
|
-
const outputChannelCount = readU8(slice);
|
|
1423
|
-
const preSkip = readU16Be(slice);
|
|
1424
|
-
const inputSampleRate = readU32Be(slice);
|
|
1425
|
-
const outputGain = readI16Be(slice);
|
|
1426
|
-
const channelMappingFamily = readU8(slice);
|
|
1427
|
-
|
|
1428
|
-
let channelMappingTable: Uint8Array;
|
|
1429
|
-
if (channelMappingFamily !== 0) {
|
|
1430
|
-
channelMappingTable = readBytes(slice, 2 + outputChannelCount);
|
|
1431
|
-
} else {
|
|
1432
|
-
channelMappingTable = new Uint8Array(0);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-codec-oggopus-06
|
|
1436
|
-
const description = new Uint8Array(8 + 1 + 1 + 2 + 4 + 2 + 1 + channelMappingTable.byteLength);
|
|
1437
|
-
const view = new DataView(description.buffer);
|
|
1438
|
-
view.setUint32(0, 0x4f707573, false); // 'Opus'
|
|
1439
|
-
view.setUint32(4, 0x48656164, false); // 'Head'
|
|
1440
|
-
view.setUint8(8, 1); // Version
|
|
1441
|
-
view.setUint8(9, outputChannelCount);
|
|
1442
|
-
view.setUint16(10, preSkip, true);
|
|
1443
|
-
view.setUint32(12, inputSampleRate, true);
|
|
1444
|
-
view.setInt16(16, outputGain, true);
|
|
1445
|
-
view.setUint8(18, channelMappingFamily);
|
|
1446
|
-
description.set(channelMappingTable, 19);
|
|
1447
|
-
|
|
1448
|
-
track.info.codecDescription = description;
|
|
1449
|
-
track.info.numberOfChannels = outputChannelCount;
|
|
1450
|
-
// Don't copy the input sample rate, irrelevant, and output sample rate is fixed
|
|
1451
|
-
}; break;
|
|
1452
|
-
|
|
1453
|
-
case 'dfLa': { // Used for FLAC audio
|
|
1454
|
-
const track = this.currentTrack;
|
|
1455
|
-
if (!track) {
|
|
1456
|
-
break;
|
|
1457
|
-
}
|
|
1458
|
-
assert(track.info?.type === 'audio');
|
|
1459
|
-
|
|
1460
|
-
slice.skip(4); // Version + flags
|
|
1461
|
-
|
|
1462
|
-
// https://datatracker.ietf.org/doc/rfc9639/
|
|
1463
|
-
|
|
1464
|
-
const BLOCK_TYPE_MASK = 0x7f;
|
|
1465
|
-
const LAST_METADATA_BLOCK_FLAG_MASK = 0x80;
|
|
1466
|
-
|
|
1467
|
-
const startPos = slice.filePos;
|
|
1468
|
-
|
|
1469
|
-
while (slice.filePos < boxEndPos) {
|
|
1470
|
-
const flagAndType = readU8(slice);
|
|
1471
|
-
const metadataBlockLength = readU24Be(slice);
|
|
1472
|
-
const type = flagAndType & BLOCK_TYPE_MASK;
|
|
1473
|
-
|
|
1474
|
-
// It's a STREAMINFO block; let's extract the actual sample rate and channel count
|
|
1475
|
-
if (type === FlacBlockType.STREAMINFO) {
|
|
1476
|
-
slice.skip(10);
|
|
1477
|
-
|
|
1478
|
-
// Extract sample rate and channel count
|
|
1479
|
-
const word = readU32Be(slice);
|
|
1480
|
-
const sampleRate = word >>> 12;
|
|
1481
|
-
const numberOfChannels = ((word >> 9) & 0b111) + 1;
|
|
1482
|
-
|
|
1483
|
-
track.info.sampleRate = sampleRate;
|
|
1484
|
-
track.info.numberOfChannels = numberOfChannels;
|
|
1485
|
-
|
|
1486
|
-
slice.skip(20);
|
|
1487
|
-
} else {
|
|
1488
|
-
// Simply skip ahead to the next block
|
|
1489
|
-
slice.skip(metadataBlockLength);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (flagAndType & LAST_METADATA_BLOCK_FLAG_MASK) {
|
|
1493
|
-
break;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
const endPos = slice.filePos;
|
|
1498
|
-
slice.filePos = startPos;
|
|
1499
|
-
const bytes = readBytes(slice, endPos - startPos);
|
|
1500
|
-
|
|
1501
|
-
const description = new Uint8Array(4 + bytes.byteLength);
|
|
1502
|
-
const view = new DataView(description.buffer);
|
|
1503
|
-
view.setUint32(0, 0x664c6143, false); // 'fLaC'
|
|
1504
|
-
description.set(bytes, 4);
|
|
1505
|
-
|
|
1506
|
-
// Set the codec description to be 'fLaC' + all metadata blocks
|
|
1507
|
-
track.info.codecDescription = description;
|
|
1508
|
-
}; break;
|
|
1509
|
-
|
|
1510
|
-
case 'stts': {
|
|
1511
|
-
const track = this.currentTrack;
|
|
1512
|
-
if (!track) {
|
|
1513
|
-
break;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
if (!track.sampleTable) {
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
slice.skip(4); // Version + flags
|
|
1521
|
-
|
|
1522
|
-
const entryCount = readU32Be(slice);
|
|
1523
|
-
|
|
1524
|
-
let currentIndex = 0;
|
|
1525
|
-
let currentTimestamp = 0;
|
|
1526
|
-
|
|
1527
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1528
|
-
const sampleCount = readU32Be(slice);
|
|
1529
|
-
const sampleDelta = readU32Be(slice);
|
|
1530
|
-
|
|
1531
|
-
track.sampleTable.sampleTimingEntries.push({
|
|
1532
|
-
startIndex: currentIndex,
|
|
1533
|
-
startDecodeTimestamp: currentTimestamp,
|
|
1534
|
-
count: sampleCount,
|
|
1535
|
-
delta: sampleDelta,
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
currentIndex += sampleCount;
|
|
1539
|
-
currentTimestamp += sampleCount * sampleDelta;
|
|
1540
|
-
}
|
|
1541
|
-
}; break;
|
|
1542
|
-
|
|
1543
|
-
case 'ctts': {
|
|
1544
|
-
const track = this.currentTrack;
|
|
1545
|
-
if (!track) {
|
|
1546
|
-
break;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
if (!track.sampleTable) {
|
|
1550
|
-
break;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
slice.skip(1 + 3); // Version + flags
|
|
1554
|
-
|
|
1555
|
-
const entryCount = readU32Be(slice);
|
|
1556
|
-
|
|
1557
|
-
let sampleIndex = 0;
|
|
1558
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1559
|
-
const sampleCount = readU32Be(slice);
|
|
1560
|
-
const sampleOffset = readI32Be(slice);
|
|
1561
|
-
|
|
1562
|
-
track.sampleTable.sampleCompositionTimeOffsets.push({
|
|
1563
|
-
startIndex: sampleIndex,
|
|
1564
|
-
count: sampleCount,
|
|
1565
|
-
offset: sampleOffset,
|
|
1566
|
-
});
|
|
1567
|
-
|
|
1568
|
-
sampleIndex += sampleCount;
|
|
1569
|
-
}
|
|
1570
|
-
}; break;
|
|
1571
|
-
|
|
1572
|
-
case 'stsz': {
|
|
1573
|
-
const track = this.currentTrack;
|
|
1574
|
-
if (!track) {
|
|
1575
|
-
break;
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
if (!track.sampleTable) {
|
|
1579
|
-
break;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
slice.skip(4); // Version + flags
|
|
1583
|
-
|
|
1584
|
-
const sampleSize = readU32Be(slice);
|
|
1585
|
-
const sampleCount = readU32Be(slice);
|
|
1586
|
-
|
|
1587
|
-
if (sampleSize === 0) {
|
|
1588
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
1589
|
-
const sampleSize = readU32Be(slice);
|
|
1590
|
-
track.sampleTable.sampleSizes.push(sampleSize);
|
|
1591
|
-
}
|
|
1592
|
-
} else {
|
|
1593
|
-
track.sampleTable.sampleSizes.push(sampleSize);
|
|
1594
|
-
}
|
|
1595
|
-
}; break;
|
|
1596
|
-
|
|
1597
|
-
case 'stz2': {
|
|
1598
|
-
const track = this.currentTrack;
|
|
1599
|
-
if (!track) {
|
|
1600
|
-
break;
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
if (!track.sampleTable) {
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
slice.skip(4); // Version + flags
|
|
1608
|
-
slice.skip(3); // Reserved
|
|
1609
|
-
|
|
1610
|
-
const fieldSize = readU8(slice); // in bits
|
|
1611
|
-
const sampleCount = readU32Be(slice);
|
|
1612
|
-
|
|
1613
|
-
const bytes = readBytes(slice, Math.ceil(sampleCount * fieldSize / 8));
|
|
1614
|
-
const bitstream = new Bitstream(bytes);
|
|
1615
|
-
|
|
1616
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
1617
|
-
const sampleSize = bitstream.readBits(fieldSize);
|
|
1618
|
-
track.sampleTable.sampleSizes.push(sampleSize);
|
|
1619
|
-
}
|
|
1620
|
-
}; break;
|
|
1621
|
-
|
|
1622
|
-
case 'stss': {
|
|
1623
|
-
const track = this.currentTrack;
|
|
1624
|
-
if (!track) {
|
|
1625
|
-
break;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
if (!track.sampleTable) {
|
|
1629
|
-
break;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
slice.skip(4); // Version + flags
|
|
1633
|
-
|
|
1634
|
-
track.sampleTable.keySampleIndices = [];
|
|
1635
|
-
|
|
1636
|
-
const entryCount = readU32Be(slice);
|
|
1637
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1638
|
-
const sampleIndex = readU32Be(slice) - 1; // Convert to 0-indexed
|
|
1639
|
-
track.sampleTable.keySampleIndices.push(sampleIndex);
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
if (track.sampleTable.keySampleIndices[0] !== 0) {
|
|
1643
|
-
// Some files don't mark the first sample a key sample, which is basically almost always incorrect.
|
|
1644
|
-
// Here, we correct for that mistake:
|
|
1645
|
-
track.sampleTable.keySampleIndices.unshift(0);
|
|
1646
|
-
}
|
|
1647
|
-
}; break;
|
|
1648
|
-
|
|
1649
|
-
case 'stsc': {
|
|
1650
|
-
const track = this.currentTrack;
|
|
1651
|
-
if (!track) {
|
|
1652
|
-
break;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
if (!track.sampleTable) {
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
slice.skip(4);
|
|
1660
|
-
|
|
1661
|
-
const entryCount = readU32Be(slice);
|
|
1662
|
-
|
|
1663
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1664
|
-
const startChunkIndex = readU32Be(slice) - 1; // Convert to 0-indexed
|
|
1665
|
-
const samplesPerChunk = readU32Be(slice);
|
|
1666
|
-
const sampleDescriptionIndex = readU32Be(slice);
|
|
1667
|
-
|
|
1668
|
-
track.sampleTable.sampleToChunk.push({
|
|
1669
|
-
startSampleIndex: -1,
|
|
1670
|
-
startChunkIndex,
|
|
1671
|
-
samplesPerChunk,
|
|
1672
|
-
sampleDescriptionIndex,
|
|
1673
|
-
});
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
let startSampleIndex = 0;
|
|
1677
|
-
for (let i = 0; i < track.sampleTable.sampleToChunk.length; i++) {
|
|
1678
|
-
track.sampleTable.sampleToChunk[i]!.startSampleIndex = startSampleIndex;
|
|
1679
|
-
|
|
1680
|
-
if (i < track.sampleTable.sampleToChunk.length - 1) {
|
|
1681
|
-
const nextChunk = track.sampleTable.sampleToChunk[i + 1]!;
|
|
1682
|
-
const chunkCount = nextChunk.startChunkIndex
|
|
1683
|
-
- track.sampleTable.sampleToChunk[i]!.startChunkIndex;
|
|
1684
|
-
startSampleIndex += chunkCount * track.sampleTable.sampleToChunk[i]!.samplesPerChunk;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}; break;
|
|
1688
|
-
|
|
1689
|
-
case 'stco': {
|
|
1690
|
-
const track = this.currentTrack;
|
|
1691
|
-
if (!track) {
|
|
1692
|
-
break;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
if (!track.sampleTable) {
|
|
1696
|
-
break;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
slice.skip(4); // Version + flags
|
|
1700
|
-
|
|
1701
|
-
const entryCount = readU32Be(slice);
|
|
1702
|
-
|
|
1703
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1704
|
-
const chunkOffset = readU32Be(slice);
|
|
1705
|
-
track.sampleTable.chunkOffsets.push(chunkOffset);
|
|
1706
|
-
}
|
|
1707
|
-
}; break;
|
|
1708
|
-
|
|
1709
|
-
case 'co64': {
|
|
1710
|
-
const track = this.currentTrack;
|
|
1711
|
-
if (!track) {
|
|
1712
|
-
break;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
if (!track.sampleTable) {
|
|
1716
|
-
break;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
slice.skip(4); // Version + flags
|
|
1720
|
-
|
|
1721
|
-
const entryCount = readU32Be(slice);
|
|
1722
|
-
|
|
1723
|
-
for (let i = 0; i < entryCount; i++) {
|
|
1724
|
-
const chunkOffset = readU64Be(slice);
|
|
1725
|
-
track.sampleTable.chunkOffsets.push(chunkOffset);
|
|
1726
|
-
}
|
|
1727
|
-
}; break;
|
|
1728
|
-
|
|
1729
|
-
case 'mvex': {
|
|
1730
|
-
this.isFragmented = true;
|
|
1731
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
1732
|
-
}; break;
|
|
1733
|
-
|
|
1734
|
-
case 'mehd': {
|
|
1735
|
-
const version = readU8(slice);
|
|
1736
|
-
slice.skip(3); // Flags
|
|
1737
|
-
|
|
1738
|
-
const fragmentDuration = version === 1 ? readU64Be(slice) : readU32Be(slice);
|
|
1739
|
-
this.movieDurationInTimescale = fragmentDuration;
|
|
1740
|
-
}; break;
|
|
1741
|
-
|
|
1742
|
-
case 'trex': {
|
|
1743
|
-
slice.skip(4); // Version + flags
|
|
1744
|
-
|
|
1745
|
-
const trackId = readU32Be(slice);
|
|
1746
|
-
const defaultSampleDescriptionIndex = readU32Be(slice);
|
|
1747
|
-
const defaultSampleDuration = readU32Be(slice);
|
|
1748
|
-
const defaultSampleSize = readU32Be(slice);
|
|
1749
|
-
const defaultSampleFlags = readU32Be(slice);
|
|
1750
|
-
|
|
1751
|
-
// We store these separately rather than in the tracks since the tracks may not exist yet
|
|
1752
|
-
this.fragmentTrackDefaults.push({
|
|
1753
|
-
trackId,
|
|
1754
|
-
defaultSampleDescriptionIndex,
|
|
1755
|
-
defaultSampleDuration,
|
|
1756
|
-
defaultSampleSize,
|
|
1757
|
-
defaultSampleFlags,
|
|
1758
|
-
});
|
|
1759
|
-
}; break;
|
|
1760
|
-
|
|
1761
|
-
case 'tfra': {
|
|
1762
|
-
const version = readU8(slice);
|
|
1763
|
-
slice.skip(3); // Flags
|
|
1764
|
-
|
|
1765
|
-
const trackId = readU32Be(slice);
|
|
1766
|
-
const track = this.tracks.find(x => x.id === trackId);
|
|
1767
|
-
if (!track) {
|
|
1768
|
-
break;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
const word = readU32Be(slice);
|
|
1772
|
-
|
|
1773
|
-
const lengthSizeOfTrafNum = (word & 0b110000) >> 4;
|
|
1774
|
-
const lengthSizeOfTrunNum = (word & 0b001100) >> 2;
|
|
1775
|
-
const lengthSizeOfSampleNum = word & 0b000011;
|
|
1776
|
-
|
|
1777
|
-
const functions = [readU8, readU16Be, readU24Be, readU32Be];
|
|
1778
|
-
|
|
1779
|
-
const readTrafNum = functions[lengthSizeOfTrafNum]!;
|
|
1780
|
-
const readTrunNum = functions[lengthSizeOfTrunNum]!;
|
|
1781
|
-
const readSampleNum = functions[lengthSizeOfSampleNum]!;
|
|
1782
|
-
|
|
1783
|
-
const numberOfEntries = readU32Be(slice);
|
|
1784
|
-
for (let i = 0; i < numberOfEntries; i++) {
|
|
1785
|
-
const time = version === 1 ? readU64Be(slice) : readU32Be(slice);
|
|
1786
|
-
const moofOffset = version === 1 ? readU64Be(slice) : readU32Be(slice);
|
|
1787
|
-
|
|
1788
|
-
readTrafNum(slice);
|
|
1789
|
-
readTrunNum(slice);
|
|
1790
|
-
readSampleNum(slice);
|
|
1791
|
-
|
|
1792
|
-
track.fragmentLookupTable.push({
|
|
1793
|
-
timestamp: time,
|
|
1794
|
-
moofOffset,
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Sort by timestamp in case it's not naturally sorted
|
|
1799
|
-
track.fragmentLookupTable.sort((a, b) => a.timestamp - b.timestamp);
|
|
1800
|
-
|
|
1801
|
-
// Remove multiple entries for the same time
|
|
1802
|
-
for (let i = 0; i < track.fragmentLookupTable.length - 1; i++) {
|
|
1803
|
-
const entry1 = track.fragmentLookupTable[i]!;
|
|
1804
|
-
const entry2 = track.fragmentLookupTable[i + 1]!;
|
|
1805
|
-
|
|
1806
|
-
if (entry1.timestamp === entry2.timestamp) {
|
|
1807
|
-
track.fragmentLookupTable.splice(i + 1, 1);
|
|
1808
|
-
i--;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
}; break;
|
|
1812
|
-
|
|
1813
|
-
case 'moof': {
|
|
1814
|
-
this.currentFragment = {
|
|
1815
|
-
moofOffset: startPos,
|
|
1816
|
-
moofSize: boxInfo.totalSize,
|
|
1817
|
-
implicitBaseDataOffset: startPos,
|
|
1818
|
-
trackData: new Map(),
|
|
1819
|
-
};
|
|
1820
|
-
|
|
1821
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
1822
|
-
|
|
1823
|
-
this.lastReadFragment = this.currentFragment;
|
|
1824
|
-
this.currentFragment = null;
|
|
1825
|
-
}; break;
|
|
1826
|
-
|
|
1827
|
-
case 'traf': {
|
|
1828
|
-
assert(this.currentFragment);
|
|
1829
|
-
|
|
1830
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
1831
|
-
|
|
1832
|
-
// It is possible that there is no current track, for example when we don't care about the track
|
|
1833
|
-
// referenced in the track fragment header.
|
|
1834
|
-
if (this.currentTrack) {
|
|
1835
|
-
const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
|
|
1836
|
-
if (trackData) {
|
|
1837
|
-
const { currentFragmentState } = this.currentTrack;
|
|
1838
|
-
assert(currentFragmentState);
|
|
1839
|
-
|
|
1840
|
-
if (currentFragmentState.startTimestamp !== null) {
|
|
1841
|
-
offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp);
|
|
1842
|
-
trackData.startTimestampIsFinal = true;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
this.currentTrack.currentFragmentState = null;
|
|
1847
|
-
this.currentTrack = null;
|
|
1848
|
-
}
|
|
1849
|
-
}; break;
|
|
1850
|
-
|
|
1851
|
-
case 'tfhd': {
|
|
1852
|
-
assert(this.currentFragment);
|
|
1853
|
-
|
|
1854
|
-
slice.skip(1); // Version
|
|
1855
|
-
|
|
1856
|
-
const flags = readU24Be(slice);
|
|
1857
|
-
const baseDataOffsetPresent = Boolean(flags & 0x000001);
|
|
1858
|
-
const sampleDescriptionIndexPresent = Boolean(flags & 0x000002);
|
|
1859
|
-
const defaultSampleDurationPresent = Boolean(flags & 0x000008);
|
|
1860
|
-
const defaultSampleSizePresent = Boolean(flags & 0x000010);
|
|
1861
|
-
const defaultSampleFlagsPresent = Boolean(flags & 0x000020);
|
|
1862
|
-
const durationIsEmpty = Boolean(flags & 0x010000);
|
|
1863
|
-
const defaultBaseIsMoof = Boolean(flags & 0x020000);
|
|
1864
|
-
|
|
1865
|
-
const trackId = readU32Be(slice);
|
|
1866
|
-
const track = this.tracks.find(x => x.id === trackId);
|
|
1867
|
-
if (!track) {
|
|
1868
|
-
// We don't care about this track
|
|
1869
|
-
break;
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
const defaults = this.fragmentTrackDefaults.find(x => x.trackId === trackId);
|
|
1873
|
-
|
|
1874
|
-
this.currentTrack = track;
|
|
1875
|
-
track.currentFragmentState = {
|
|
1876
|
-
baseDataOffset: this.currentFragment.implicitBaseDataOffset,
|
|
1877
|
-
sampleDescriptionIndex: defaults?.defaultSampleDescriptionIndex ?? null,
|
|
1878
|
-
defaultSampleDuration: defaults?.defaultSampleDuration ?? null,
|
|
1879
|
-
defaultSampleSize: defaults?.defaultSampleSize ?? null,
|
|
1880
|
-
defaultSampleFlags: defaults?.defaultSampleFlags ?? null,
|
|
1881
|
-
startTimestamp: null,
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
if (baseDataOffsetPresent) {
|
|
1885
|
-
track.currentFragmentState.baseDataOffset = readU64Be(slice);
|
|
1886
|
-
} else if (defaultBaseIsMoof) {
|
|
1887
|
-
track.currentFragmentState.baseDataOffset = this.currentFragment.moofOffset;
|
|
1888
|
-
}
|
|
1889
|
-
if (sampleDescriptionIndexPresent) {
|
|
1890
|
-
track.currentFragmentState.sampleDescriptionIndex = readU32Be(slice);
|
|
1891
|
-
}
|
|
1892
|
-
if (defaultSampleDurationPresent) {
|
|
1893
|
-
track.currentFragmentState.defaultSampleDuration = readU32Be(slice);
|
|
1894
|
-
}
|
|
1895
|
-
if (defaultSampleSizePresent) {
|
|
1896
|
-
track.currentFragmentState.defaultSampleSize = readU32Be(slice);
|
|
1897
|
-
}
|
|
1898
|
-
if (defaultSampleFlagsPresent) {
|
|
1899
|
-
track.currentFragmentState.defaultSampleFlags = readU32Be(slice);
|
|
1900
|
-
}
|
|
1901
|
-
if (durationIsEmpty) {
|
|
1902
|
-
track.currentFragmentState.defaultSampleDuration = 0;
|
|
1903
|
-
}
|
|
1904
|
-
}; break;
|
|
1905
|
-
|
|
1906
|
-
case 'tfdt': {
|
|
1907
|
-
const track = this.currentTrack;
|
|
1908
|
-
if (!track) {
|
|
1909
|
-
break;
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
assert(track.currentFragmentState);
|
|
1913
|
-
|
|
1914
|
-
const version = readU8(slice);
|
|
1915
|
-
slice.skip(3); // Flags
|
|
1916
|
-
|
|
1917
|
-
const baseMediaDecodeTime = version === 0 ? readU32Be(slice) : readU64Be(slice);
|
|
1918
|
-
track.currentFragmentState.startTimestamp = baseMediaDecodeTime;
|
|
1919
|
-
}; break;
|
|
1920
|
-
|
|
1921
|
-
case 'trun': {
|
|
1922
|
-
const track = this.currentTrack;
|
|
1923
|
-
if (!track) {
|
|
1924
|
-
break;
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
assert(this.currentFragment);
|
|
1928
|
-
assert(track.currentFragmentState);
|
|
1929
|
-
|
|
1930
|
-
if (this.currentFragment.trackData.has(track.id)) {
|
|
1931
|
-
console.warn('Can\'t have two trun boxes for the same track in one fragment. Ignoring...');
|
|
1932
|
-
break;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
const version = readU8(slice);
|
|
1936
|
-
|
|
1937
|
-
const flags = readU24Be(slice);
|
|
1938
|
-
const dataOffsetPresent = Boolean(flags & 0x000001);
|
|
1939
|
-
const firstSampleFlagsPresent = Boolean(flags & 0x000004);
|
|
1940
|
-
const sampleDurationPresent = Boolean(flags & 0x000100);
|
|
1941
|
-
const sampleSizePresent = Boolean(flags & 0x000200);
|
|
1942
|
-
const sampleFlagsPresent = Boolean(flags & 0x000400);
|
|
1943
|
-
const sampleCompositionTimeOffsetsPresent = Boolean(flags & 0x000800);
|
|
1944
|
-
|
|
1945
|
-
const sampleCount = readU32Be(slice);
|
|
1946
|
-
|
|
1947
|
-
let dataOffset = track.currentFragmentState.baseDataOffset;
|
|
1948
|
-
if (dataOffsetPresent) {
|
|
1949
|
-
dataOffset += readI32Be(slice);
|
|
1950
|
-
}
|
|
1951
|
-
let firstSampleFlags: number | null = null;
|
|
1952
|
-
if (firstSampleFlagsPresent) {
|
|
1953
|
-
firstSampleFlags = readU32Be(slice);
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
let currentOffset = dataOffset;
|
|
1957
|
-
|
|
1958
|
-
if (sampleCount === 0) {
|
|
1959
|
-
// Don't associate the fragment with the track if it has no samples, this simplifies other code
|
|
1960
|
-
this.currentFragment.implicitBaseDataOffset = currentOffset;
|
|
1961
|
-
break;
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
let currentTimestamp = 0;
|
|
1965
|
-
|
|
1966
|
-
const trackData: FragmentTrackData = {
|
|
1967
|
-
track,
|
|
1968
|
-
startTimestamp: 0,
|
|
1969
|
-
endTimestamp: 0,
|
|
1970
|
-
firstKeyFrameTimestamp: null,
|
|
1971
|
-
samples: [],
|
|
1972
|
-
presentationTimestamps: [],
|
|
1973
|
-
startTimestampIsFinal: false,
|
|
1974
|
-
};
|
|
1975
|
-
this.currentFragment.trackData.set(track.id, trackData);
|
|
1976
|
-
|
|
1977
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
1978
|
-
let sampleDuration: number;
|
|
1979
|
-
if (sampleDurationPresent) {
|
|
1980
|
-
sampleDuration = readU32Be(slice);
|
|
1981
|
-
} else {
|
|
1982
|
-
assert(track.currentFragmentState.defaultSampleDuration !== null);
|
|
1983
|
-
sampleDuration = track.currentFragmentState.defaultSampleDuration;
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
let sampleSize: number;
|
|
1987
|
-
if (sampleSizePresent) {
|
|
1988
|
-
sampleSize = readU32Be(slice);
|
|
1989
|
-
} else {
|
|
1990
|
-
assert(track.currentFragmentState.defaultSampleSize !== null);
|
|
1991
|
-
sampleSize = track.currentFragmentState.defaultSampleSize;
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
let sampleFlags: number;
|
|
1995
|
-
if (sampleFlagsPresent) {
|
|
1996
|
-
sampleFlags = readU32Be(slice);
|
|
1997
|
-
} else {
|
|
1998
|
-
assert(track.currentFragmentState.defaultSampleFlags !== null);
|
|
1999
|
-
sampleFlags = track.currentFragmentState.defaultSampleFlags;
|
|
2000
|
-
}
|
|
2001
|
-
if (i === 0 && firstSampleFlags !== null) {
|
|
2002
|
-
sampleFlags = firstSampleFlags;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
let sampleCompositionTimeOffset = 0;
|
|
2006
|
-
if (sampleCompositionTimeOffsetsPresent) {
|
|
2007
|
-
if (version === 0) {
|
|
2008
|
-
sampleCompositionTimeOffset = readU32Be(slice);
|
|
2009
|
-
} else {
|
|
2010
|
-
sampleCompositionTimeOffset = readI32Be(slice);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
const isKeyFrame = !(sampleFlags & 0x00010000);
|
|
2015
|
-
|
|
2016
|
-
trackData.samples.push({
|
|
2017
|
-
presentationTimestamp: currentTimestamp + sampleCompositionTimeOffset,
|
|
2018
|
-
duration: sampleDuration,
|
|
2019
|
-
byteOffset: currentOffset,
|
|
2020
|
-
byteSize: sampleSize,
|
|
2021
|
-
isKeyFrame,
|
|
2022
|
-
});
|
|
2023
|
-
|
|
2024
|
-
currentOffset += sampleSize;
|
|
2025
|
-
currentTimestamp += sampleDuration;
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
trackData.presentationTimestamps = trackData.samples
|
|
2029
|
-
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
|
|
2030
|
-
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
|
|
2031
|
-
|
|
2032
|
-
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
|
2033
|
-
const currentEntry = trackData.presentationTimestamps[i]!;
|
|
2034
|
-
const currentSample = trackData.samples[currentEntry.sampleIndex]!;
|
|
2035
|
-
|
|
2036
|
-
if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
|
|
2037
|
-
trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
if (i < trackData.presentationTimestamps.length - 1) {
|
|
2041
|
-
// Update sample durations based on presentation order
|
|
2042
|
-
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
|
2043
|
-
currentSample.duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
|
|
2048
|
-
const lastSample = trackData.samples[last(trackData.presentationTimestamps)!.sampleIndex]!;
|
|
2049
|
-
|
|
2050
|
-
trackData.startTimestamp = firstSample.presentationTimestamp;
|
|
2051
|
-
trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
|
|
2052
|
-
|
|
2053
|
-
this.currentFragment.implicitBaseDataOffset = currentOffset;
|
|
2054
|
-
}; break;
|
|
2055
|
-
|
|
2056
|
-
// Metadata section
|
|
2057
|
-
// https://exiftool.org/TagNames/QuickTime.html
|
|
2058
|
-
// https://mp4workshop.com/about
|
|
2059
|
-
|
|
2060
|
-
case 'udta': { // Contains either movie metadata or track metadata
|
|
2061
|
-
const iterator = this.iterateContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
2062
|
-
|
|
2063
|
-
for (const { boxInfo, slice } of iterator) {
|
|
2064
|
-
if (boxInfo.name !== 'meta' && !this.currentTrack) {
|
|
2065
|
-
const startPos = slice.filePos;
|
|
2066
|
-
this.metadataTags.raw ??= {};
|
|
2067
|
-
|
|
2068
|
-
if (boxInfo.name[0] === '©') {
|
|
2069
|
-
// https://mp4workshop.com/about
|
|
2070
|
-
// Box name starting with © indicates "international text"
|
|
2071
|
-
this.metadataTags.raw[boxInfo.name] ??= readMetadataStringShort(slice);
|
|
2072
|
-
} else {
|
|
2073
|
-
this.metadataTags.raw[boxInfo.name] ??= readBytes(slice, boxInfo.contentSize);
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
slice.filePos = startPos;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
switch (boxInfo.name) {
|
|
2080
|
-
case 'meta': {
|
|
2081
|
-
slice.skip(-boxInfo.headerSize);
|
|
2082
|
-
this.traverseBox(slice);
|
|
2083
|
-
}; break;
|
|
2084
|
-
|
|
2085
|
-
case '©nam':
|
|
2086
|
-
case 'name': {
|
|
2087
|
-
if (this.currentTrack) {
|
|
2088
|
-
this.currentTrack.name = textDecoder.decode(readBytes(slice, boxInfo.contentSize));
|
|
2089
|
-
} else {
|
|
2090
|
-
this.metadataTags.title ??= readMetadataStringShort(slice);
|
|
2091
|
-
}
|
|
2092
|
-
}; break;
|
|
2093
|
-
|
|
2094
|
-
case '©des': {
|
|
2095
|
-
if (!this.currentTrack) {
|
|
2096
|
-
this.metadataTags.description ??= readMetadataStringShort(slice);
|
|
2097
|
-
}
|
|
2098
|
-
}; break;
|
|
2099
|
-
|
|
2100
|
-
case '©ART': {
|
|
2101
|
-
if (!this.currentTrack) {
|
|
2102
|
-
this.metadataTags.artist ??= readMetadataStringShort(slice);
|
|
2103
|
-
}
|
|
2104
|
-
}; break;
|
|
2105
|
-
|
|
2106
|
-
case '©alb': {
|
|
2107
|
-
if (!this.currentTrack) {
|
|
2108
|
-
this.metadataTags.album ??= readMetadataStringShort(slice);
|
|
2109
|
-
}
|
|
2110
|
-
}; break;
|
|
2111
|
-
|
|
2112
|
-
case 'albr': {
|
|
2113
|
-
if (!this.currentTrack) {
|
|
2114
|
-
this.metadataTags.albumArtist ??= readMetadataStringShort(slice);
|
|
2115
|
-
}
|
|
2116
|
-
}; break;
|
|
2117
|
-
|
|
2118
|
-
case '©gen': {
|
|
2119
|
-
if (!this.currentTrack) {
|
|
2120
|
-
this.metadataTags.genre ??= readMetadataStringShort(slice);
|
|
2121
|
-
}
|
|
2122
|
-
}; break;
|
|
2123
|
-
|
|
2124
|
-
case '©day': {
|
|
2125
|
-
if (!this.currentTrack) {
|
|
2126
|
-
const date = new Date(readMetadataStringShort(slice));
|
|
2127
|
-
if (!Number.isNaN(date.getTime())) {
|
|
2128
|
-
this.metadataTags.date ??= date;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}; break;
|
|
2132
|
-
|
|
2133
|
-
case '©cmt': {
|
|
2134
|
-
if (!this.currentTrack) {
|
|
2135
|
-
this.metadataTags.comment ??= readMetadataStringShort(slice);
|
|
2136
|
-
}
|
|
2137
|
-
}; break;
|
|
2138
|
-
|
|
2139
|
-
case '©lyr': {
|
|
2140
|
-
if (!this.currentTrack) {
|
|
2141
|
-
this.metadataTags.lyrics ??= readMetadataStringShort(slice);
|
|
2142
|
-
}
|
|
2143
|
-
}; break;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
}; break;
|
|
2147
|
-
|
|
2148
|
-
case 'meta': {
|
|
2149
|
-
if (this.currentTrack) {
|
|
2150
|
-
break; // Only care about movie-level metadata for now
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
// The 'meta' box comes in two flavors, one with flags/version and one without. To know which is which,
|
|
2154
|
-
// let's read the next 4 bytes, which are either the version or the size of the first subbox.
|
|
2155
|
-
const word = readU32Be(slice);
|
|
2156
|
-
const isQuickTime = word !== 0;
|
|
2157
|
-
|
|
2158
|
-
this.currentMetadataKeys = new Map();
|
|
2159
|
-
|
|
2160
|
-
if (isQuickTime) {
|
|
2161
|
-
this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
2162
|
-
} else {
|
|
2163
|
-
this.readContiguousBoxes(slice.slice(contentStartPos + 4, boxInfo.contentSize - 4));
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
this.currentMetadataKeys = null;
|
|
2167
|
-
}; break;
|
|
2168
|
-
|
|
2169
|
-
case 'keys': {
|
|
2170
|
-
if (!this.currentMetadataKeys) {
|
|
2171
|
-
break;
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
slice.skip(4); // Version + flags
|
|
2175
|
-
|
|
2176
|
-
const entryCount = readU32Be(slice);
|
|
2177
|
-
|
|
2178
|
-
for (let i = 0; i < entryCount; i++) {
|
|
2179
|
-
const keySize = readU32Be(slice);
|
|
2180
|
-
slice.skip(4); // Key namespace
|
|
2181
|
-
const keyName = textDecoder.decode(readBytes(slice, keySize - 8));
|
|
2182
|
-
|
|
2183
|
-
this.currentMetadataKeys.set(i + 1, keyName);
|
|
2184
|
-
}
|
|
2185
|
-
}; break;
|
|
2186
|
-
|
|
2187
|
-
case 'ilst': {
|
|
2188
|
-
if (!this.currentMetadataKeys) {
|
|
2189
|
-
break;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
const iterator = this.iterateContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
|
|
2193
|
-
|
|
2194
|
-
for (const { boxInfo, slice } of iterator) {
|
|
2195
|
-
let metadataKey = boxInfo.name;
|
|
2196
|
-
|
|
2197
|
-
// Interpret the box name as a u32be
|
|
2198
|
-
const nameAsNumber = (metadataKey.charCodeAt(0) << 24)
|
|
2199
|
-
+ (metadataKey.charCodeAt(1) << 16)
|
|
2200
|
-
+ (metadataKey.charCodeAt(2) << 8)
|
|
2201
|
-
+ metadataKey.charCodeAt(3);
|
|
2202
|
-
|
|
2203
|
-
if (this.currentMetadataKeys.has(nameAsNumber)) {
|
|
2204
|
-
// An entry exists for this number
|
|
2205
|
-
metadataKey = this.currentMetadataKeys.get(nameAsNumber)!;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
const data = readDataBox(slice);
|
|
2209
|
-
|
|
2210
|
-
this.metadataTags.raw ??= {};
|
|
2211
|
-
this.metadataTags.raw[metadataKey] ??= data;
|
|
2212
|
-
|
|
2213
|
-
switch (metadataKey) {
|
|
2214
|
-
case '©nam':
|
|
2215
|
-
case 'titl':
|
|
2216
|
-
case 'com.apple.quicktime.title':
|
|
2217
|
-
case 'title': {
|
|
2218
|
-
if (typeof data === 'string') {
|
|
2219
|
-
this.metadataTags.title ??= data;
|
|
2220
|
-
}
|
|
2221
|
-
}; break;
|
|
2222
|
-
|
|
2223
|
-
case '©des':
|
|
2224
|
-
case 'desc':
|
|
2225
|
-
case 'dscp':
|
|
2226
|
-
case 'com.apple.quicktime.description':
|
|
2227
|
-
case 'description': {
|
|
2228
|
-
if (typeof data === 'string') {
|
|
2229
|
-
this.metadataTags.description ??= data;
|
|
2230
|
-
}
|
|
2231
|
-
}; break;
|
|
2232
|
-
|
|
2233
|
-
case '©ART':
|
|
2234
|
-
case 'com.apple.quicktime.artist':
|
|
2235
|
-
case 'artist': {
|
|
2236
|
-
if (typeof data === 'string') {
|
|
2237
|
-
this.metadataTags.artist ??= data;
|
|
2238
|
-
}
|
|
2239
|
-
}; break;
|
|
2240
|
-
|
|
2241
|
-
case '©alb':
|
|
2242
|
-
case 'albm':
|
|
2243
|
-
case 'com.apple.quicktime.album':
|
|
2244
|
-
case 'album': {
|
|
2245
|
-
if (typeof data === 'string') {
|
|
2246
|
-
this.metadataTags.album ??= data;
|
|
2247
|
-
}
|
|
2248
|
-
}; break;
|
|
2249
|
-
|
|
2250
|
-
case 'aART':
|
|
2251
|
-
case 'album_artist': {
|
|
2252
|
-
if (typeof data === 'string') {
|
|
2253
|
-
this.metadataTags.albumArtist ??= data;
|
|
2254
|
-
}
|
|
2255
|
-
}; break;
|
|
2256
|
-
|
|
2257
|
-
case '©cmt':
|
|
2258
|
-
case 'com.apple.quicktime.comment':
|
|
2259
|
-
case 'comment': {
|
|
2260
|
-
if (typeof data === 'string') {
|
|
2261
|
-
this.metadataTags.comment ??= data;
|
|
2262
|
-
}
|
|
2263
|
-
}; break;
|
|
2264
|
-
|
|
2265
|
-
case '©gen':
|
|
2266
|
-
case 'gnre':
|
|
2267
|
-
case 'com.apple.quicktime.genre':
|
|
2268
|
-
case 'genre': {
|
|
2269
|
-
if (typeof data === 'string') {
|
|
2270
|
-
this.metadataTags.genre ??= data;
|
|
2271
|
-
}
|
|
2272
|
-
}; break;
|
|
2273
|
-
|
|
2274
|
-
case '©lyr':
|
|
2275
|
-
case 'lyrics': {
|
|
2276
|
-
if (typeof data === 'string') {
|
|
2277
|
-
this.metadataTags.lyrics ??= data;
|
|
2278
|
-
}
|
|
2279
|
-
}; break;
|
|
2280
|
-
|
|
2281
|
-
case '©day':
|
|
2282
|
-
case 'rldt':
|
|
2283
|
-
case 'com.apple.quicktime.creationdate':
|
|
2284
|
-
case 'date': {
|
|
2285
|
-
if (typeof data === 'string') {
|
|
2286
|
-
const date = new Date(data);
|
|
2287
|
-
if (!Number.isNaN(date.getTime())) {
|
|
2288
|
-
this.metadataTags.date ??= date;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
}; break;
|
|
2292
|
-
|
|
2293
|
-
case 'covr':
|
|
2294
|
-
case 'com.apple.quicktime.artwork': {
|
|
2295
|
-
if (data instanceof RichImageData) {
|
|
2296
|
-
this.metadataTags.images ??= [];
|
|
2297
|
-
this.metadataTags.images.push({
|
|
2298
|
-
data: data.data,
|
|
2299
|
-
kind: 'coverFront',
|
|
2300
|
-
mimeType: data.mimeType,
|
|
2301
|
-
});
|
|
2302
|
-
} else if (data instanceof Uint8Array) {
|
|
2303
|
-
this.metadataTags.images ??= [];
|
|
2304
|
-
this.metadataTags.images.push({
|
|
2305
|
-
data,
|
|
2306
|
-
kind: 'coverFront',
|
|
2307
|
-
mimeType: 'image/*',
|
|
2308
|
-
});
|
|
2309
|
-
}
|
|
2310
|
-
}; break;
|
|
2311
|
-
|
|
2312
|
-
case 'track': {
|
|
2313
|
-
if (typeof data === 'string') {
|
|
2314
|
-
const parts = data.split('/');
|
|
2315
|
-
const trackNum = Number.parseInt(parts[0]!, 10);
|
|
2316
|
-
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
|
|
2317
|
-
|
|
2318
|
-
if (Number.isInteger(trackNum) && trackNum > 0) {
|
|
2319
|
-
this.metadataTags.trackNumber ??= trackNum;
|
|
2320
|
-
}
|
|
2321
|
-
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
|
|
2322
|
-
this.metadataTags.tracksTotal ??= tracksTotal;
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
}; break;
|
|
2326
|
-
|
|
2327
|
-
case 'trkn': {
|
|
2328
|
-
if (data instanceof Uint8Array && data.length >= 6) {
|
|
2329
|
-
const view = toDataView(data);
|
|
2330
|
-
|
|
2331
|
-
const trackNumber = view.getUint16(2, false);
|
|
2332
|
-
const tracksTotal = view.getUint16(4, false);
|
|
2333
|
-
|
|
2334
|
-
if (trackNumber > 0) {
|
|
2335
|
-
this.metadataTags.trackNumber ??= trackNumber;
|
|
2336
|
-
}
|
|
2337
|
-
if (tracksTotal > 0) {
|
|
2338
|
-
this.metadataTags.tracksTotal ??= tracksTotal;
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
}; break;
|
|
2342
|
-
|
|
2343
|
-
case 'disc':
|
|
2344
|
-
case 'disk': {
|
|
2345
|
-
if (data instanceof Uint8Array && data.length >= 6) {
|
|
2346
|
-
const view = toDataView(data);
|
|
2347
|
-
|
|
2348
|
-
const discNumber = view.getUint16(2, false);
|
|
2349
|
-
const discNumberMax = view.getUint16(4, false);
|
|
2350
|
-
|
|
2351
|
-
if (discNumber > 0) {
|
|
2352
|
-
this.metadataTags.discNumber ??= discNumber;
|
|
2353
|
-
}
|
|
2354
|
-
if (discNumberMax > 0) {
|
|
2355
|
-
this.metadataTags.discsTotal ??= discNumberMax;
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
}; break;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
}; break;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
slice.filePos = boxEndPos;
|
|
2365
|
-
return true;
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|
2370
|
-
packetToSampleIndex = new WeakMap<EncodedPacket, number>();
|
|
2371
|
-
packetToFragmentLocation = new WeakMap<EncodedPacket, {
|
|
2372
|
-
fragment: Fragment;
|
|
2373
|
-
sampleIndex: number;
|
|
2374
|
-
}>();
|
|
2375
|
-
|
|
2376
|
-
constructor(public internalTrack: InternalTrack) {}
|
|
2377
|
-
|
|
2378
|
-
getId() {
|
|
2379
|
-
return this.internalTrack.id;
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
getCodec(): MediaCodec | null {
|
|
2383
|
-
throw new Error('Not implemented on base class.');
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
getInternalCodecId() {
|
|
2387
|
-
return this.internalTrack.internalCodecId;
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
getName() {
|
|
2391
|
-
return this.internalTrack.name;
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
getLanguageCode() {
|
|
2395
|
-
return this.internalTrack.languageCode;
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
getTimeResolution() {
|
|
2399
|
-
return this.internalTrack.timescale;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
getDisposition() {
|
|
2403
|
-
return this.internalTrack.disposition;
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
async computeDuration() {
|
|
2407
|
-
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
|
|
2408
|
-
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
async getFirstTimestamp() {
|
|
2412
|
-
const firstPacket = await this.getFirstPacket({ metadataOnly: true });
|
|
2413
|
-
return firstPacket?.timestamp ?? 0;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
async getFirstPacket(options: PacketRetrievalOptions) {
|
|
2417
|
-
const regularPacket = await this.fetchPacketForSampleIndex(0, options);
|
|
2418
|
-
if (regularPacket || !this.internalTrack.demuxer.isFragmented) {
|
|
2419
|
-
// If there's a non-fragmented packet, always prefer that
|
|
2420
|
-
return regularPacket;
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
return this.performFragmentedLookup(
|
|
2424
|
-
null,
|
|
2425
|
-
(fragment) => {
|
|
2426
|
-
const trackData = fragment.trackData.get(this.internalTrack.id);
|
|
2427
|
-
if (trackData) {
|
|
2428
|
-
return {
|
|
2429
|
-
sampleIndex: 0,
|
|
2430
|
-
correctSampleFound: true,
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
return {
|
|
2435
|
-
sampleIndex: -1,
|
|
2436
|
-
correctSampleFound: false,
|
|
2437
|
-
};
|
|
2438
|
-
},
|
|
2439
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries
|
|
2440
|
-
Infinity,
|
|
2441
|
-
options,
|
|
2442
|
-
);
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
private mapTimestampIntoTimescale(timestamp: number) {
|
|
2446
|
-
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
|
2447
|
-
// that the number was originally an integer divided by the timescale. For stability, it's best
|
|
2448
|
-
// to return the integer in this case.
|
|
2449
|
-
return roundIfAlmostInteger(timestamp * this.internalTrack.timescale) + this.internalTrack.editListOffset;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
|
2453
|
-
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
|
2454
|
-
|
|
2455
|
-
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
2456
|
-
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
|
2457
|
-
const regularPacket = await this.fetchPacketForSampleIndex(sampleIndex, options);
|
|
2458
|
-
|
|
2459
|
-
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
|
2460
|
-
// Prefer the non-fragmented packet
|
|
2461
|
-
return regularPacket;
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
return this.performFragmentedLookup(
|
|
2465
|
-
null,
|
|
2466
|
-
(fragment) => {
|
|
2467
|
-
const trackData = fragment.trackData.get(this.internalTrack.id);
|
|
2468
|
-
if (!trackData) {
|
|
2469
|
-
return { sampleIndex: -1, correctSampleFound: false };
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
const index = binarySearchLessOrEqual(
|
|
2473
|
-
trackData.presentationTimestamps,
|
|
2474
|
-
timestampInTimescale,
|
|
2475
|
-
x => x.presentationTimestamp,
|
|
2476
|
-
);
|
|
2477
|
-
|
|
2478
|
-
const sampleIndex = index !== -1 ? trackData.presentationTimestamps[index]!.sampleIndex : -1;
|
|
2479
|
-
const correctSampleFound = index !== -1 && timestampInTimescale < trackData.endTimestamp;
|
|
2480
|
-
|
|
2481
|
-
return { sampleIndex, correctSampleFound };
|
|
2482
|
-
},
|
|
2483
|
-
timestampInTimescale,
|
|
2484
|
-
timestampInTimescale,
|
|
2485
|
-
options,
|
|
2486
|
-
);
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
|
2490
|
-
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
|
2491
|
-
|
|
2492
|
-
if (regularSampleIndex !== undefined) {
|
|
2493
|
-
// Prefer the non-fragmented packet
|
|
2494
|
-
return this.fetchPacketForSampleIndex(regularSampleIndex + 1, options);
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
|
2498
|
-
if (locationInFragment === undefined) {
|
|
2499
|
-
throw new Error('Packet was not created from this track.');
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
return this.performFragmentedLookup(
|
|
2503
|
-
locationInFragment.fragment,
|
|
2504
|
-
(fragment) => {
|
|
2505
|
-
if (fragment === locationInFragment.fragment) {
|
|
2506
|
-
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
|
2507
|
-
if (locationInFragment.sampleIndex + 1 < trackData.samples.length) {
|
|
2508
|
-
// We can simply take the next sample in the fragment
|
|
2509
|
-
return {
|
|
2510
|
-
sampleIndex: locationInFragment.sampleIndex + 1,
|
|
2511
|
-
correctSampleFound: true,
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
} else {
|
|
2515
|
-
const trackData = fragment.trackData.get(this.internalTrack.id);
|
|
2516
|
-
if (trackData) {
|
|
2517
|
-
return {
|
|
2518
|
-
sampleIndex: 0,
|
|
2519
|
-
correctSampleFound: true,
|
|
2520
|
-
};
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
return {
|
|
2525
|
-
sampleIndex: -1,
|
|
2526
|
-
correctSampleFound: false,
|
|
2527
|
-
};
|
|
2528
|
-
},
|
|
2529
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries
|
|
2530
|
-
Infinity,
|
|
2531
|
-
options,
|
|
2532
|
-
);
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
|
2536
|
-
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
|
2537
|
-
|
|
2538
|
-
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
2539
|
-
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
|
2540
|
-
const keyFrameSampleIndex = sampleIndex === -1
|
|
2541
|
-
? -1
|
|
2542
|
-
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
|
2543
|
-
const regularPacket = await this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
|
|
2544
|
-
|
|
2545
|
-
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
|
2546
|
-
// Prefer the non-fragmented packet
|
|
2547
|
-
return regularPacket;
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
return this.performFragmentedLookup(
|
|
2551
|
-
null,
|
|
2552
|
-
(fragment) => {
|
|
2553
|
-
const trackData = fragment.trackData.get(this.internalTrack.id);
|
|
2554
|
-
if (!trackData) {
|
|
2555
|
-
return { sampleIndex: -1, correctSampleFound: false };
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
|
|
2559
|
-
const sample = trackData.samples[x.sampleIndex]!;
|
|
2560
|
-
return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
|
|
2561
|
-
});
|
|
2562
|
-
|
|
2563
|
-
const sampleIndex = index !== -1 ? trackData.presentationTimestamps[index]!.sampleIndex : -1;
|
|
2564
|
-
const correctSampleFound = index !== -1 && timestampInTimescale < trackData.endTimestamp;
|
|
2565
|
-
|
|
2566
|
-
return { sampleIndex, correctSampleFound };
|
|
2567
|
-
},
|
|
2568
|
-
timestampInTimescale,
|
|
2569
|
-
timestampInTimescale,
|
|
2570
|
-
options,
|
|
2571
|
-
);
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
|
2575
|
-
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
|
2576
|
-
if (regularSampleIndex !== undefined) {
|
|
2577
|
-
// Prefer the non-fragmented packet
|
|
2578
|
-
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
2579
|
-
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
|
|
2580
|
-
return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
|
2584
|
-
if (locationInFragment === undefined) {
|
|
2585
|
-
throw new Error('Packet was not created from this track.');
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
return this.performFragmentedLookup(
|
|
2589
|
-
locationInFragment.fragment,
|
|
2590
|
-
(fragment) => {
|
|
2591
|
-
if (fragment === locationInFragment.fragment) {
|
|
2592
|
-
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
|
2593
|
-
const nextKeyFrameIndex = trackData.samples.findIndex(
|
|
2594
|
-
(x, i) => x.isKeyFrame && i > locationInFragment.sampleIndex,
|
|
2595
|
-
);
|
|
2596
|
-
|
|
2597
|
-
if (nextKeyFrameIndex !== -1) {
|
|
2598
|
-
// We can simply take the next key frame in the fragment
|
|
2599
|
-
return {
|
|
2600
|
-
sampleIndex: nextKeyFrameIndex,
|
|
2601
|
-
correctSampleFound: true,
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
} else {
|
|
2605
|
-
const trackData = fragment.trackData.get(this.internalTrack.id);
|
|
2606
|
-
if (trackData && trackData.firstKeyFrameTimestamp !== null) {
|
|
2607
|
-
const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame);
|
|
2608
|
-
assert(keyFrameIndex !== -1); // There must be one
|
|
2609
|
-
|
|
2610
|
-
return {
|
|
2611
|
-
sampleIndex: keyFrameIndex,
|
|
2612
|
-
correctSampleFound: true,
|
|
2613
|
-
};
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
return {
|
|
2618
|
-
sampleIndex: -1,
|
|
2619
|
-
correctSampleFound: false,
|
|
2620
|
-
};
|
|
2621
|
-
},
|
|
2622
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries
|
|
2623
|
-
Infinity,
|
|
2624
|
-
options,
|
|
2625
|
-
);
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) {
|
|
2629
|
-
if (sampleIndex === -1) {
|
|
2630
|
-
return null;
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
|
2634
|
-
const sampleInfo = getSampleInfo(sampleTable, sampleIndex);
|
|
2635
|
-
if (!sampleInfo) {
|
|
2636
|
-
return null;
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
let data: Uint8Array;
|
|
2640
|
-
if (options.metadataOnly) {
|
|
2641
|
-
data = PLACEHOLDER_DATA;
|
|
2642
|
-
} else {
|
|
2643
|
-
let slice = this.internalTrack.demuxer.reader.requestSlice(
|
|
2644
|
-
sampleInfo.sampleOffset,
|
|
2645
|
-
sampleInfo.sampleSize,
|
|
2646
|
-
);
|
|
2647
|
-
if (slice instanceof Promise) slice = await slice;
|
|
2648
|
-
assert(slice);
|
|
2649
|
-
|
|
2650
|
-
data = readBytes(slice, sampleInfo.sampleSize);
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
const timestamp = (sampleInfo.presentationTimestamp - this.internalTrack.editListOffset)
|
|
2654
|
-
/ this.internalTrack.timescale;
|
|
2655
|
-
const duration = sampleInfo.duration / this.internalTrack.timescale;
|
|
2656
|
-
const packet = new EncodedPacket(
|
|
2657
|
-
data,
|
|
2658
|
-
sampleInfo.isKeyFrame ? 'key' : 'delta',
|
|
2659
|
-
timestamp,
|
|
2660
|
-
duration,
|
|
2661
|
-
sampleIndex,
|
|
2662
|
-
sampleInfo.sampleSize,
|
|
2663
|
-
);
|
|
2664
|
-
|
|
2665
|
-
this.packetToSampleIndex.set(packet, sampleIndex);
|
|
2666
|
-
|
|
2667
|
-
return packet;
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
private async fetchPacketInFragment(fragment: Fragment, sampleIndex: number, options: PacketRetrievalOptions) {
|
|
2671
|
-
if (sampleIndex === -1) {
|
|
2672
|
-
return null;
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
|
2676
|
-
const fragmentSample = trackData.samples[sampleIndex];
|
|
2677
|
-
assert(fragmentSample);
|
|
2678
|
-
|
|
2679
|
-
let data: Uint8Array;
|
|
2680
|
-
if (options.metadataOnly) {
|
|
2681
|
-
data = PLACEHOLDER_DATA;
|
|
2682
|
-
} else {
|
|
2683
|
-
let slice = this.internalTrack.demuxer.reader.requestSlice(
|
|
2684
|
-
fragmentSample.byteOffset,
|
|
2685
|
-
fragmentSample.byteSize,
|
|
2686
|
-
);
|
|
2687
|
-
if (slice instanceof Promise) slice = await slice;
|
|
2688
|
-
assert(slice);
|
|
2689
|
-
|
|
2690
|
-
data = readBytes(slice, fragmentSample.byteSize);
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
const timestamp = (fragmentSample.presentationTimestamp - this.internalTrack.editListOffset)
|
|
2694
|
-
/ this.internalTrack.timescale;
|
|
2695
|
-
const duration = fragmentSample.duration / this.internalTrack.timescale;
|
|
2696
|
-
const packet = new EncodedPacket(
|
|
2697
|
-
data,
|
|
2698
|
-
fragmentSample.isKeyFrame ? 'key' : 'delta',
|
|
2699
|
-
timestamp,
|
|
2700
|
-
duration,
|
|
2701
|
-
fragment.moofOffset + sampleIndex,
|
|
2702
|
-
fragmentSample.byteSize,
|
|
2703
|
-
);
|
|
2704
|
-
|
|
2705
|
-
this.packetToFragmentLocation.set(packet, { fragment, sampleIndex });
|
|
2706
|
-
|
|
2707
|
-
return packet;
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
/** Looks for a packet in the fragments while trying to load as few fragments as possible to retrieve it. */
|
|
2711
|
-
private async performFragmentedLookup(
|
|
2712
|
-
// The fragment where we start looking
|
|
2713
|
-
startFragment: Fragment | null,
|
|
2714
|
-
// This function returns the best-matching sample in a given fragment
|
|
2715
|
-
getMatchInFragment: (fragment: Fragment) => { sampleIndex: number; correctSampleFound: boolean },
|
|
2716
|
-
// The timestamp with which we can search the lookup table
|
|
2717
|
-
searchTimestamp: number,
|
|
2718
|
-
// The timestamp for which we know the correct sample will not come after it
|
|
2719
|
-
latestTimestamp: number,
|
|
2720
|
-
options: PacketRetrievalOptions,
|
|
2721
|
-
): Promise<EncodedPacket | null> {
|
|
2722
|
-
const demuxer = this.internalTrack.demuxer;
|
|
2723
|
-
|
|
2724
|
-
let currentFragment: Fragment | null = null;
|
|
2725
|
-
let bestFragment: Fragment | null = null;
|
|
2726
|
-
let bestSampleIndex = -1;
|
|
2727
|
-
|
|
2728
|
-
if (startFragment) {
|
|
2729
|
-
const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment);
|
|
2730
|
-
|
|
2731
|
-
if (correctSampleFound) {
|
|
2732
|
-
return this.fetchPacketInFragment(startFragment, sampleIndex, options);
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
if (sampleIndex !== -1) {
|
|
2736
|
-
bestFragment = startFragment;
|
|
2737
|
-
bestSampleIndex = sampleIndex;
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
// Search for a lookup entry; this way, we won't need to start searching from the start of the file
|
|
2742
|
-
// but can jump right into the correct fragment (or at least nearby).
|
|
2743
|
-
const lookupEntryIndex = binarySearchLessOrEqual(
|
|
2744
|
-
this.internalTrack.fragmentLookupTable,
|
|
2745
|
-
searchTimestamp,
|
|
2746
|
-
x => x.timestamp,
|
|
2747
|
-
);
|
|
2748
|
-
const lookupEntry = lookupEntryIndex !== -1
|
|
2749
|
-
? this.internalTrack.fragmentLookupTable[lookupEntryIndex]!
|
|
2750
|
-
: null;
|
|
2751
|
-
|
|
2752
|
-
const positionCacheIndex = binarySearchLessOrEqual(
|
|
2753
|
-
this.internalTrack.fragmentPositionCache,
|
|
2754
|
-
searchTimestamp,
|
|
2755
|
-
x => x.startTimestamp,
|
|
2756
|
-
);
|
|
2757
|
-
const positionCacheEntry = positionCacheIndex !== -1
|
|
2758
|
-
? this.internalTrack.fragmentPositionCache[positionCacheIndex]!
|
|
2759
|
-
: null;
|
|
2760
|
-
|
|
2761
|
-
const lookupEntryPosition = Math.max(
|
|
2762
|
-
lookupEntry?.moofOffset ?? 0,
|
|
2763
|
-
positionCacheEntry?.moofOffset ?? 0,
|
|
2764
|
-
) || null;
|
|
2765
|
-
|
|
2766
|
-
let currentPos: number;
|
|
2767
|
-
|
|
2768
|
-
if (!startFragment) {
|
|
2769
|
-
currentPos = lookupEntryPosition ?? 0;
|
|
2770
|
-
} else {
|
|
2771
|
-
if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) {
|
|
2772
|
-
currentPos = startFragment.moofOffset + startFragment.moofSize;
|
|
2773
|
-
currentFragment = startFragment;
|
|
2774
|
-
} else {
|
|
2775
|
-
// Use the lookup entry
|
|
2776
|
-
currentPos = lookupEntryPosition;
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
while (true) {
|
|
2781
|
-
if (currentFragment) {
|
|
2782
|
-
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
|
2783
|
-
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
|
2784
|
-
// We're already past the upper bound, no need to keep searching
|
|
2785
|
-
break;
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
// Load the header
|
|
2790
|
-
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
|
|
2791
|
-
if (slice instanceof Promise) slice = await slice;
|
|
2792
|
-
if (!slice) break;
|
|
2793
|
-
|
|
2794
|
-
const boxStartPos = currentPos;
|
|
2795
|
-
const boxInfo = readBoxHeader(slice);
|
|
2796
|
-
if (!boxInfo) {
|
|
2797
|
-
break;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
if (boxInfo.name === 'moof') {
|
|
2801
|
-
currentFragment = await demuxer.readFragment(boxStartPos);
|
|
2802
|
-
const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment);
|
|
2803
|
-
if (correctSampleFound) {
|
|
2804
|
-
return this.fetchPacketInFragment(currentFragment, sampleIndex, options);
|
|
2805
|
-
}
|
|
2806
|
-
if (sampleIndex !== -1) {
|
|
2807
|
-
bestFragment = currentFragment;
|
|
2808
|
-
bestSampleIndex = sampleIndex;
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
currentPos = boxStartPos + boxInfo.totalSize;
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
// Catch faulty lookup table entries
|
|
2816
|
-
if (lookupEntry && (!bestFragment || bestFragment.moofOffset < lookupEntry.moofOffset)) {
|
|
2817
|
-
// The lookup table entry lied to us! We found a lookup entry but no fragment there that satisfied
|
|
2818
|
-
// the match. In this case, let's search again but using the lookup entry before that.
|
|
2819
|
-
const previousLookupEntry = this.internalTrack.fragmentLookupTable[lookupEntryIndex - 1];
|
|
2820
|
-
assert(!previousLookupEntry || previousLookupEntry.timestamp < lookupEntry.timestamp);
|
|
2821
|
-
|
|
2822
|
-
const newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity;
|
|
2823
|
-
return this.performFragmentedLookup(
|
|
2824
|
-
null,
|
|
2825
|
-
getMatchInFragment,
|
|
2826
|
-
newSearchTimestamp,
|
|
2827
|
-
latestTimestamp,
|
|
2828
|
-
options,
|
|
2829
|
-
);
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
if (bestFragment) {
|
|
2833
|
-
// If we finished looping but didn't find a perfect match, still return the best match we found
|
|
2834
|
-
return this.fetchPacketInFragment(bestFragment, bestSampleIndex, options);
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
return null;
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideoTrackBacking {
|
|
2842
|
-
override internalTrack: InternalVideoTrack;
|
|
2843
|
-
decoderConfigPromise: Promise<VideoDecoderConfig> | null = null;
|
|
2844
|
-
|
|
2845
|
-
constructor(internalTrack: InternalVideoTrack) {
|
|
2846
|
-
super(internalTrack);
|
|
2847
|
-
this.internalTrack = internalTrack;
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
override getCodec(): VideoCodec | null {
|
|
2851
|
-
return this.internalTrack.info.codec;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
getCodedWidth() {
|
|
2855
|
-
return this.internalTrack.info.width;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
getCodedHeight() {
|
|
2859
|
-
return this.internalTrack.info.height;
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
getRotation() {
|
|
2863
|
-
return this.internalTrack.rotation;
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
async getColorSpace(): Promise<VideoColorSpaceInit> {
|
|
2867
|
-
return {
|
|
2868
|
-
primaries: this.internalTrack.info.colorSpace?.primaries,
|
|
2869
|
-
transfer: this.internalTrack.info.colorSpace?.transfer,
|
|
2870
|
-
matrix: this.internalTrack.info.colorSpace?.matrix,
|
|
2871
|
-
fullRange: this.internalTrack.info.colorSpace?.fullRange,
|
|
2872
|
-
};
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
async canBeTransparent() {
|
|
2876
|
-
return false;
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
async getDecoderConfig(): Promise<VideoDecoderConfig | null> {
|
|
2880
|
-
if (!this.internalTrack.info.codec) {
|
|
2881
|
-
return null;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
return this.decoderConfigPromise ??= (async (): Promise<VideoDecoderConfig> => {
|
|
2885
|
-
if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) {
|
|
2886
|
-
const firstPacket = await this.getFirstPacket({});
|
|
2887
|
-
this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data);
|
|
2888
|
-
} else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) {
|
|
2889
|
-
const firstPacket = await this.getFirstPacket({});
|
|
2890
|
-
this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data);
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
return {
|
|
2894
|
-
codec: extractVideoCodecString(this.internalTrack.info),
|
|
2895
|
-
codedWidth: this.internalTrack.info.width,
|
|
2896
|
-
codedHeight: this.internalTrack.info.height,
|
|
2897
|
-
description: this.internalTrack.info.codecDescription ?? undefined,
|
|
2898
|
-
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
|
|
2899
|
-
};
|
|
2900
|
-
})();
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
class IsobmffAudioTrackBacking extends IsobmffTrackBacking implements InputAudioTrackBacking {
|
|
2905
|
-
override internalTrack: InternalAudioTrack;
|
|
2906
|
-
decoderConfig: AudioDecoderConfig | null = null;
|
|
2907
|
-
|
|
2908
|
-
constructor(internalTrack: InternalAudioTrack) {
|
|
2909
|
-
super(internalTrack);
|
|
2910
|
-
this.internalTrack = internalTrack;
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
override getCodec(): AudioCodec | null {
|
|
2914
|
-
return this.internalTrack.info.codec;
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
getNumberOfChannels() {
|
|
2918
|
-
return this.internalTrack.info.numberOfChannels;
|
|
2919
|
-
}
|
|
2920
|
-
|
|
2921
|
-
getSampleRate() {
|
|
2922
|
-
return this.internalTrack.info.sampleRate;
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
async getDecoderConfig(): Promise<AudioDecoderConfig | null> {
|
|
2926
|
-
if (!this.internalTrack.info.codec) {
|
|
2927
|
-
return null;
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
return this.decoderConfig ??= {
|
|
2931
|
-
codec: extractAudioCodecString(this.internalTrack.info),
|
|
2932
|
-
numberOfChannels: this.internalTrack.info.numberOfChannels,
|
|
2933
|
-
sampleRate: this.internalTrack.info.sampleRate,
|
|
2934
|
-
description: this.internalTrack.info.codecDescription ?? undefined,
|
|
2935
|
-
};
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
class IsobmffSubtitleTrackBacking extends IsobmffTrackBacking implements InputSubtitleTrackBacking {
|
|
2940
|
-
override internalTrack: InternalSubtitleTrack;
|
|
2941
|
-
|
|
2942
|
-
constructor(internalTrack: InternalSubtitleTrack) {
|
|
2943
|
-
super(internalTrack);
|
|
2944
|
-
this.internalTrack = internalTrack;
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
override getCodec(): SubtitleCodec | null {
|
|
2948
|
-
return this.internalTrack.info.codec;
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
getCodecPrivate(): string | null {
|
|
2952
|
-
return this.internalTrack.info.codecPrivateText;
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
async *getCues(): AsyncGenerator<SubtitleCue> {
|
|
2956
|
-
// Use the existing packet reading infrastructure
|
|
2957
|
-
let packet = await this.getFirstPacket({});
|
|
2958
|
-
|
|
2959
|
-
while (packet) {
|
|
2960
|
-
// Parse WebVTT box structure or plain text
|
|
2961
|
-
let text = '';
|
|
2962
|
-
|
|
2963
|
-
if (this.internalTrack.info.codec === 'webvtt') {
|
|
2964
|
-
// WebVTT in MP4 is stored as a series of boxes
|
|
2965
|
-
const dataBytes = new Uint8Array(packet.data);
|
|
2966
|
-
const dataSlice = new FileSlice(
|
|
2967
|
-
dataBytes,
|
|
2968
|
-
new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength),
|
|
2969
|
-
0,
|
|
2970
|
-
0,
|
|
2971
|
-
dataBytes.length,
|
|
2972
|
-
);
|
|
2973
|
-
|
|
2974
|
-
while (dataSlice.remainingLength > 0) {
|
|
2975
|
-
const boxHeader = readBoxHeader(dataSlice);
|
|
2976
|
-
if (!boxHeader) break;
|
|
2977
|
-
|
|
2978
|
-
if (boxHeader.name === 'vttc') {
|
|
2979
|
-
// WebVTT cue box, contains the actual text
|
|
2980
|
-
// Skip to content and continue parsing nested boxes
|
|
2981
|
-
const vttcEnd = dataSlice.filePos + boxHeader.contentSize;
|
|
2982
|
-
|
|
2983
|
-
while (dataSlice.filePos < vttcEnd && dataSlice.remainingLength > 0) {
|
|
2984
|
-
const innerBox = readBoxHeader(dataSlice);
|
|
2985
|
-
if (!innerBox) break;
|
|
2986
|
-
|
|
2987
|
-
if (innerBox.name === 'payl') {
|
|
2988
|
-
// Payload box contains the actual text
|
|
2989
|
-
const textBytes = readBytes(dataSlice, innerBox.contentSize);
|
|
2990
|
-
const decoder = new TextDecoder('utf-8');
|
|
2991
|
-
text += decoder.decode(textBytes);
|
|
2992
|
-
} else {
|
|
2993
|
-
// Skip unknown boxes
|
|
2994
|
-
dataSlice.skip(innerBox.contentSize);
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
} else if (boxHeader.name === 'vtte') {
|
|
2998
|
-
// Empty cue box, skip it
|
|
2999
|
-
dataSlice.skip(boxHeader.contentSize);
|
|
3000
|
-
} else {
|
|
3001
|
-
// Skip unknown boxes
|
|
3002
|
-
dataSlice.skip(boxHeader.contentSize);
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
} else {
|
|
3006
|
-
// Plain text for other subtitle formats (tx3g, etc.)
|
|
3007
|
-
const decoder = new TextDecoder('utf-8');
|
|
3008
|
-
text = decoder.decode(packet.data);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
if (text) {
|
|
3012
|
-
// Only yield cues with actual text content
|
|
3013
|
-
yield {
|
|
3014
|
-
timestamp: packet.timestamp,
|
|
3015
|
-
duration: packet.duration,
|
|
3016
|
-
text,
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
packet = await this.getNextPacket(packet, {});
|
|
3021
|
-
}
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
const getSampleIndexForTimestamp = (sampleTable: SampleTable, timescaleUnits: number) => {
|
|
3026
|
-
if (sampleTable.presentationTimestamps) {
|
|
3027
|
-
const index = binarySearchLessOrEqual(
|
|
3028
|
-
sampleTable.presentationTimestamps,
|
|
3029
|
-
timescaleUnits,
|
|
3030
|
-
x => x.presentationTimestamp,
|
|
3031
|
-
);
|
|
3032
|
-
if (index === -1) {
|
|
3033
|
-
return -1;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
return sampleTable.presentationTimestamps[index]!.sampleIndex;
|
|
3037
|
-
} else {
|
|
3038
|
-
const index = binarySearchLessOrEqual(
|
|
3039
|
-
sampleTable.sampleTimingEntries,
|
|
3040
|
-
timescaleUnits,
|
|
3041
|
-
x => x.startDecodeTimestamp,
|
|
3042
|
-
);
|
|
3043
|
-
if (index === -1) {
|
|
3044
|
-
return -1;
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
const entry = sampleTable.sampleTimingEntries[index]!;
|
|
3048
|
-
return entry.startIndex
|
|
3049
|
-
+ Math.min(Math.floor((timescaleUnits - entry.startDecodeTimestamp) / entry.delta), entry.count - 1);
|
|
3050
|
-
}
|
|
3051
|
-
};
|
|
3052
|
-
|
|
3053
|
-
type SampleInfo = {
|
|
3054
|
-
presentationTimestamp: number;
|
|
3055
|
-
duration: number;
|
|
3056
|
-
sampleOffset: number;
|
|
3057
|
-
sampleSize: number;
|
|
3058
|
-
chunkOffset: number;
|
|
3059
|
-
chunkSize: number;
|
|
3060
|
-
isKeyFrame: boolean;
|
|
3061
|
-
};
|
|
3062
|
-
|
|
3063
|
-
const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInfo | null => {
|
|
3064
|
-
const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, x => x.startIndex);
|
|
3065
|
-
const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex];
|
|
3066
|
-
if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) {
|
|
3067
|
-
return null;
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
const decodeTimestamp = timingEntry.startDecodeTimestamp
|
|
3071
|
-
+ (sampleIndex - timingEntry.startIndex) * timingEntry.delta;
|
|
3072
|
-
let presentationTimestamp = decodeTimestamp;
|
|
3073
|
-
const offsetEntryIndex = binarySearchLessOrEqual(
|
|
3074
|
-
sampleTable.sampleCompositionTimeOffsets,
|
|
3075
|
-
sampleIndex,
|
|
3076
|
-
x => x.startIndex,
|
|
3077
|
-
);
|
|
3078
|
-
const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex];
|
|
3079
|
-
if (offsetEntry && sampleIndex - offsetEntry.startIndex < offsetEntry.count) {
|
|
3080
|
-
presentationTimestamp += offsetEntry.offset;
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)]!;
|
|
3084
|
-
const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, x => x.startSampleIndex);
|
|
3085
|
-
const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex];
|
|
3086
|
-
assert(chunkEntry);
|
|
3087
|
-
|
|
3088
|
-
const chunkIndex = chunkEntry.startChunkIndex
|
|
3089
|
-
+ Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk);
|
|
3090
|
-
const chunkOffset = sampleTable.chunkOffsets[chunkIndex]!;
|
|
3091
|
-
|
|
3092
|
-
const startSampleIndexOfChunk = chunkEntry.startSampleIndex
|
|
3093
|
-
+ (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
|
|
3094
|
-
let chunkSize = 0;
|
|
3095
|
-
let sampleOffset = chunkOffset;
|
|
3096
|
-
|
|
3097
|
-
if (sampleTable.sampleSizes.length === 1) {
|
|
3098
|
-
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
|
|
3099
|
-
chunkSize += sampleSize * chunkEntry.samplesPerChunk;
|
|
3100
|
-
} else {
|
|
3101
|
-
for (let i = startSampleIndexOfChunk; i < startSampleIndexOfChunk + chunkEntry.samplesPerChunk; i++) {
|
|
3102
|
-
const sampleSize = sampleTable.sampleSizes[i]!;
|
|
3103
|
-
|
|
3104
|
-
if (i < sampleIndex) {
|
|
3105
|
-
sampleOffset += sampleSize;
|
|
3106
|
-
}
|
|
3107
|
-
chunkSize += sampleSize;
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
let duration = timingEntry.delta;
|
|
3112
|
-
if (sampleTable.presentationTimestamps) {
|
|
3113
|
-
// In order to accurately compute the duration, we need to take the duration to the next sample in presentation
|
|
3114
|
-
// order, not in decode order
|
|
3115
|
-
const presentationIndex = sampleTable.presentationTimestampIndexMap![sampleIndex];
|
|
3116
|
-
assert(presentationIndex !== undefined);
|
|
3117
|
-
|
|
3118
|
-
if (presentationIndex < sampleTable.presentationTimestamps.length - 1) {
|
|
3119
|
-
const nextEntry = sampleTable.presentationTimestamps[presentationIndex + 1]!;
|
|
3120
|
-
const nextPresentationTimestamp = nextEntry.presentationTimestamp;
|
|
3121
|
-
duration = nextPresentationTimestamp - presentationTimestamp;
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
return {
|
|
3126
|
-
presentationTimestamp,
|
|
3127
|
-
duration,
|
|
3128
|
-
sampleOffset,
|
|
3129
|
-
sampleSize,
|
|
3130
|
-
chunkOffset,
|
|
3131
|
-
chunkSize,
|
|
3132
|
-
isKeyFrame: sampleTable.keySampleIndices
|
|
3133
|
-
? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, x => x) !== -1
|
|
3134
|
-
: true,
|
|
3135
|
-
};
|
|
3136
|
-
};
|
|
3137
|
-
|
|
3138
|
-
const getRelevantKeyframeIndexForSample = (sampleTable: SampleTable, sampleIndex: number) => {
|
|
3139
|
-
if (!sampleTable.keySampleIndices) {
|
|
3140
|
-
return sampleIndex;
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, x => x);
|
|
3144
|
-
return sampleTable.keySampleIndices[index] ?? -1;
|
|
3145
|
-
};
|
|
3146
|
-
|
|
3147
|
-
const getNextKeyframeIndexForSample = (sampleTable: SampleTable, sampleIndex: number) => {
|
|
3148
|
-
if (!sampleTable.keySampleIndices) {
|
|
3149
|
-
return sampleIndex + 1;
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, x => x);
|
|
3153
|
-
return sampleTable.keySampleIndices[index + 1] ?? -1;
|
|
3154
|
-
};
|
|
3155
|
-
|
|
3156
|
-
const offsetFragmentTrackDataByTimestamp = (trackData: FragmentTrackData, timestamp: number) => {
|
|
3157
|
-
trackData.startTimestamp += timestamp;
|
|
3158
|
-
trackData.endTimestamp += timestamp;
|
|
3159
|
-
|
|
3160
|
-
for (const sample of trackData.samples) {
|
|
3161
|
-
sample.presentationTimestamp += timestamp;
|
|
3162
|
-
}
|
|
3163
|
-
for (const entry of trackData.presentationTimestamps) {
|
|
3164
|
-
entry.presentationTimestamp += timestamp;
|
|
3165
|
-
}
|
|
3166
|
-
};
|
|
3167
|
-
|
|
3168
|
-
/** Extracts the rotation component from a transformation matrix, in degrees. */
|
|
3169
|
-
const extractRotationFromMatrix = (matrix: TransformationMatrix) => {
|
|
3170
|
-
const [m11, , , m21] = matrix;
|
|
3171
|
-
|
|
3172
|
-
const scaleX = Math.hypot(m11, m21);
|
|
3173
|
-
|
|
3174
|
-
const cosTheta = m11 / scaleX;
|
|
3175
|
-
const sinTheta = m21 / scaleX;
|
|
3176
|
-
|
|
3177
|
-
// Invert the rotation because matrices are post-multiplied in ISOBMFF
|
|
3178
|
-
const result = -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI);
|
|
3179
|
-
|
|
3180
|
-
if (!Number.isFinite(result)) {
|
|
3181
|
-
// Can happen if the entire matrix is 0, for example
|
|
3182
|
-
return 0;
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
return result;
|
|
3186
|
-
};
|
|
3187
|
-
|
|
3188
|
-
const sampleTableIsEmpty = (sampleTable: SampleTable) => {
|
|
3189
|
-
return sampleTable.sampleSizes.length === 0;
|
|
3190
|
-
};
|