@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,2481 +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
|
-
extractAv1CodecInfoFromPacket,
|
|
11
|
-
extractAvcDecoderConfigurationRecord,
|
|
12
|
-
extractHevcDecoderConfigurationRecord,
|
|
13
|
-
extractVp9CodecInfoFromPacket,
|
|
14
|
-
} from '../codec-data';
|
|
15
|
-
import {
|
|
16
|
-
AacCodecInfo,
|
|
17
|
-
AudioCodec,
|
|
18
|
-
extractAudioCodecString,
|
|
19
|
-
extractVideoCodecString,
|
|
20
|
-
MediaCodec,
|
|
21
|
-
OPUS_SAMPLE_RATE,
|
|
22
|
-
SubtitleCodec,
|
|
23
|
-
VideoCodec,
|
|
24
|
-
} from '../codec';
|
|
25
|
-
import { Demuxer } from '../demuxer';
|
|
26
|
-
import { Input } from '../input';
|
|
27
|
-
import {
|
|
28
|
-
InputAudioTrack,
|
|
29
|
-
InputAudioTrackBacking,
|
|
30
|
-
InputSubtitleTrack,
|
|
31
|
-
InputSubtitleTrackBacking,
|
|
32
|
-
InputTrack,
|
|
33
|
-
InputTrackBacking,
|
|
34
|
-
InputVideoTrack,
|
|
35
|
-
InputVideoTrackBacking,
|
|
36
|
-
} from '../input-track';
|
|
37
|
-
import { AttachedFile, DEFAULT_TRACK_DISPOSITION, MetadataTags, TrackDisposition } from '../metadata';
|
|
38
|
-
import { PacketRetrievalOptions } from '../media-sink';
|
|
39
|
-
import {
|
|
40
|
-
assert,
|
|
41
|
-
binarySearchLessOrEqual,
|
|
42
|
-
COLOR_PRIMARIES_MAP_INVERSE,
|
|
43
|
-
findLastIndex,
|
|
44
|
-
isIso639Dash2LanguageCode,
|
|
45
|
-
last,
|
|
46
|
-
MATRIX_COEFFICIENTS_MAP_INVERSE,
|
|
47
|
-
normalizeRotation,
|
|
48
|
-
Rotation,
|
|
49
|
-
roundIfAlmostInteger,
|
|
50
|
-
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
|
51
|
-
UNDETERMINED_LANGUAGE,
|
|
52
|
-
} from '../misc';
|
|
53
|
-
import { EncodedPacket, EncodedPacketSideData, PLACEHOLDER_DATA } from '../packet';
|
|
54
|
-
import { SubtitleCue } from '../subtitles';
|
|
55
|
-
import {
|
|
56
|
-
assertDefinedSize,
|
|
57
|
-
CODEC_STRING_MAP,
|
|
58
|
-
EBMLId,
|
|
59
|
-
LEVEL_0_AND_1_EBML_IDS,
|
|
60
|
-
LEVEL_1_EBML_IDS,
|
|
61
|
-
MAX_HEADER_SIZE,
|
|
62
|
-
MIN_HEADER_SIZE,
|
|
63
|
-
readAsciiString,
|
|
64
|
-
readUnicodeString,
|
|
65
|
-
readElementHeader,
|
|
66
|
-
readElementId,
|
|
67
|
-
readFloat,
|
|
68
|
-
readUnsignedInt,
|
|
69
|
-
readVarInt,
|
|
70
|
-
resync,
|
|
71
|
-
searchForNextElementId,
|
|
72
|
-
readUnsignedBigInt,
|
|
73
|
-
} from './ebml';
|
|
74
|
-
import { buildMatroskaMimeType } from './matroska-misc';
|
|
75
|
-
import { FileSlice, readBytes, Reader, readI16Be, readU8 } from '../reader';
|
|
76
|
-
|
|
77
|
-
type Segment = {
|
|
78
|
-
seekHeadSeen: boolean;
|
|
79
|
-
infoSeen: boolean;
|
|
80
|
-
tracksSeen: boolean;
|
|
81
|
-
cuesSeen: boolean;
|
|
82
|
-
attachmentsSeen: boolean;
|
|
83
|
-
tagsSeen: boolean;
|
|
84
|
-
|
|
85
|
-
timestampScale: number;
|
|
86
|
-
timestampFactor: number;
|
|
87
|
-
duration: number;
|
|
88
|
-
seekEntries: SeekEntry[];
|
|
89
|
-
tracks: InternalTrack[];
|
|
90
|
-
cuePoints: CuePoint[];
|
|
91
|
-
|
|
92
|
-
dataStartPos: number;
|
|
93
|
-
elementEndPos: number | null;
|
|
94
|
-
clusterSeekStartPos: number;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Caches the last cluster that was read. Based on the assumption that there will be multiple reads to the
|
|
98
|
-
* same cluster in quick succession.
|
|
99
|
-
*/
|
|
100
|
-
lastReadCluster: Cluster | null;
|
|
101
|
-
|
|
102
|
-
metadataTags: MetadataTags;
|
|
103
|
-
metadataTagsCollected: boolean;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
type SeekEntry = {
|
|
107
|
-
id: number;
|
|
108
|
-
segmentPosition: number;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
type Cluster = {
|
|
112
|
-
segment: Segment;
|
|
113
|
-
elementStartPos: number;
|
|
114
|
-
elementEndPos: number;
|
|
115
|
-
dataStartPos: number;
|
|
116
|
-
timestamp: number;
|
|
117
|
-
trackData: Map<number, ClusterTrackData>;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
type ClusterTrackData = {
|
|
121
|
-
track: InternalTrack;
|
|
122
|
-
startTimestamp: number;
|
|
123
|
-
endTimestamp: number;
|
|
124
|
-
firstKeyFrameTimestamp: number | null;
|
|
125
|
-
blocks: ClusterBlock[];
|
|
126
|
-
presentationTimestamps: {
|
|
127
|
-
timestamp: number;
|
|
128
|
-
blockIndex: number;
|
|
129
|
-
}[];
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
enum BlockLacing {
|
|
133
|
-
None,
|
|
134
|
-
Xiph,
|
|
135
|
-
FixedSize,
|
|
136
|
-
Ebml,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
type ClusterBlock = {
|
|
140
|
-
timestamp: number;
|
|
141
|
-
duration: number;
|
|
142
|
-
isKeyFrame: boolean;
|
|
143
|
-
data: Uint8Array;
|
|
144
|
-
lacing: BlockLacing;
|
|
145
|
-
decoded: boolean;
|
|
146
|
-
mainAdditional: Uint8Array | null;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
type CuePoint = {
|
|
150
|
-
time: number;
|
|
151
|
-
trackId: number;
|
|
152
|
-
clusterPosition: number;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
enum ContentEncodingScope {
|
|
156
|
-
Block = 1,
|
|
157
|
-
Private = 2,
|
|
158
|
-
Next = 4,
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
enum ContentCompAlgo {
|
|
162
|
-
Zlib,
|
|
163
|
-
Bzlib,
|
|
164
|
-
lzo1x,
|
|
165
|
-
HeaderStripping,
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
type DecodingInstruction = {
|
|
169
|
-
order: number;
|
|
170
|
-
scope: ContentEncodingScope;
|
|
171
|
-
data: {
|
|
172
|
-
type: 'decompress';
|
|
173
|
-
algorithm: ContentCompAlgo | null;
|
|
174
|
-
settings: Uint8Array | null;
|
|
175
|
-
} | {
|
|
176
|
-
type: 'decrypt';
|
|
177
|
-
// Don't store more yet since this operation is unsupported
|
|
178
|
-
} | null;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
type InternalTrack = {
|
|
182
|
-
id: number;
|
|
183
|
-
demuxer: MatroskaDemuxer;
|
|
184
|
-
segment: Segment;
|
|
185
|
-
/**
|
|
186
|
-
* List of all encountered cluster offsets alongside their timestamps. This list never gets truncated, but memory
|
|
187
|
-
* consumption should be negligible.
|
|
188
|
-
*/
|
|
189
|
-
clusterPositionCache: {
|
|
190
|
-
elementStartPos: number;
|
|
191
|
-
startTimestamp: number;
|
|
192
|
-
}[];
|
|
193
|
-
cuePoints: CuePoint[];
|
|
194
|
-
|
|
195
|
-
disposition: TrackDisposition;
|
|
196
|
-
inputTrack: InputTrack | null;
|
|
197
|
-
codecId: string | null;
|
|
198
|
-
codecPrivate: Uint8Array | null;
|
|
199
|
-
defaultDuration: number | null;
|
|
200
|
-
name: string | null;
|
|
201
|
-
languageCode: string;
|
|
202
|
-
decodingInstructions: DecodingInstruction[];
|
|
203
|
-
|
|
204
|
-
info:
|
|
205
|
-
| null
|
|
206
|
-
| {
|
|
207
|
-
type: 'video';
|
|
208
|
-
width: number;
|
|
209
|
-
height: number;
|
|
210
|
-
rotation: Rotation;
|
|
211
|
-
codec: VideoCodec | null;
|
|
212
|
-
codecDescription: Uint8Array | null;
|
|
213
|
-
colorSpace: VideoColorSpaceInit | null;
|
|
214
|
-
alphaMode: boolean;
|
|
215
|
-
}
|
|
216
|
-
| {
|
|
217
|
-
type: 'audio';
|
|
218
|
-
numberOfChannels: number;
|
|
219
|
-
sampleRate: number;
|
|
220
|
-
bitDepth: number;
|
|
221
|
-
codec: AudioCodec | null;
|
|
222
|
-
codecDescription: Uint8Array | null;
|
|
223
|
-
aacCodecInfo: AacCodecInfo | null;
|
|
224
|
-
}
|
|
225
|
-
| {
|
|
226
|
-
type: 'subtitle';
|
|
227
|
-
codec: SubtitleCodec | null;
|
|
228
|
-
codecPrivateText: string | null;
|
|
229
|
-
};
|
|
230
|
-
};
|
|
231
|
-
type InternalVideoTrack = InternalTrack & { info: { type: 'video' } };
|
|
232
|
-
type InternalAudioTrack = InternalTrack & { info: { type: 'audio' } };
|
|
233
|
-
type InternalSubtitleTrack = InternalTrack & { info: { type: 'subtitle' } };
|
|
234
|
-
|
|
235
|
-
const METADATA_ELEMENTS = [
|
|
236
|
-
{ id: EBMLId.SeekHead, flag: 'seekHeadSeen' },
|
|
237
|
-
{ id: EBMLId.Info, flag: 'infoSeen' },
|
|
238
|
-
{ id: EBMLId.Tracks, flag: 'tracksSeen' },
|
|
239
|
-
{ id: EBMLId.Cues, flag: 'cuesSeen' },
|
|
240
|
-
] as const;
|
|
241
|
-
const MAX_RESYNC_LENGTH = /* #__PURE__ */ 10 * 2 ** 20; // 10 MiB
|
|
242
|
-
|
|
243
|
-
export class MatroskaDemuxer extends Demuxer {
|
|
244
|
-
reader: Reader;
|
|
245
|
-
|
|
246
|
-
readMetadataPromise: Promise<void> | null = null;
|
|
247
|
-
|
|
248
|
-
segments: Segment[] = [];
|
|
249
|
-
currentSegment: Segment | null = null;
|
|
250
|
-
currentTrack: InternalTrack | null = null;
|
|
251
|
-
currentCluster: Cluster | null = null;
|
|
252
|
-
currentBlock: ClusterBlock | null = null;
|
|
253
|
-
currentBlockAdditional: {
|
|
254
|
-
addId: number;
|
|
255
|
-
data: Uint8Array | null;
|
|
256
|
-
} | null = null;
|
|
257
|
-
|
|
258
|
-
currentCueTime: number | null = null;
|
|
259
|
-
currentDecodingInstruction: DecodingInstruction | null = null;
|
|
260
|
-
currentTagTargetIsMovie: boolean = true;
|
|
261
|
-
currentSimpleTagName: string | null = null;
|
|
262
|
-
currentAttachedFile: {
|
|
263
|
-
fileUid: bigint | null;
|
|
264
|
-
fileName: string | null;
|
|
265
|
-
fileMediaType: string | null;
|
|
266
|
-
fileData: Uint8Array | null;
|
|
267
|
-
fileDescription: string | null;
|
|
268
|
-
} | null = null;
|
|
269
|
-
|
|
270
|
-
isWebM = false;
|
|
271
|
-
|
|
272
|
-
constructor(input: Input) {
|
|
273
|
-
super(input);
|
|
274
|
-
|
|
275
|
-
this.reader = input._reader;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
override async computeDuration() {
|
|
279
|
-
const tracks = await this.getTracks();
|
|
280
|
-
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
|
|
281
|
-
return Math.max(0, ...trackDurations);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async getTracks() {
|
|
285
|
-
await this.readMetadata();
|
|
286
|
-
return this.segments.flatMap(segment => segment.tracks.map(track => track.inputTrack!));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
override async getMimeType() {
|
|
290
|
-
await this.readMetadata();
|
|
291
|
-
|
|
292
|
-
const tracks = await this.getTracks();
|
|
293
|
-
const codecStrings = await Promise.all(tracks.map(x => x.getCodecParameterString()));
|
|
294
|
-
|
|
295
|
-
return buildMatroskaMimeType({
|
|
296
|
-
isWebM: this.isWebM,
|
|
297
|
-
hasVideo: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'video')),
|
|
298
|
-
hasAudio: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'audio')),
|
|
299
|
-
codecStrings: codecStrings.filter(Boolean) as string[],
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async getMetadataTags() {
|
|
304
|
-
await this.readMetadata();
|
|
305
|
-
|
|
306
|
-
// Load metadata tags from each segment lazily (only once)
|
|
307
|
-
for (const segment of this.segments) {
|
|
308
|
-
if (!segment.metadataTagsCollected) {
|
|
309
|
-
if (this.reader.fileSize !== null) {
|
|
310
|
-
await this.loadSegmentMetadata(segment);
|
|
311
|
-
} else {
|
|
312
|
-
// The seeking would be too crazy, let's not
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
segment.metadataTagsCollected = true;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// This is kinda handwavy, and how we handle multiple segments isn't suuuuper well-defined anyway; so we just
|
|
320
|
-
// shallow-merge metadata tags from all (usually just one) segments.
|
|
321
|
-
let metadataTags: MetadataTags = {};
|
|
322
|
-
for (const segment of this.segments) {
|
|
323
|
-
metadataTags = { ...metadataTags, ...segment.metadataTags };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return metadataTags;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
readMetadata() {
|
|
330
|
-
return this.readMetadataPromise ??= (async () => {
|
|
331
|
-
let currentPos = 0;
|
|
332
|
-
|
|
333
|
-
// Loop over all top-level elements in the file
|
|
334
|
-
while (true) {
|
|
335
|
-
let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
|
336
|
-
if (slice instanceof Promise) slice = await slice;
|
|
337
|
-
if (!slice) break;
|
|
338
|
-
|
|
339
|
-
const header = readElementHeader(slice);
|
|
340
|
-
if (!header) {
|
|
341
|
-
break; // Zero padding at the end of the file triggers this, for example
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const id = header.id;
|
|
345
|
-
let size = header.size;
|
|
346
|
-
const dataStartPos = slice.filePos;
|
|
347
|
-
|
|
348
|
-
if (id === EBMLId.EBML) {
|
|
349
|
-
assertDefinedSize(size);
|
|
350
|
-
|
|
351
|
-
let slice = this.reader.requestSlice(dataStartPos, size);
|
|
352
|
-
if (slice instanceof Promise) slice = await slice;
|
|
353
|
-
if (!slice) break;
|
|
354
|
-
|
|
355
|
-
this.readContiguousElements(slice);
|
|
356
|
-
} else if (id === EBMLId.Segment) { // Segment found!
|
|
357
|
-
await this.readSegment(dataStartPos, size);
|
|
358
|
-
|
|
359
|
-
if (size === null) {
|
|
360
|
-
// Segment sizes can be undefined (common in livestreamed files), so assume this is the last
|
|
361
|
-
// and only segment
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (this.reader.fileSize === null) {
|
|
366
|
-
break; // Stop at the first segment
|
|
367
|
-
}
|
|
368
|
-
} else if (id === EBMLId.Cluster) {
|
|
369
|
-
if (this.reader.fileSize === null) {
|
|
370
|
-
break; // Shouldn't be reached anyway, since we stop at the first segment
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Clusters are not a top-level element in Matroska, but some files contain a Segment whose size
|
|
374
|
-
// doesn't contain any of the clusters that follow it. In the case, we apply the following logic: if
|
|
375
|
-
// we find a top-level cluster, attribute it to the previous segment.
|
|
376
|
-
|
|
377
|
-
if (size === null) {
|
|
378
|
-
// Just in case this is one of those weird sizeless clusters, let's do our best and still try to
|
|
379
|
-
// determine its size.
|
|
380
|
-
const nextElementPos = await searchForNextElementId(
|
|
381
|
-
this.reader,
|
|
382
|
-
dataStartPos,
|
|
383
|
-
LEVEL_0_AND_1_EBML_IDS,
|
|
384
|
-
this.reader.fileSize,
|
|
385
|
-
);
|
|
386
|
-
size = nextElementPos.pos - dataStartPos;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const lastSegment = last(this.segments);
|
|
390
|
-
if (lastSegment) {
|
|
391
|
-
// Extend the previous segment's size
|
|
392
|
-
lastSegment.elementEndPos = dataStartPos + size;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
assertDefinedSize(size);
|
|
397
|
-
currentPos = dataStartPos + size;
|
|
398
|
-
}
|
|
399
|
-
})();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async readSegment(segmentDataStart: number, dataSize: number | null) {
|
|
403
|
-
this.currentSegment = {
|
|
404
|
-
seekHeadSeen: false,
|
|
405
|
-
infoSeen: false,
|
|
406
|
-
tracksSeen: false,
|
|
407
|
-
cuesSeen: false,
|
|
408
|
-
tagsSeen: false,
|
|
409
|
-
attachmentsSeen: false,
|
|
410
|
-
|
|
411
|
-
timestampScale: -1,
|
|
412
|
-
timestampFactor: -1,
|
|
413
|
-
duration: -1,
|
|
414
|
-
seekEntries: [],
|
|
415
|
-
tracks: [],
|
|
416
|
-
cuePoints: [],
|
|
417
|
-
|
|
418
|
-
dataStartPos: segmentDataStart,
|
|
419
|
-
elementEndPos: dataSize === null
|
|
420
|
-
? null // Assume it goes until the end of the file
|
|
421
|
-
: segmentDataStart + dataSize,
|
|
422
|
-
clusterSeekStartPos: segmentDataStart,
|
|
423
|
-
|
|
424
|
-
lastReadCluster: null,
|
|
425
|
-
|
|
426
|
-
metadataTags: {},
|
|
427
|
-
metadataTagsCollected: false,
|
|
428
|
-
};
|
|
429
|
-
this.segments.push(this.currentSegment);
|
|
430
|
-
|
|
431
|
-
let currentPos = segmentDataStart;
|
|
432
|
-
|
|
433
|
-
while (this.currentSegment.elementEndPos === null || currentPos < this.currentSegment.elementEndPos) {
|
|
434
|
-
let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
|
435
|
-
if (slice instanceof Promise) slice = await slice;
|
|
436
|
-
if (!slice) break;
|
|
437
|
-
|
|
438
|
-
const elementStartPos = currentPos;
|
|
439
|
-
const header = readElementHeader(slice);
|
|
440
|
-
|
|
441
|
-
if (!header || (!LEVEL_1_EBML_IDS.includes(header.id) && header.id !== EBMLId.Void)) {
|
|
442
|
-
// Potential junk. Let's try to resync
|
|
443
|
-
|
|
444
|
-
const nextPos = await resync(
|
|
445
|
-
this.reader,
|
|
446
|
-
elementStartPos,
|
|
447
|
-
LEVEL_1_EBML_IDS,
|
|
448
|
-
Math.min(this.currentSegment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH),
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
if (nextPos) {
|
|
452
|
-
currentPos = nextPos;
|
|
453
|
-
continue;
|
|
454
|
-
} else {
|
|
455
|
-
break; // Resync failed
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const { id, size } = header;
|
|
460
|
-
const dataStartPos = slice.filePos;
|
|
461
|
-
|
|
462
|
-
const metadataElementIndex = METADATA_ELEMENTS.findIndex(x => x.id === id);
|
|
463
|
-
if (metadataElementIndex !== -1) {
|
|
464
|
-
const field = METADATA_ELEMENTS[metadataElementIndex]!.flag;
|
|
465
|
-
this.currentSegment[field] = true;
|
|
466
|
-
|
|
467
|
-
assertDefinedSize(size);
|
|
468
|
-
|
|
469
|
-
let slice = this.reader.requestSlice(dataStartPos, size);
|
|
470
|
-
if (slice instanceof Promise) slice = await slice;
|
|
471
|
-
|
|
472
|
-
if (slice) {
|
|
473
|
-
this.readContiguousElements(slice);
|
|
474
|
-
}
|
|
475
|
-
} else if (id === EBMLId.Tags || id === EBMLId.Attachments) {
|
|
476
|
-
// Metadata found at the beginning of the segment, great, let's parse it
|
|
477
|
-
if (id === EBMLId.Tags) {
|
|
478
|
-
this.currentSegment.tagsSeen = true;
|
|
479
|
-
} else {
|
|
480
|
-
this.currentSegment.attachmentsSeen = true;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
assertDefinedSize(size);
|
|
484
|
-
|
|
485
|
-
let slice = this.reader.requestSlice(dataStartPos, size);
|
|
486
|
-
if (slice instanceof Promise) slice = await slice;
|
|
487
|
-
|
|
488
|
-
if (slice) {
|
|
489
|
-
this.readContiguousElements(slice);
|
|
490
|
-
}
|
|
491
|
-
} else if (id === EBMLId.Cluster) {
|
|
492
|
-
this.currentSegment.clusterSeekStartPos = elementStartPos;
|
|
493
|
-
break; // Stop at the first cluster
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (size === null) {
|
|
497
|
-
break;
|
|
498
|
-
} else {
|
|
499
|
-
currentPos = dataStartPos + size;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Sort the seek entries by file position so reading them exhibits a sequential pattern
|
|
504
|
-
this.currentSegment.seekEntries.sort((a, b) => a.segmentPosition - b.segmentPosition);
|
|
505
|
-
|
|
506
|
-
if (this.reader.fileSize !== null) {
|
|
507
|
-
// Use the seek head to read missing metadata elements
|
|
508
|
-
for (const seekEntry of this.currentSegment.seekEntries) {
|
|
509
|
-
const target = METADATA_ELEMENTS.find(x => x.id === seekEntry.id);
|
|
510
|
-
if (!target) {
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (this.currentSegment[target.flag]) continue;
|
|
515
|
-
|
|
516
|
-
let slice = this.reader.requestSliceRange(
|
|
517
|
-
segmentDataStart + seekEntry.segmentPosition,
|
|
518
|
-
MIN_HEADER_SIZE,
|
|
519
|
-
MAX_HEADER_SIZE,
|
|
520
|
-
);
|
|
521
|
-
if (slice instanceof Promise) slice = await slice;
|
|
522
|
-
if (!slice) continue;
|
|
523
|
-
|
|
524
|
-
const header = readElementHeader(slice);
|
|
525
|
-
if (!header) continue;
|
|
526
|
-
|
|
527
|
-
const { id, size } = header;
|
|
528
|
-
if (id !== target.id) continue;
|
|
529
|
-
|
|
530
|
-
assertDefinedSize(size);
|
|
531
|
-
|
|
532
|
-
this.currentSegment[target.flag] = true;
|
|
533
|
-
|
|
534
|
-
let dataSlice = this.reader.requestSlice(slice.filePos, size);
|
|
535
|
-
if (dataSlice instanceof Promise) dataSlice = await dataSlice;
|
|
536
|
-
if (!dataSlice) continue;
|
|
537
|
-
|
|
538
|
-
this.readContiguousElements(dataSlice);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (this.currentSegment.timestampScale === -1) {
|
|
543
|
-
// TimestampScale element is missing. Technically an invalid file, but let's default to the typical value,
|
|
544
|
-
// which is 1e6.
|
|
545
|
-
this.currentSegment.timestampScale = 1e6;
|
|
546
|
-
this.currentSegment.timestampFactor = 1e9 / 1e6;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Put default tracks first
|
|
550
|
-
this.currentSegment.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
|
|
551
|
-
|
|
552
|
-
// Now, let's distribute the cue points to the tracks
|
|
553
|
-
const idToTrack = new Map(this.currentSegment.tracks.map(x => [x.id, x]));
|
|
554
|
-
|
|
555
|
-
// Assign cue points to their respective tracks
|
|
556
|
-
for (const cuePoint of this.currentSegment.cuePoints) {
|
|
557
|
-
const track = idToTrack.get(cuePoint.trackId);
|
|
558
|
-
if (track) {
|
|
559
|
-
track.cuePoints.push(cuePoint);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
for (const track of this.currentSegment.tracks) {
|
|
564
|
-
// Sort cue points by time
|
|
565
|
-
track.cuePoints.sort((a, b) => a.time - b.time);
|
|
566
|
-
|
|
567
|
-
// Remove multiple cue points for the same time
|
|
568
|
-
for (let i = 0; i < track.cuePoints.length - 1; i++) {
|
|
569
|
-
const cuePoint1 = track.cuePoints[i]!;
|
|
570
|
-
const cuePoint2 = track.cuePoints[i + 1]!;
|
|
571
|
-
|
|
572
|
-
if (cuePoint1.time === cuePoint2.time) {
|
|
573
|
-
track.cuePoints.splice(i + 1, 1);
|
|
574
|
-
i--;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
let trackWithMostCuePoints: InternalTrack | null = null;
|
|
580
|
-
let maxCuePointCount = -Infinity;
|
|
581
|
-
for (const track of this.currentSegment.tracks) {
|
|
582
|
-
if (track.cuePoints.length > maxCuePointCount) {
|
|
583
|
-
maxCuePointCount = track.cuePoints.length;
|
|
584
|
-
trackWithMostCuePoints = track;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// For every track that has received 0 cue points (can happen, often only the video track receives cue points),
|
|
589
|
-
// we still want to have better seeking. Therefore, let's give it the cue points of the track with the most cue
|
|
590
|
-
// points, which should provide us with the most fine-grained seeking.
|
|
591
|
-
for (const track of this.currentSegment.tracks) {
|
|
592
|
-
if (track.cuePoints.length === 0) {
|
|
593
|
-
track.cuePoints = trackWithMostCuePoints!.cuePoints;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
this.currentSegment = null;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
async readCluster(startPos: number, segment: Segment) {
|
|
601
|
-
if (segment.lastReadCluster?.elementStartPos === startPos) {
|
|
602
|
-
return segment.lastReadCluster;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
let headerSlice = this.reader.requestSliceRange(startPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
|
606
|
-
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
|
607
|
-
assert(headerSlice);
|
|
608
|
-
|
|
609
|
-
const elementStartPos = startPos;
|
|
610
|
-
const elementHeader = readElementHeader(headerSlice);
|
|
611
|
-
assert(elementHeader);
|
|
612
|
-
|
|
613
|
-
const id = elementHeader.id;
|
|
614
|
-
assert(id === EBMLId.Cluster);
|
|
615
|
-
|
|
616
|
-
let size = elementHeader.size;
|
|
617
|
-
const dataStartPos = headerSlice.filePos;
|
|
618
|
-
|
|
619
|
-
if (size === null) {
|
|
620
|
-
// The cluster's size is undefined (can happen in livestreamed files). We'd still like to know the size of
|
|
621
|
-
// it, so we have no other choice but to iterate over the EBML structure until we find an element at level
|
|
622
|
-
// 0 or 1, indicating the end of the cluster (all elements inside the cluster are at level 2).
|
|
623
|
-
const nextElementPos = await searchForNextElementId(
|
|
624
|
-
this.reader,
|
|
625
|
-
dataStartPos,
|
|
626
|
-
LEVEL_0_AND_1_EBML_IDS,
|
|
627
|
-
segment.elementEndPos,
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
size = nextElementPos.pos - dataStartPos;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Load the entire cluster
|
|
634
|
-
let dataSlice = this.reader.requestSlice(dataStartPos, size);
|
|
635
|
-
if (dataSlice instanceof Promise) dataSlice = await dataSlice;
|
|
636
|
-
|
|
637
|
-
const cluster: Cluster = {
|
|
638
|
-
segment,
|
|
639
|
-
elementStartPos,
|
|
640
|
-
elementEndPos: dataStartPos + size,
|
|
641
|
-
dataStartPos,
|
|
642
|
-
timestamp: -1,
|
|
643
|
-
trackData: new Map(),
|
|
644
|
-
};
|
|
645
|
-
this.currentCluster = cluster;
|
|
646
|
-
|
|
647
|
-
if (dataSlice) {
|
|
648
|
-
// Read the children of the cluster, stopping early at level 0 or 1 EBML elements. We do this because some
|
|
649
|
-
// clusters have incorrect sizes that are too large
|
|
650
|
-
const endPos = this.readContiguousElements(dataSlice, LEVEL_0_AND_1_EBML_IDS);
|
|
651
|
-
cluster.elementEndPos = endPos;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
for (const [, trackData] of cluster.trackData) {
|
|
655
|
-
const track = trackData.track;
|
|
656
|
-
|
|
657
|
-
// This must hold, as track datas only get created if a block for that track is encountered
|
|
658
|
-
assert(trackData.blocks.length > 0);
|
|
659
|
-
|
|
660
|
-
let hasLacedBlocks = false;
|
|
661
|
-
|
|
662
|
-
for (let i = 0; i < trackData.blocks.length; i++) {
|
|
663
|
-
const block = trackData.blocks[i]!;
|
|
664
|
-
block.timestamp += cluster.timestamp;
|
|
665
|
-
|
|
666
|
-
hasLacedBlocks ||= block.lacing !== BlockLacing.None;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
trackData.presentationTimestamps = trackData.blocks
|
|
670
|
-
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
|
671
|
-
.sort((a, b) => a.timestamp - b.timestamp);
|
|
672
|
-
|
|
673
|
-
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
|
674
|
-
const currentEntry = trackData.presentationTimestamps[i]!;
|
|
675
|
-
const currentBlock = trackData.blocks[currentEntry.blockIndex]!;
|
|
676
|
-
|
|
677
|
-
if (trackData.firstKeyFrameTimestamp === null && currentBlock.isKeyFrame) {
|
|
678
|
-
trackData.firstKeyFrameTimestamp = currentBlock.timestamp;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (i < trackData.presentationTimestamps.length - 1) {
|
|
682
|
-
// Update block durations based on presentation order
|
|
683
|
-
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
|
684
|
-
currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp;
|
|
685
|
-
} else if (currentBlock.duration === 0) {
|
|
686
|
-
if (track.defaultDuration != null) {
|
|
687
|
-
if (currentBlock.lacing === BlockLacing.None) {
|
|
688
|
-
currentBlock.duration = track.defaultDuration;
|
|
689
|
-
} else {
|
|
690
|
-
// Handled by the lace resolution code
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (hasLacedBlocks) {
|
|
697
|
-
// Perform lace resolution. Here, we expand each laced block into multiple blocks where each contains
|
|
698
|
-
// one frame of the lace. We do this after determining block timestamps so we can properly distribute
|
|
699
|
-
// the block's duration across the laced frames.
|
|
700
|
-
this.expandLacedBlocks(trackData.blocks, track);
|
|
701
|
-
|
|
702
|
-
// Recompute since blocks have changed
|
|
703
|
-
trackData.presentationTimestamps = trackData.blocks
|
|
704
|
-
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
|
705
|
-
.sort((a, b) => a.timestamp - b.timestamp);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const firstBlock = trackData.blocks[trackData.presentationTimestamps[0]!.blockIndex]!;
|
|
709
|
-
const lastBlock = trackData.blocks[last(trackData.presentationTimestamps)!.blockIndex]!;
|
|
710
|
-
|
|
711
|
-
trackData.startTimestamp = firstBlock.timestamp;
|
|
712
|
-
trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration;
|
|
713
|
-
|
|
714
|
-
// Let's remember that a cluster with a given timestamp is here, speeding up future lookups if no cues exist
|
|
715
|
-
const insertionIndex = binarySearchLessOrEqual(
|
|
716
|
-
track.clusterPositionCache,
|
|
717
|
-
trackData.startTimestamp,
|
|
718
|
-
x => x.startTimestamp,
|
|
719
|
-
);
|
|
720
|
-
if (
|
|
721
|
-
insertionIndex === -1
|
|
722
|
-
|| track.clusterPositionCache[insertionIndex]!.elementStartPos !== elementStartPos
|
|
723
|
-
) {
|
|
724
|
-
track.clusterPositionCache.splice(insertionIndex + 1, 0, {
|
|
725
|
-
elementStartPos: cluster.elementStartPos,
|
|
726
|
-
startTimestamp: trackData.startTimestamp,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
segment.lastReadCluster = cluster;
|
|
732
|
-
return cluster;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
getTrackDataInCluster(cluster: Cluster, trackNumber: number) {
|
|
736
|
-
let trackData = cluster.trackData.get(trackNumber);
|
|
737
|
-
if (!trackData) {
|
|
738
|
-
const track = cluster.segment.tracks.find(x => x.id === trackNumber);
|
|
739
|
-
if (!track) {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
trackData = {
|
|
744
|
-
track,
|
|
745
|
-
startTimestamp: 0,
|
|
746
|
-
endTimestamp: 0,
|
|
747
|
-
firstKeyFrameTimestamp: null,
|
|
748
|
-
blocks: [],
|
|
749
|
-
presentationTimestamps: [],
|
|
750
|
-
};
|
|
751
|
-
cluster.trackData.set(trackNumber, trackData);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return trackData;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
expandLacedBlocks(blocks: ClusterBlock[], track: InternalTrack) {
|
|
758
|
-
// https://www.matroska.org/technical/notes.html#block-lacing
|
|
759
|
-
|
|
760
|
-
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
|
761
|
-
const originalBlock = blocks[blockIndex]!;
|
|
762
|
-
if (originalBlock.lacing === BlockLacing.None) {
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Decode the block data if it hasn't been decoded yet (needed for lacing expansion)
|
|
767
|
-
if (!originalBlock.decoded) {
|
|
768
|
-
originalBlock.data = this.decodeBlockData(track, originalBlock.data);
|
|
769
|
-
originalBlock.decoded = true;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const slice = FileSlice.tempFromBytes(originalBlock.data);
|
|
773
|
-
|
|
774
|
-
const frameSizes: number[] = [];
|
|
775
|
-
const frameCount = readU8(slice) + 1;
|
|
776
|
-
|
|
777
|
-
switch (originalBlock.lacing) {
|
|
778
|
-
case BlockLacing.Xiph: {
|
|
779
|
-
let totalUsedSize = 0;
|
|
780
|
-
|
|
781
|
-
// Xiph lacing, just like in Ogg
|
|
782
|
-
for (let i = 0; i < frameCount - 1; i++) {
|
|
783
|
-
let frameSize = 0;
|
|
784
|
-
|
|
785
|
-
while (slice.bufferPos < slice.length) {
|
|
786
|
-
const value = readU8(slice);
|
|
787
|
-
frameSize += value;
|
|
788
|
-
|
|
789
|
-
if (value < 255) {
|
|
790
|
-
frameSizes.push(frameSize);
|
|
791
|
-
totalUsedSize += frameSize;
|
|
792
|
-
|
|
793
|
-
break;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Compute the last frame's size from whatever's left
|
|
799
|
-
frameSizes.push(slice.length - (slice.bufferPos + totalUsedSize));
|
|
800
|
-
}; break;
|
|
801
|
-
|
|
802
|
-
case BlockLacing.FixedSize: {
|
|
803
|
-
// Fixed size lacing: all frames have same size
|
|
804
|
-
const totalDataSize = slice.length - 1; // Minus the frame count byte
|
|
805
|
-
const frameSize = Math.floor(totalDataSize / frameCount);
|
|
806
|
-
|
|
807
|
-
for (let i = 0; i < frameCount; i++) {
|
|
808
|
-
frameSizes.push(frameSize);
|
|
809
|
-
}
|
|
810
|
-
}; break;
|
|
811
|
-
|
|
812
|
-
case BlockLacing.Ebml: {
|
|
813
|
-
// EBML lacing: first size absolute, subsequent ones are coded as signed differences from the last
|
|
814
|
-
const firstResult = readVarInt(slice);
|
|
815
|
-
assert(firstResult !== null); // Assume it's not an invalid VINT
|
|
816
|
-
|
|
817
|
-
let currentSize = firstResult;
|
|
818
|
-
frameSizes.push(currentSize);
|
|
819
|
-
|
|
820
|
-
let totalUsedSize = currentSize;
|
|
821
|
-
|
|
822
|
-
for (let i = 1; i < frameCount - 1; i++) {
|
|
823
|
-
const startPos = slice.bufferPos;
|
|
824
|
-
const diffResult = readVarInt(slice);
|
|
825
|
-
assert(diffResult !== null);
|
|
826
|
-
|
|
827
|
-
const unsignedDiff = diffResult;
|
|
828
|
-
const width = slice.bufferPos - startPos;
|
|
829
|
-
const bias = (1 << (width * 7 - 1)) - 1; // Typo-corrected version of 2^((7*n)-1)^-1
|
|
830
|
-
const diff = unsignedDiff - bias;
|
|
831
|
-
|
|
832
|
-
currentSize += diff;
|
|
833
|
-
frameSizes.push(currentSize);
|
|
834
|
-
|
|
835
|
-
totalUsedSize += currentSize;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Compute the last frame's size from whatever's left
|
|
839
|
-
frameSizes.push(slice.length - (slice.bufferPos + totalUsedSize));
|
|
840
|
-
}; break;
|
|
841
|
-
|
|
842
|
-
default: assert(false);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
assert(frameSizes.length === frameCount);
|
|
846
|
-
|
|
847
|
-
blocks.splice(blockIndex, 1); // Remove the original block
|
|
848
|
-
|
|
849
|
-
const blockDuration = originalBlock.duration || frameCount * (track.defaultDuration ?? 0);
|
|
850
|
-
|
|
851
|
-
// Now, let's insert each frame as its own block
|
|
852
|
-
for (let i = 0; i < frameCount; i++) {
|
|
853
|
-
const frameSize = frameSizes[i]!;
|
|
854
|
-
const frameData = readBytes(slice, frameSize);
|
|
855
|
-
|
|
856
|
-
// Distribute timestamps evenly across the block duration
|
|
857
|
-
const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount);
|
|
858
|
-
const frameDuration = blockDuration / frameCount;
|
|
859
|
-
|
|
860
|
-
blocks.splice(blockIndex + i, 0, {
|
|
861
|
-
timestamp: frameTimestamp,
|
|
862
|
-
duration: frameDuration,
|
|
863
|
-
isKeyFrame: originalBlock.isKeyFrame,
|
|
864
|
-
data: frameData,
|
|
865
|
-
lacing: BlockLacing.None,
|
|
866
|
-
decoded: true,
|
|
867
|
-
mainAdditional: originalBlock.mainAdditional,
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
blockIndex += frameCount; // Skip the blocks we just added
|
|
872
|
-
blockIndex--;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async loadSegmentMetadata(segment: Segment) {
|
|
877
|
-
for (const seekEntry of segment.seekEntries) {
|
|
878
|
-
if (seekEntry.id === EBMLId.Tags && !segment.tagsSeen) {
|
|
879
|
-
// We need to load the tags
|
|
880
|
-
} else if (seekEntry.id === EBMLId.Attachments && !segment.attachmentsSeen) {
|
|
881
|
-
// We need to load the attachments
|
|
882
|
-
} else {
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
let slice = this.reader.requestSliceRange(
|
|
887
|
-
segment.dataStartPos + seekEntry.segmentPosition,
|
|
888
|
-
MIN_HEADER_SIZE,
|
|
889
|
-
MAX_HEADER_SIZE,
|
|
890
|
-
);
|
|
891
|
-
if (slice instanceof Promise) slice = await slice;
|
|
892
|
-
if (!slice) continue;
|
|
893
|
-
|
|
894
|
-
const header = readElementHeader(slice);
|
|
895
|
-
if (!header || header.id !== seekEntry.id) continue;
|
|
896
|
-
|
|
897
|
-
const { size } = header;
|
|
898
|
-
assertDefinedSize(size);
|
|
899
|
-
|
|
900
|
-
assert(!this.currentSegment);
|
|
901
|
-
this.currentSegment = segment;
|
|
902
|
-
|
|
903
|
-
let dataSlice = this.reader.requestSlice(slice.filePos, size);
|
|
904
|
-
if (dataSlice instanceof Promise) dataSlice = await dataSlice;
|
|
905
|
-
if (dataSlice) {
|
|
906
|
-
this.readContiguousElements(dataSlice);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
this.currentSegment = null;
|
|
910
|
-
|
|
911
|
-
// Mark as seen
|
|
912
|
-
if (seekEntry.id === EBMLId.Tags) {
|
|
913
|
-
segment.tagsSeen = true;
|
|
914
|
-
} else if (seekEntry.id === EBMLId.Attachments) {
|
|
915
|
-
segment.attachmentsSeen = true;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
readContiguousElements(slice: FileSlice, stopIds?: number[]) {
|
|
921
|
-
const startIndex = slice.filePos;
|
|
922
|
-
|
|
923
|
-
while (slice.filePos - startIndex <= slice.length - MIN_HEADER_SIZE) {
|
|
924
|
-
const startPos = slice.filePos;
|
|
925
|
-
const foundElement = this.traverseElement(slice, stopIds);
|
|
926
|
-
|
|
927
|
-
if (!foundElement) {
|
|
928
|
-
return startPos;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
return slice.filePos;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
traverseElement(slice: FileSlice, stopIds?: number[]): boolean {
|
|
936
|
-
const header = readElementHeader(slice);
|
|
937
|
-
if (!header) {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (stopIds && stopIds.includes(header.id)) {
|
|
942
|
-
return false;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const { id, size } = header;
|
|
946
|
-
const dataStartPos = slice.filePos;
|
|
947
|
-
assertDefinedSize(size);
|
|
948
|
-
|
|
949
|
-
switch (id) {
|
|
950
|
-
case EBMLId.DocType: {
|
|
951
|
-
this.isWebM = readAsciiString(slice, size) === 'webm';
|
|
952
|
-
}; break;
|
|
953
|
-
|
|
954
|
-
case EBMLId.Seek: {
|
|
955
|
-
if (!this.currentSegment) break;
|
|
956
|
-
const seekEntry: SeekEntry = { id: -1, segmentPosition: -1 };
|
|
957
|
-
this.currentSegment.seekEntries.push(seekEntry);
|
|
958
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
959
|
-
|
|
960
|
-
if (seekEntry.id === -1 || seekEntry.segmentPosition === -1) {
|
|
961
|
-
this.currentSegment.seekEntries.pop();
|
|
962
|
-
}
|
|
963
|
-
}; break;
|
|
964
|
-
|
|
965
|
-
case EBMLId.SeekID: {
|
|
966
|
-
const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1];
|
|
967
|
-
if (!lastSeekEntry) break;
|
|
968
|
-
|
|
969
|
-
lastSeekEntry.id = readUnsignedInt(slice, size);
|
|
970
|
-
}; break;
|
|
971
|
-
|
|
972
|
-
case EBMLId.SeekPosition: {
|
|
973
|
-
const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1];
|
|
974
|
-
if (!lastSeekEntry) break;
|
|
975
|
-
|
|
976
|
-
lastSeekEntry.segmentPosition = readUnsignedInt(slice, size);
|
|
977
|
-
}; break;
|
|
978
|
-
|
|
979
|
-
case EBMLId.TimestampScale: {
|
|
980
|
-
if (!this.currentSegment) break;
|
|
981
|
-
|
|
982
|
-
this.currentSegment.timestampScale = readUnsignedInt(slice, size);
|
|
983
|
-
this.currentSegment.timestampFactor = 1e9 / this.currentSegment.timestampScale;
|
|
984
|
-
}; break;
|
|
985
|
-
|
|
986
|
-
case EBMLId.Duration: {
|
|
987
|
-
if (!this.currentSegment) break;
|
|
988
|
-
|
|
989
|
-
this.currentSegment.duration = readFloat(slice, size);
|
|
990
|
-
}; break;
|
|
991
|
-
|
|
992
|
-
case EBMLId.TrackEntry: {
|
|
993
|
-
if (!this.currentSegment) break;
|
|
994
|
-
|
|
995
|
-
this.currentTrack = {
|
|
996
|
-
id: -1,
|
|
997
|
-
segment: this.currentSegment,
|
|
998
|
-
demuxer: this,
|
|
999
|
-
clusterPositionCache: [],
|
|
1000
|
-
cuePoints: [],
|
|
1001
|
-
|
|
1002
|
-
disposition: {
|
|
1003
|
-
...DEFAULT_TRACK_DISPOSITION,
|
|
1004
|
-
},
|
|
1005
|
-
inputTrack: null,
|
|
1006
|
-
codecId: null,
|
|
1007
|
-
codecPrivate: null,
|
|
1008
|
-
defaultDuration: null,
|
|
1009
|
-
name: null,
|
|
1010
|
-
languageCode: UNDETERMINED_LANGUAGE,
|
|
1011
|
-
decodingInstructions: [],
|
|
1012
|
-
|
|
1013
|
-
info: null,
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1017
|
-
|
|
1018
|
-
if (this.currentTrack.decodingInstructions.some((instruction) => {
|
|
1019
|
-
return instruction.data?.type !== 'decompress'
|
|
1020
|
-
|| instruction.scope !== ContentEncodingScope.Block
|
|
1021
|
-
|| instruction.data.algorithm !== ContentCompAlgo.HeaderStripping;
|
|
1022
|
-
})) {
|
|
1023
|
-
console.warn(`Track #${this.currentTrack.id} has an unsupported content encoding; dropping.`);
|
|
1024
|
-
this.currentTrack = null;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (
|
|
1028
|
-
this.currentTrack
|
|
1029
|
-
&& this.currentTrack.id !== -1
|
|
1030
|
-
&& this.currentTrack.codecId
|
|
1031
|
-
&& this.currentTrack.info
|
|
1032
|
-
) {
|
|
1033
|
-
const slashIndex = this.currentTrack.codecId.indexOf('/');
|
|
1034
|
-
const codecIdWithoutSuffix = slashIndex === -1
|
|
1035
|
-
? this.currentTrack.codecId
|
|
1036
|
-
: this.currentTrack.codecId.slice(0, slashIndex);
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1039
|
-
this.currentTrack.info.type === 'video'
|
|
1040
|
-
&& this.currentTrack.info.width !== -1
|
|
1041
|
-
&& this.currentTrack.info.height !== -1
|
|
1042
|
-
) {
|
|
1043
|
-
if (this.currentTrack.codecId === CODEC_STRING_MAP.avc) {
|
|
1044
|
-
this.currentTrack.info.codec = 'avc';
|
|
1045
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1046
|
-
} else if (this.currentTrack.codecId === CODEC_STRING_MAP.hevc) {
|
|
1047
|
-
this.currentTrack.info.codec = 'hevc';
|
|
1048
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1049
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vp8) {
|
|
1050
|
-
this.currentTrack.info.codec = 'vp8';
|
|
1051
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vp9) {
|
|
1052
|
-
this.currentTrack.info.codec = 'vp9';
|
|
1053
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.av1) {
|
|
1054
|
-
this.currentTrack.info.codec = 'av1';
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const videoTrack = this.currentTrack as InternalVideoTrack;
|
|
1058
|
-
const inputTrack = new InputVideoTrack(this.input, new MatroskaVideoTrackBacking(videoTrack));
|
|
1059
|
-
this.currentTrack.inputTrack = inputTrack;
|
|
1060
|
-
this.currentSegment.tracks.push(this.currentTrack);
|
|
1061
|
-
} else if (
|
|
1062
|
-
this.currentTrack.info.type === 'audio'
|
|
1063
|
-
&& this.currentTrack.info.numberOfChannels !== -1
|
|
1064
|
-
&& this.currentTrack.info.sampleRate !== -1
|
|
1065
|
-
) {
|
|
1066
|
-
if (codecIdWithoutSuffix === CODEC_STRING_MAP.aac) {
|
|
1067
|
-
this.currentTrack.info.codec = 'aac';
|
|
1068
|
-
this.currentTrack.info.aacCodecInfo = {
|
|
1069
|
-
isMpeg2: this.currentTrack.codecId.includes('MPEG2'),
|
|
1070
|
-
};
|
|
1071
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1072
|
-
} else if (this.currentTrack.codecId === CODEC_STRING_MAP.mp3) {
|
|
1073
|
-
this.currentTrack.info.codec = 'mp3';
|
|
1074
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) {
|
|
1075
|
-
this.currentTrack.info.codec = 'opus';
|
|
1076
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1077
|
-
this.currentTrack.info.sampleRate = OPUS_SAMPLE_RATE; // Always the same
|
|
1078
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) {
|
|
1079
|
-
this.currentTrack.info.codec = 'vorbis';
|
|
1080
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1081
|
-
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.flac) {
|
|
1082
|
-
this.currentTrack.info.codec = 'flac';
|
|
1083
|
-
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
|
|
1084
|
-
} else if (this.currentTrack.codecId === 'A_PCM/INT/LIT') {
|
|
1085
|
-
if (this.currentTrack.info.bitDepth === 8) {
|
|
1086
|
-
this.currentTrack.info.codec = 'pcm-u8';
|
|
1087
|
-
} else if (this.currentTrack.info.bitDepth === 16) {
|
|
1088
|
-
this.currentTrack.info.codec = 'pcm-s16';
|
|
1089
|
-
} else if (this.currentTrack.info.bitDepth === 24) {
|
|
1090
|
-
this.currentTrack.info.codec = 'pcm-s24';
|
|
1091
|
-
} else if (this.currentTrack.info.bitDepth === 32) {
|
|
1092
|
-
this.currentTrack.info.codec = 'pcm-s32';
|
|
1093
|
-
}
|
|
1094
|
-
} else if (this.currentTrack.codecId === 'A_PCM/INT/BIG') {
|
|
1095
|
-
if (this.currentTrack.info.bitDepth === 8) {
|
|
1096
|
-
this.currentTrack.info.codec = 'pcm-u8';
|
|
1097
|
-
} else if (this.currentTrack.info.bitDepth === 16) {
|
|
1098
|
-
this.currentTrack.info.codec = 'pcm-s16be';
|
|
1099
|
-
} else if (this.currentTrack.info.bitDepth === 24) {
|
|
1100
|
-
this.currentTrack.info.codec = 'pcm-s24be';
|
|
1101
|
-
} else if (this.currentTrack.info.bitDepth === 32) {
|
|
1102
|
-
this.currentTrack.info.codec = 'pcm-s32be';
|
|
1103
|
-
}
|
|
1104
|
-
} else if (this.currentTrack.codecId === 'A_PCM/FLOAT/IEEE') {
|
|
1105
|
-
if (this.currentTrack.info.bitDepth === 32) {
|
|
1106
|
-
this.currentTrack.info.codec = 'pcm-f32';
|
|
1107
|
-
} else if (this.currentTrack.info.bitDepth === 64) {
|
|
1108
|
-
this.currentTrack.info.codec = 'pcm-f64';
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
const audioTrack = this.currentTrack as InternalAudioTrack;
|
|
1113
|
-
const inputTrack = new InputAudioTrack(this.input, new MatroskaAudioTrackBacking(audioTrack));
|
|
1114
|
-
this.currentTrack.inputTrack = inputTrack;
|
|
1115
|
-
this.currentSegment.tracks.push(this.currentTrack);
|
|
1116
|
-
} else if (this.currentTrack.info.type === 'subtitle') {
|
|
1117
|
-
// Map Matroska codec IDs to our subtitle codecs
|
|
1118
|
-
const codecId = this.currentTrack.codecId;
|
|
1119
|
-
if (codecId === 'S_TEXT/UTF8') {
|
|
1120
|
-
this.currentTrack.info.codec = 'srt';
|
|
1121
|
-
} else if (codecId === 'S_TEXT/SSA' || codecId === 'S_SSA') {
|
|
1122
|
-
this.currentTrack.info.codec = 'ssa';
|
|
1123
|
-
} else if (codecId === 'S_TEXT/ASS' || codecId === 'S_ASS') {
|
|
1124
|
-
this.currentTrack.info.codec = 'ass';
|
|
1125
|
-
} else if (codecId === 'S_TEXT/WEBVTT' || codecId === 'D_WEBVTT' || codecId === 'D_WEBVTT/SUBTITLES') {
|
|
1126
|
-
this.currentTrack.info.codec = 'webvtt';
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Store CodecPrivate as text for ASS/SSA headers
|
|
1130
|
-
if (this.currentTrack.codecPrivate) {
|
|
1131
|
-
const decoder = new TextDecoder('utf-8');
|
|
1132
|
-
this.currentTrack.info.codecPrivateText = decoder.decode(this.currentTrack.codecPrivate);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const subtitleTrack = this.currentTrack as InternalSubtitleTrack;
|
|
1136
|
-
const inputTrack = new InputSubtitleTrack(this.input, new MatroskaSubtitleTrackBacking(subtitleTrack));
|
|
1137
|
-
this.currentTrack.inputTrack = inputTrack;
|
|
1138
|
-
this.currentSegment.tracks.push(this.currentTrack);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
this.currentTrack = null;
|
|
1143
|
-
}; break;
|
|
1144
|
-
|
|
1145
|
-
case EBMLId.TrackNumber: {
|
|
1146
|
-
if (!this.currentTrack) break;
|
|
1147
|
-
|
|
1148
|
-
this.currentTrack.id = readUnsignedInt(slice, size);
|
|
1149
|
-
}; break;
|
|
1150
|
-
|
|
1151
|
-
case EBMLId.TrackType: {
|
|
1152
|
-
if (!this.currentTrack) break;
|
|
1153
|
-
|
|
1154
|
-
const type = readUnsignedInt(slice, size);
|
|
1155
|
-
if (type === 1) {
|
|
1156
|
-
this.currentTrack.info = {
|
|
1157
|
-
type: 'video',
|
|
1158
|
-
width: -1,
|
|
1159
|
-
height: -1,
|
|
1160
|
-
rotation: 0,
|
|
1161
|
-
codec: null,
|
|
1162
|
-
codecDescription: null,
|
|
1163
|
-
colorSpace: null,
|
|
1164
|
-
alphaMode: false,
|
|
1165
|
-
};
|
|
1166
|
-
} else if (type === 2) {
|
|
1167
|
-
this.currentTrack.info = {
|
|
1168
|
-
type: 'audio',
|
|
1169
|
-
numberOfChannels: -1,
|
|
1170
|
-
sampleRate: -1,
|
|
1171
|
-
bitDepth: -1,
|
|
1172
|
-
codec: null,
|
|
1173
|
-
codecDescription: null,
|
|
1174
|
-
aacCodecInfo: null,
|
|
1175
|
-
};
|
|
1176
|
-
} else if (type === 17) {
|
|
1177
|
-
this.currentTrack.info = {
|
|
1178
|
-
type: 'subtitle',
|
|
1179
|
-
codec: null,
|
|
1180
|
-
codecPrivateText: null,
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
}; break;
|
|
1184
|
-
|
|
1185
|
-
case EBMLId.FlagEnabled: {
|
|
1186
|
-
if (!this.currentTrack) break;
|
|
1187
|
-
|
|
1188
|
-
const enabled = readUnsignedInt(slice, size);
|
|
1189
|
-
if (!enabled) {
|
|
1190
|
-
this.currentSegment!.tracks.pop();
|
|
1191
|
-
this.currentTrack = null;
|
|
1192
|
-
}
|
|
1193
|
-
}; break;
|
|
1194
|
-
|
|
1195
|
-
case EBMLId.FlagDefault: {
|
|
1196
|
-
if (!this.currentTrack) break;
|
|
1197
|
-
|
|
1198
|
-
this.currentTrack.disposition.default = !!readUnsignedInt(slice, size);
|
|
1199
|
-
}; break;
|
|
1200
|
-
|
|
1201
|
-
case EBMLId.FlagForced: {
|
|
1202
|
-
if (!this.currentTrack) break;
|
|
1203
|
-
|
|
1204
|
-
this.currentTrack.disposition.forced = !!readUnsignedInt(slice, size);
|
|
1205
|
-
}; break;
|
|
1206
|
-
|
|
1207
|
-
case EBMLId.FlagOriginal: {
|
|
1208
|
-
if (!this.currentTrack) break;
|
|
1209
|
-
|
|
1210
|
-
this.currentTrack.disposition.original = !!readUnsignedInt(slice, size);
|
|
1211
|
-
}; break;
|
|
1212
|
-
|
|
1213
|
-
case EBMLId.FlagHearingImpaired: {
|
|
1214
|
-
if (!this.currentTrack) break;
|
|
1215
|
-
|
|
1216
|
-
this.currentTrack.disposition.hearingImpaired = !!readUnsignedInt(slice, size);
|
|
1217
|
-
}; break;
|
|
1218
|
-
|
|
1219
|
-
case EBMLId.FlagVisualImpaired: {
|
|
1220
|
-
if (!this.currentTrack) break;
|
|
1221
|
-
|
|
1222
|
-
this.currentTrack.disposition.visuallyImpaired = !!readUnsignedInt(slice, size);
|
|
1223
|
-
}; break;
|
|
1224
|
-
|
|
1225
|
-
case EBMLId.FlagCommentary: {
|
|
1226
|
-
if (!this.currentTrack) break;
|
|
1227
|
-
|
|
1228
|
-
this.currentTrack.disposition.commentary = !!readUnsignedInt(slice, size);
|
|
1229
|
-
}; break;
|
|
1230
|
-
|
|
1231
|
-
case EBMLId.CodecID: {
|
|
1232
|
-
if (!this.currentTrack) break;
|
|
1233
|
-
|
|
1234
|
-
this.currentTrack.codecId = readAsciiString(slice, size);
|
|
1235
|
-
}; break;
|
|
1236
|
-
|
|
1237
|
-
case EBMLId.CodecPrivate: {
|
|
1238
|
-
if (!this.currentTrack) break;
|
|
1239
|
-
|
|
1240
|
-
this.currentTrack.codecPrivate = readBytes(slice, size);
|
|
1241
|
-
}; break;
|
|
1242
|
-
|
|
1243
|
-
case EBMLId.DefaultDuration: {
|
|
1244
|
-
if (!this.currentTrack) break;
|
|
1245
|
-
|
|
1246
|
-
this.currentTrack.defaultDuration
|
|
1247
|
-
= this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9;
|
|
1248
|
-
}; break;
|
|
1249
|
-
|
|
1250
|
-
case EBMLId.Name: {
|
|
1251
|
-
if (!this.currentTrack) break;
|
|
1252
|
-
|
|
1253
|
-
this.currentTrack.name = readUnicodeString(slice, size);
|
|
1254
|
-
}; break;
|
|
1255
|
-
|
|
1256
|
-
case EBMLId.Language: {
|
|
1257
|
-
if (!this.currentTrack) break;
|
|
1258
|
-
if (this.currentTrack.languageCode !== UNDETERMINED_LANGUAGE) {
|
|
1259
|
-
// LanguageBCP47 was present, which takes precedence
|
|
1260
|
-
break;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
this.currentTrack.languageCode = readAsciiString(slice, size);
|
|
1264
|
-
|
|
1265
|
-
if (!isIso639Dash2LanguageCode(this.currentTrack.languageCode)) {
|
|
1266
|
-
this.currentTrack.languageCode = UNDETERMINED_LANGUAGE;
|
|
1267
|
-
}
|
|
1268
|
-
}; break;
|
|
1269
|
-
|
|
1270
|
-
case EBMLId.LanguageBCP47: {
|
|
1271
|
-
if (!this.currentTrack) break;
|
|
1272
|
-
|
|
1273
|
-
const bcp47 = readAsciiString(slice, size);
|
|
1274
|
-
const languageSubtag = bcp47.split('-')[0];
|
|
1275
|
-
|
|
1276
|
-
if (languageSubtag) {
|
|
1277
|
-
// Technically invalid, for now: The language subtag might be a language code from ISO 639-1,
|
|
1278
|
-
// ISO 639-2, ISO 639-3, ISO 639-5 or some other thing (source: Wikipedia). But, `languageCode` is
|
|
1279
|
-
// documented as ISO 639-2. Changing the definition would be a breaking change. This will get
|
|
1280
|
-
// cleaned up in the future by defining languageCode to be BCP 47 instead.
|
|
1281
|
-
this.currentTrack.languageCode = languageSubtag;
|
|
1282
|
-
} else {
|
|
1283
|
-
this.currentTrack.languageCode = UNDETERMINED_LANGUAGE;
|
|
1284
|
-
}
|
|
1285
|
-
}; break;
|
|
1286
|
-
|
|
1287
|
-
case EBMLId.Video: {
|
|
1288
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1289
|
-
|
|
1290
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1291
|
-
}; break;
|
|
1292
|
-
|
|
1293
|
-
case EBMLId.PixelWidth: {
|
|
1294
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1295
|
-
|
|
1296
|
-
this.currentTrack.info.width = readUnsignedInt(slice, size);
|
|
1297
|
-
}; break;
|
|
1298
|
-
|
|
1299
|
-
case EBMLId.PixelHeight: {
|
|
1300
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1301
|
-
|
|
1302
|
-
this.currentTrack.info.height = readUnsignedInt(slice, size);
|
|
1303
|
-
}; break;
|
|
1304
|
-
|
|
1305
|
-
case EBMLId.AlphaMode: {
|
|
1306
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1307
|
-
|
|
1308
|
-
this.currentTrack.info.alphaMode = readUnsignedInt(slice, size) === 1;
|
|
1309
|
-
}; break;
|
|
1310
|
-
|
|
1311
|
-
case EBMLId.Colour: {
|
|
1312
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1313
|
-
|
|
1314
|
-
this.currentTrack.info.colorSpace = {};
|
|
1315
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1316
|
-
}; break;
|
|
1317
|
-
|
|
1318
|
-
case EBMLId.MatrixCoefficients: {
|
|
1319
|
-
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
|
|
1320
|
-
|
|
1321
|
-
const matrixCoefficients = readUnsignedInt(slice, size);
|
|
1322
|
-
const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null;
|
|
1323
|
-
this.currentTrack.info.colorSpace.matrix = mapped as VideoColorSpaceInit['matrix'];
|
|
1324
|
-
}; break;
|
|
1325
|
-
|
|
1326
|
-
case EBMLId.Range: {
|
|
1327
|
-
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
|
|
1328
|
-
|
|
1329
|
-
this.currentTrack.info.colorSpace.fullRange = readUnsignedInt(slice, size) === 2;
|
|
1330
|
-
}; break;
|
|
1331
|
-
|
|
1332
|
-
case EBMLId.TransferCharacteristics: {
|
|
1333
|
-
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
|
|
1334
|
-
|
|
1335
|
-
const transferCharacteristics = readUnsignedInt(slice, size);
|
|
1336
|
-
const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null;
|
|
1337
|
-
this.currentTrack.info.colorSpace.transfer = mapped as VideoColorSpaceInit['transfer'];
|
|
1338
|
-
}; break;
|
|
1339
|
-
|
|
1340
|
-
case EBMLId.Primaries: {
|
|
1341
|
-
if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break;
|
|
1342
|
-
|
|
1343
|
-
const primaries = readUnsignedInt(slice, size);
|
|
1344
|
-
const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null;
|
|
1345
|
-
this.currentTrack.info.colorSpace.primaries = mapped as VideoColorSpaceInit['primaries'];
|
|
1346
|
-
}; break;
|
|
1347
|
-
|
|
1348
|
-
case EBMLId.Projection: {
|
|
1349
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1350
|
-
|
|
1351
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1352
|
-
}; break;
|
|
1353
|
-
|
|
1354
|
-
case EBMLId.ProjectionPoseRoll: {
|
|
1355
|
-
if (this.currentTrack?.info?.type !== 'video') break;
|
|
1356
|
-
|
|
1357
|
-
const rotation = readFloat(slice, size);
|
|
1358
|
-
const flippedRotation = -rotation; // Convert counter-clockwise to clockwise
|
|
1359
|
-
|
|
1360
|
-
try {
|
|
1361
|
-
this.currentTrack.info.rotation = normalizeRotation(flippedRotation);
|
|
1362
|
-
} catch {
|
|
1363
|
-
// It wasn't a valid rotation
|
|
1364
|
-
}
|
|
1365
|
-
}; break;
|
|
1366
|
-
|
|
1367
|
-
case EBMLId.Audio: {
|
|
1368
|
-
if (this.currentTrack?.info?.type !== 'audio') break;
|
|
1369
|
-
|
|
1370
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1371
|
-
}; break;
|
|
1372
|
-
|
|
1373
|
-
case EBMLId.SamplingFrequency: {
|
|
1374
|
-
if (this.currentTrack?.info?.type !== 'audio') break;
|
|
1375
|
-
|
|
1376
|
-
this.currentTrack.info.sampleRate = readFloat(slice, size);
|
|
1377
|
-
}; break;
|
|
1378
|
-
|
|
1379
|
-
case EBMLId.Channels: {
|
|
1380
|
-
if (this.currentTrack?.info?.type !== 'audio') break;
|
|
1381
|
-
|
|
1382
|
-
this.currentTrack.info.numberOfChannels = readUnsignedInt(slice, size);
|
|
1383
|
-
}; break;
|
|
1384
|
-
|
|
1385
|
-
case EBMLId.BitDepth: {
|
|
1386
|
-
if (this.currentTrack?.info?.type !== 'audio') break;
|
|
1387
|
-
|
|
1388
|
-
this.currentTrack.info.bitDepth = readUnsignedInt(slice, size);
|
|
1389
|
-
}; break;
|
|
1390
|
-
|
|
1391
|
-
case EBMLId.CuePoint: {
|
|
1392
|
-
if (!this.currentSegment) break;
|
|
1393
|
-
|
|
1394
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1395
|
-
this.currentCueTime = null;
|
|
1396
|
-
}; break;
|
|
1397
|
-
|
|
1398
|
-
case EBMLId.CueTime: {
|
|
1399
|
-
this.currentCueTime = readUnsignedInt(slice, size);
|
|
1400
|
-
}; break;
|
|
1401
|
-
|
|
1402
|
-
case EBMLId.CueTrackPositions: {
|
|
1403
|
-
if (this.currentCueTime === null) break;
|
|
1404
|
-
assert(this.currentSegment);
|
|
1405
|
-
|
|
1406
|
-
const cuePoint: CuePoint = { time: this.currentCueTime, trackId: -1, clusterPosition: -1 };
|
|
1407
|
-
this.currentSegment.cuePoints.push(cuePoint);
|
|
1408
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1409
|
-
|
|
1410
|
-
if (cuePoint.trackId === -1 || cuePoint.clusterPosition === -1) {
|
|
1411
|
-
this.currentSegment.cuePoints.pop();
|
|
1412
|
-
}
|
|
1413
|
-
}; break;
|
|
1414
|
-
|
|
1415
|
-
case EBMLId.CueTrack: {
|
|
1416
|
-
const lastCuePoint = this.currentSegment?.cuePoints[this.currentSegment.cuePoints.length - 1];
|
|
1417
|
-
if (!lastCuePoint) break;
|
|
1418
|
-
|
|
1419
|
-
lastCuePoint.trackId = readUnsignedInt(slice, size);
|
|
1420
|
-
}; break;
|
|
1421
|
-
|
|
1422
|
-
case EBMLId.CueClusterPosition: {
|
|
1423
|
-
const lastCuePoint = this.currentSegment?.cuePoints[this.currentSegment.cuePoints.length - 1];
|
|
1424
|
-
if (!lastCuePoint) break;
|
|
1425
|
-
|
|
1426
|
-
assert(this.currentSegment);
|
|
1427
|
-
lastCuePoint.clusterPosition = this.currentSegment.dataStartPos + readUnsignedInt(slice, size);
|
|
1428
|
-
}; break;
|
|
1429
|
-
|
|
1430
|
-
case EBMLId.Timestamp: {
|
|
1431
|
-
if (!this.currentCluster) break;
|
|
1432
|
-
|
|
1433
|
-
this.currentCluster.timestamp = readUnsignedInt(slice, size);
|
|
1434
|
-
}; break;
|
|
1435
|
-
|
|
1436
|
-
case EBMLId.SimpleBlock: {
|
|
1437
|
-
if (!this.currentCluster) break;
|
|
1438
|
-
|
|
1439
|
-
const trackNumber = readVarInt(slice);
|
|
1440
|
-
if (trackNumber === null) break;
|
|
1441
|
-
|
|
1442
|
-
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
|
|
1443
|
-
if (!trackData) break; // Not a track we care about
|
|
1444
|
-
|
|
1445
|
-
const relativeTimestamp = readI16Be(slice);
|
|
1446
|
-
|
|
1447
|
-
const flags = readU8(slice);
|
|
1448
|
-
const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later
|
|
1449
|
-
|
|
1450
|
-
let isKeyFrame = !!(flags & 0x80);
|
|
1451
|
-
if (trackData.track.info?.type === 'audio' && trackData.track.info.codec) {
|
|
1452
|
-
// Some files don't mark their audio packets as key packets (I'm looking at you, Firefox). But, we
|
|
1453
|
-
// can fix this in most cases: if we recognize the codec of the track, then we know every packet is
|
|
1454
|
-
// necessarily a key packet, no matter what the container says.
|
|
1455
|
-
// https://github.com/Vanilagy/mediabunny/issues/192
|
|
1456
|
-
isKeyFrame = true;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
const blockData = readBytes(slice, size - (slice.filePos - dataStartPos));
|
|
1460
|
-
const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0;
|
|
1461
|
-
|
|
1462
|
-
trackData.blocks.push({
|
|
1463
|
-
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
|
|
1464
|
-
duration: 0, // Will set later
|
|
1465
|
-
isKeyFrame,
|
|
1466
|
-
data: blockData,
|
|
1467
|
-
lacing,
|
|
1468
|
-
decoded: !hasDecodingInstructions,
|
|
1469
|
-
mainAdditional: null,
|
|
1470
|
-
});
|
|
1471
|
-
}; break;
|
|
1472
|
-
|
|
1473
|
-
case EBMLId.BlockGroup: {
|
|
1474
|
-
if (!this.currentCluster) break;
|
|
1475
|
-
|
|
1476
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1477
|
-
|
|
1478
|
-
this.currentBlock = null;
|
|
1479
|
-
}; break;
|
|
1480
|
-
|
|
1481
|
-
case EBMLId.Block: {
|
|
1482
|
-
if (!this.currentCluster) break;
|
|
1483
|
-
|
|
1484
|
-
const trackNumber = readVarInt(slice);
|
|
1485
|
-
if (trackNumber === null) break;
|
|
1486
|
-
|
|
1487
|
-
const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
|
|
1488
|
-
if (!trackData) break;
|
|
1489
|
-
|
|
1490
|
-
const relativeTimestamp = readI16Be(slice);
|
|
1491
|
-
|
|
1492
|
-
const flags = readU8(slice);
|
|
1493
|
-
const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later
|
|
1494
|
-
|
|
1495
|
-
const blockData = readBytes(slice, size - (slice.filePos - dataStartPos));
|
|
1496
|
-
const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0;
|
|
1497
|
-
|
|
1498
|
-
this.currentBlock = {
|
|
1499
|
-
timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later
|
|
1500
|
-
duration: 0, // Will set later
|
|
1501
|
-
isKeyFrame: true,
|
|
1502
|
-
data: blockData,
|
|
1503
|
-
lacing,
|
|
1504
|
-
decoded: !hasDecodingInstructions,
|
|
1505
|
-
mainAdditional: null,
|
|
1506
|
-
};
|
|
1507
|
-
trackData.blocks.push(this.currentBlock);
|
|
1508
|
-
}; break;
|
|
1509
|
-
|
|
1510
|
-
case EBMLId.BlockAdditions: {
|
|
1511
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1512
|
-
}; break;
|
|
1513
|
-
|
|
1514
|
-
case EBMLId.BlockMore: {
|
|
1515
|
-
if (!this.currentBlock) break;
|
|
1516
|
-
|
|
1517
|
-
this.currentBlockAdditional = {
|
|
1518
|
-
addId: 1,
|
|
1519
|
-
data: null,
|
|
1520
|
-
};
|
|
1521
|
-
|
|
1522
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1523
|
-
|
|
1524
|
-
if (this.currentBlockAdditional.data && this.currentBlockAdditional.addId === 1) {
|
|
1525
|
-
this.currentBlock.mainAdditional = this.currentBlockAdditional.data;
|
|
1526
|
-
}
|
|
1527
|
-
this.currentBlockAdditional = null;
|
|
1528
|
-
}; break;
|
|
1529
|
-
|
|
1530
|
-
case EBMLId.BlockAdditional: {
|
|
1531
|
-
if (!this.currentBlockAdditional) break;
|
|
1532
|
-
|
|
1533
|
-
this.currentBlockAdditional.data = readBytes(slice, size);
|
|
1534
|
-
}; break;
|
|
1535
|
-
|
|
1536
|
-
case EBMLId.BlockAddID: {
|
|
1537
|
-
if (!this.currentBlockAdditional) break;
|
|
1538
|
-
|
|
1539
|
-
this.currentBlockAdditional.addId = readUnsignedInt(slice, size);
|
|
1540
|
-
}; break;
|
|
1541
|
-
|
|
1542
|
-
case EBMLId.BlockDuration: {
|
|
1543
|
-
if (!this.currentBlock) break;
|
|
1544
|
-
|
|
1545
|
-
this.currentBlock.duration = readUnsignedInt(slice, size);
|
|
1546
|
-
}; break;
|
|
1547
|
-
|
|
1548
|
-
case EBMLId.ReferenceBlock: {
|
|
1549
|
-
if (!this.currentBlock) break;
|
|
1550
|
-
|
|
1551
|
-
this.currentBlock.isKeyFrame = false;
|
|
1552
|
-
// We ignore the actual value here, we just use the reference as an indicator for "not a key frame".
|
|
1553
|
-
// This is in line with FFmpeg's behavior.
|
|
1554
|
-
}; break;
|
|
1555
|
-
|
|
1556
|
-
case EBMLId.Tag: {
|
|
1557
|
-
this.currentTagTargetIsMovie = true;
|
|
1558
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1559
|
-
}; break;
|
|
1560
|
-
|
|
1561
|
-
case EBMLId.Targets: {
|
|
1562
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1563
|
-
}; break;
|
|
1564
|
-
|
|
1565
|
-
case EBMLId.TargetTypeValue: {
|
|
1566
|
-
const targetTypeValue = readUnsignedInt(slice, size);
|
|
1567
|
-
if (targetTypeValue !== 50) {
|
|
1568
|
-
this.currentTagTargetIsMovie = false;
|
|
1569
|
-
}
|
|
1570
|
-
}; break;
|
|
1571
|
-
|
|
1572
|
-
case EBMLId.TagTrackUID:
|
|
1573
|
-
case EBMLId.TagEditionUID:
|
|
1574
|
-
case EBMLId.TagChapterUID:
|
|
1575
|
-
case EBMLId.TagAttachmentUID: {
|
|
1576
|
-
this.currentTagTargetIsMovie = false;
|
|
1577
|
-
}; break;
|
|
1578
|
-
|
|
1579
|
-
case EBMLId.SimpleTag: {
|
|
1580
|
-
if (!this.currentTagTargetIsMovie) break;
|
|
1581
|
-
|
|
1582
|
-
this.currentSimpleTagName = null;
|
|
1583
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1584
|
-
}; break;
|
|
1585
|
-
|
|
1586
|
-
case EBMLId.TagName: {
|
|
1587
|
-
this.currentSimpleTagName = readUnicodeString(slice, size);
|
|
1588
|
-
}; break;
|
|
1589
|
-
|
|
1590
|
-
case EBMLId.TagString: {
|
|
1591
|
-
if (!this.currentSimpleTagName) break;
|
|
1592
|
-
|
|
1593
|
-
const value = readUnicodeString(slice, size);
|
|
1594
|
-
this.processTagValue(this.currentSimpleTagName, value);
|
|
1595
|
-
}; break;
|
|
1596
|
-
|
|
1597
|
-
case EBMLId.TagBinary: {
|
|
1598
|
-
if (!this.currentSimpleTagName) break;
|
|
1599
|
-
|
|
1600
|
-
const value = readBytes(slice, size);
|
|
1601
|
-
this.processTagValue(this.currentSimpleTagName, value);
|
|
1602
|
-
}; break;
|
|
1603
|
-
|
|
1604
|
-
case EBMLId.AttachedFile: {
|
|
1605
|
-
if (!this.currentSegment) break;
|
|
1606
|
-
|
|
1607
|
-
this.currentAttachedFile = {
|
|
1608
|
-
fileUid: null,
|
|
1609
|
-
fileName: null,
|
|
1610
|
-
fileMediaType: null,
|
|
1611
|
-
fileData: null,
|
|
1612
|
-
fileDescription: null,
|
|
1613
|
-
};
|
|
1614
|
-
|
|
1615
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1616
|
-
|
|
1617
|
-
const tags = this.currentSegment.metadataTags;
|
|
1618
|
-
|
|
1619
|
-
if (this.currentAttachedFile.fileUid && this.currentAttachedFile.fileData) {
|
|
1620
|
-
// All attached files get surfaced in the `raw` metadata tags
|
|
1621
|
-
tags.raw ??= {};
|
|
1622
|
-
tags.raw[this.currentAttachedFile.fileUid.toString()] = new AttachedFile(
|
|
1623
|
-
this.currentAttachedFile.fileData,
|
|
1624
|
-
this.currentAttachedFile.fileMediaType ?? undefined,
|
|
1625
|
-
this.currentAttachedFile.fileName ?? undefined,
|
|
1626
|
-
this.currentAttachedFile.fileDescription ?? undefined,
|
|
1627
|
-
);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// Only process image attachments
|
|
1631
|
-
if (this.currentAttachedFile.fileMediaType?.startsWith('image/') && this.currentAttachedFile.fileData) {
|
|
1632
|
-
const fileName = this.currentAttachedFile.fileName;
|
|
1633
|
-
let kind: 'coverFront' | 'coverBack' | 'unknown' = 'unknown';
|
|
1634
|
-
|
|
1635
|
-
if (fileName) {
|
|
1636
|
-
const lowerName = fileName.toLowerCase();
|
|
1637
|
-
if (lowerName.startsWith('cover.')) {
|
|
1638
|
-
kind = 'coverFront';
|
|
1639
|
-
} else if (lowerName.startsWith('back.')) {
|
|
1640
|
-
kind = 'coverBack';
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
tags.images ??= [];
|
|
1645
|
-
tags.images.push({
|
|
1646
|
-
data: this.currentAttachedFile.fileData,
|
|
1647
|
-
mimeType: this.currentAttachedFile.fileMediaType,
|
|
1648
|
-
kind,
|
|
1649
|
-
name: this.currentAttachedFile.fileName ?? undefined,
|
|
1650
|
-
description: this.currentAttachedFile.fileDescription ?? undefined,
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
this.currentAttachedFile = null;
|
|
1655
|
-
}; break;
|
|
1656
|
-
|
|
1657
|
-
case EBMLId.FileUID: {
|
|
1658
|
-
if (!this.currentAttachedFile) break;
|
|
1659
|
-
|
|
1660
|
-
this.currentAttachedFile.fileUid = readUnsignedBigInt(slice, size);
|
|
1661
|
-
}; break;
|
|
1662
|
-
|
|
1663
|
-
case EBMLId.FileName: {
|
|
1664
|
-
if (!this.currentAttachedFile) break;
|
|
1665
|
-
|
|
1666
|
-
this.currentAttachedFile.fileName = readUnicodeString(slice, size);
|
|
1667
|
-
}; break;
|
|
1668
|
-
|
|
1669
|
-
case EBMLId.FileMediaType: {
|
|
1670
|
-
if (!this.currentAttachedFile) break;
|
|
1671
|
-
|
|
1672
|
-
this.currentAttachedFile.fileMediaType = readAsciiString(slice, size);
|
|
1673
|
-
}; break;
|
|
1674
|
-
|
|
1675
|
-
case EBMLId.FileData: {
|
|
1676
|
-
if (!this.currentAttachedFile) break;
|
|
1677
|
-
|
|
1678
|
-
this.currentAttachedFile.fileData = readBytes(slice, size);
|
|
1679
|
-
}; break;
|
|
1680
|
-
|
|
1681
|
-
case EBMLId.FileDescription: {
|
|
1682
|
-
if (!this.currentAttachedFile) break;
|
|
1683
|
-
|
|
1684
|
-
this.currentAttachedFile.fileDescription = readUnicodeString(slice, size);
|
|
1685
|
-
}; break;
|
|
1686
|
-
|
|
1687
|
-
case EBMLId.ContentEncodings: {
|
|
1688
|
-
if (!this.currentTrack) break;
|
|
1689
|
-
|
|
1690
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1691
|
-
|
|
1692
|
-
// "**MUST** start with the `ContentEncoding` with the highest `ContentEncodingOrder`"
|
|
1693
|
-
this.currentTrack.decodingInstructions.sort((a, b) => b.order - a.order);
|
|
1694
|
-
}; break;
|
|
1695
|
-
|
|
1696
|
-
case EBMLId.ContentEncoding: {
|
|
1697
|
-
this.currentDecodingInstruction = {
|
|
1698
|
-
order: 0,
|
|
1699
|
-
scope: ContentEncodingScope.Block,
|
|
1700
|
-
data: null,
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1704
|
-
|
|
1705
|
-
if (this.currentDecodingInstruction.data) {
|
|
1706
|
-
this.currentTrack!.decodingInstructions.push(this.currentDecodingInstruction);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
this.currentDecodingInstruction = null;
|
|
1710
|
-
}; break;
|
|
1711
|
-
|
|
1712
|
-
case EBMLId.ContentEncodingOrder: {
|
|
1713
|
-
if (!this.currentDecodingInstruction) break;
|
|
1714
|
-
|
|
1715
|
-
this.currentDecodingInstruction.order = readUnsignedInt(slice, size);
|
|
1716
|
-
}; break;
|
|
1717
|
-
|
|
1718
|
-
case EBMLId.ContentEncodingScope: {
|
|
1719
|
-
if (!this.currentDecodingInstruction) break;
|
|
1720
|
-
|
|
1721
|
-
this.currentDecodingInstruction.scope = readUnsignedInt(slice, size);
|
|
1722
|
-
}; break;
|
|
1723
|
-
|
|
1724
|
-
case EBMLId.ContentCompression: {
|
|
1725
|
-
if (!this.currentDecodingInstruction) break;
|
|
1726
|
-
|
|
1727
|
-
this.currentDecodingInstruction.data = {
|
|
1728
|
-
type: 'decompress',
|
|
1729
|
-
algorithm: ContentCompAlgo.Zlib,
|
|
1730
|
-
settings: null,
|
|
1731
|
-
};
|
|
1732
|
-
|
|
1733
|
-
this.readContiguousElements(slice.slice(dataStartPos, size));
|
|
1734
|
-
}; break;
|
|
1735
|
-
|
|
1736
|
-
case EBMLId.ContentCompAlgo: {
|
|
1737
|
-
if (this.currentDecodingInstruction?.data?.type !== 'decompress') break;
|
|
1738
|
-
|
|
1739
|
-
this.currentDecodingInstruction.data.algorithm = readUnsignedInt(slice, size);
|
|
1740
|
-
}; break;
|
|
1741
|
-
|
|
1742
|
-
case EBMLId.ContentCompSettings: {
|
|
1743
|
-
if (this.currentDecodingInstruction?.data?.type !== 'decompress') break;
|
|
1744
|
-
|
|
1745
|
-
this.currentDecodingInstruction.data.settings = readBytes(slice, size);
|
|
1746
|
-
}; break;
|
|
1747
|
-
|
|
1748
|
-
case EBMLId.ContentEncryption: {
|
|
1749
|
-
if (!this.currentDecodingInstruction) break;
|
|
1750
|
-
|
|
1751
|
-
this.currentDecodingInstruction.data = {
|
|
1752
|
-
type: 'decrypt',
|
|
1753
|
-
};
|
|
1754
|
-
}; break;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
slice.filePos = dataStartPos + size;
|
|
1758
|
-
return true;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
decodeBlockData(track: InternalTrack, rawData: Uint8Array) {
|
|
1762
|
-
assert(track.decodingInstructions.length > 0); // This method shouldn't be called otherwise
|
|
1763
|
-
|
|
1764
|
-
let currentData = rawData;
|
|
1765
|
-
|
|
1766
|
-
for (const instruction of track.decodingInstructions) {
|
|
1767
|
-
assert(instruction.data);
|
|
1768
|
-
|
|
1769
|
-
switch (instruction.data.type) {
|
|
1770
|
-
case 'decompress': {
|
|
1771
|
-
switch (instruction.data.algorithm) {
|
|
1772
|
-
case ContentCompAlgo.HeaderStripping: {
|
|
1773
|
-
if (instruction.data.settings && instruction.data.settings.length > 0) {
|
|
1774
|
-
const prefix = instruction.data.settings;
|
|
1775
|
-
const newData = new Uint8Array(prefix.length + currentData.length);
|
|
1776
|
-
|
|
1777
|
-
newData.set(prefix, 0);
|
|
1778
|
-
newData.set(currentData, prefix.length);
|
|
1779
|
-
|
|
1780
|
-
currentData = newData;
|
|
1781
|
-
}
|
|
1782
|
-
}; break;
|
|
1783
|
-
|
|
1784
|
-
default: {
|
|
1785
|
-
// Unhandled
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
}; break;
|
|
1789
|
-
|
|
1790
|
-
default: {
|
|
1791
|
-
// Unhandled
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
return currentData;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
processTagValue(name: string, value: string | Uint8Array) {
|
|
1800
|
-
if (!this.currentSegment?.metadataTags) return;
|
|
1801
|
-
|
|
1802
|
-
const metadataTags = this.currentSegment.metadataTags;
|
|
1803
|
-
metadataTags.raw ??= {};
|
|
1804
|
-
metadataTags.raw[name] ??= value;
|
|
1805
|
-
|
|
1806
|
-
if (typeof value === 'string') {
|
|
1807
|
-
switch (name.toLowerCase()) {
|
|
1808
|
-
case 'title': {
|
|
1809
|
-
metadataTags.title ??= value;
|
|
1810
|
-
}; break;
|
|
1811
|
-
|
|
1812
|
-
case 'description': {
|
|
1813
|
-
metadataTags.description ??= value;
|
|
1814
|
-
}; break;
|
|
1815
|
-
|
|
1816
|
-
case 'artist': {
|
|
1817
|
-
metadataTags.artist ??= value;
|
|
1818
|
-
}; break;
|
|
1819
|
-
|
|
1820
|
-
case 'album': {
|
|
1821
|
-
metadataTags.album ??= value;
|
|
1822
|
-
}; break;
|
|
1823
|
-
|
|
1824
|
-
case 'album_artist': {
|
|
1825
|
-
metadataTags.albumArtist ??= value;
|
|
1826
|
-
}; break;
|
|
1827
|
-
|
|
1828
|
-
case 'genre': {
|
|
1829
|
-
metadataTags.genre ??= value;
|
|
1830
|
-
}; break;
|
|
1831
|
-
|
|
1832
|
-
case 'comment': {
|
|
1833
|
-
metadataTags.comment ??= value;
|
|
1834
|
-
}; break;
|
|
1835
|
-
|
|
1836
|
-
case 'lyrics': {
|
|
1837
|
-
metadataTags.lyrics ??= value;
|
|
1838
|
-
}; break;
|
|
1839
|
-
|
|
1840
|
-
case 'date': {
|
|
1841
|
-
const date = new Date(value);
|
|
1842
|
-
if (!Number.isNaN(date.getTime())) {
|
|
1843
|
-
metadataTags.date ??= date;
|
|
1844
|
-
}
|
|
1845
|
-
}; break;
|
|
1846
|
-
|
|
1847
|
-
case 'track_number':
|
|
1848
|
-
case 'part_number': {
|
|
1849
|
-
const parts = value.split('/');
|
|
1850
|
-
const trackNum = Number.parseInt(parts[0]!, 10);
|
|
1851
|
-
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
|
|
1852
|
-
|
|
1853
|
-
if (Number.isInteger(trackNum) && trackNum > 0) {
|
|
1854
|
-
metadataTags.trackNumber ??= trackNum;
|
|
1855
|
-
}
|
|
1856
|
-
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
|
|
1857
|
-
metadataTags.tracksTotal ??= tracksTotal;
|
|
1858
|
-
}
|
|
1859
|
-
}; break;
|
|
1860
|
-
|
|
1861
|
-
case 'disc_number':
|
|
1862
|
-
case 'disc': {
|
|
1863
|
-
const discParts = value.split('/');
|
|
1864
|
-
const discNum = Number.parseInt(discParts[0]!, 10);
|
|
1865
|
-
const discsTotal = discParts[1] && Number.parseInt(discParts[1], 10);
|
|
1866
|
-
|
|
1867
|
-
if (Number.isInteger(discNum) && discNum > 0) {
|
|
1868
|
-
metadataTags.discNumber ??= discNum;
|
|
1869
|
-
}
|
|
1870
|
-
if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
|
|
1871
|
-
metadataTags.discsTotal ??= discsTotal;
|
|
1872
|
-
}
|
|
1873
|
-
}; break;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
abstract class MatroskaTrackBacking implements InputTrackBacking {
|
|
1880
|
-
packetToClusterLocation = new WeakMap<EncodedPacket, {
|
|
1881
|
-
cluster: Cluster;
|
|
1882
|
-
blockIndex: number;
|
|
1883
|
-
}>();
|
|
1884
|
-
|
|
1885
|
-
constructor(public internalTrack: InternalTrack) {}
|
|
1886
|
-
|
|
1887
|
-
getId() {
|
|
1888
|
-
return this.internalTrack.id;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
getCodec(): MediaCodec | null {
|
|
1892
|
-
throw new Error('Not implemented on base class.');
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
getInternalCodecId() {
|
|
1896
|
-
return this.internalTrack.codecId;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
async computeDuration() {
|
|
1900
|
-
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
|
|
1901
|
-
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
getName() {
|
|
1905
|
-
return this.internalTrack.name;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
getLanguageCode() {
|
|
1909
|
-
return this.internalTrack.languageCode;
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
async getFirstTimestamp() {
|
|
1913
|
-
const firstPacket = await this.getFirstPacket({ metadataOnly: true });
|
|
1914
|
-
return firstPacket?.timestamp ?? 0;
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
getTimeResolution() {
|
|
1918
|
-
return this.internalTrack.segment.timestampFactor;
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
getDisposition() {
|
|
1922
|
-
return this.internalTrack.disposition;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
async getFirstPacket(options: PacketRetrievalOptions) {
|
|
1926
|
-
return this.performClusterLookup(
|
|
1927
|
-
null,
|
|
1928
|
-
(cluster) => {
|
|
1929
|
-
const trackData = cluster.trackData.get(this.internalTrack.id);
|
|
1930
|
-
if (trackData) {
|
|
1931
|
-
return {
|
|
1932
|
-
blockIndex: 0,
|
|
1933
|
-
correctBlockFound: true,
|
|
1934
|
-
};
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
return {
|
|
1938
|
-
blockIndex: -1,
|
|
1939
|
-
correctBlockFound: false,
|
|
1940
|
-
};
|
|
1941
|
-
},
|
|
1942
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the cues
|
|
1943
|
-
Infinity,
|
|
1944
|
-
options,
|
|
1945
|
-
);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
private intoTimescale(timestamp: number) {
|
|
1949
|
-
// Do a little rounding to catch cases where the result is very close to an integer. If it is, it's likely
|
|
1950
|
-
// that the number was originally an integer divided by the timescale. For stability, it's best
|
|
1951
|
-
// to return the integer in this case.
|
|
1952
|
-
return roundIfAlmostInteger(timestamp * this.internalTrack.segment.timestampFactor);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
|
1956
|
-
const timestampInTimescale = this.intoTimescale(timestamp);
|
|
1957
|
-
|
|
1958
|
-
return this.performClusterLookup(
|
|
1959
|
-
null,
|
|
1960
|
-
(cluster) => {
|
|
1961
|
-
const trackData = cluster.trackData.get(this.internalTrack.id);
|
|
1962
|
-
if (!trackData) {
|
|
1963
|
-
return { blockIndex: -1, correctBlockFound: false };
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const index = binarySearchLessOrEqual(
|
|
1967
|
-
trackData.presentationTimestamps,
|
|
1968
|
-
timestampInTimescale,
|
|
1969
|
-
x => x.timestamp,
|
|
1970
|
-
);
|
|
1971
|
-
|
|
1972
|
-
const blockIndex = index !== -1 ? trackData.presentationTimestamps[index]!.blockIndex : -1;
|
|
1973
|
-
const correctBlockFound = index !== -1 && timestampInTimescale < trackData.endTimestamp;
|
|
1974
|
-
|
|
1975
|
-
return { blockIndex, correctBlockFound };
|
|
1976
|
-
},
|
|
1977
|
-
timestampInTimescale,
|
|
1978
|
-
timestampInTimescale,
|
|
1979
|
-
options,
|
|
1980
|
-
);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
|
1984
|
-
const locationInCluster = this.packetToClusterLocation.get(packet);
|
|
1985
|
-
if (locationInCluster === undefined) {
|
|
1986
|
-
throw new Error('Packet was not created from this track.');
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
return this.performClusterLookup(
|
|
1990
|
-
locationInCluster.cluster,
|
|
1991
|
-
(cluster) => {
|
|
1992
|
-
if (cluster === locationInCluster.cluster) {
|
|
1993
|
-
const trackData = cluster.trackData.get(this.internalTrack.id)!;
|
|
1994
|
-
if (locationInCluster.blockIndex + 1 < trackData.blocks.length) {
|
|
1995
|
-
// We can simply take the next block in the cluster
|
|
1996
|
-
return {
|
|
1997
|
-
blockIndex: locationInCluster.blockIndex + 1,
|
|
1998
|
-
correctBlockFound: true,
|
|
1999
|
-
};
|
|
2000
|
-
}
|
|
2001
|
-
} else {
|
|
2002
|
-
const trackData = cluster.trackData.get(this.internalTrack.id);
|
|
2003
|
-
if (trackData) {
|
|
2004
|
-
return {
|
|
2005
|
-
blockIndex: 0,
|
|
2006
|
-
correctBlockFound: true,
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
return {
|
|
2012
|
-
blockIndex: -1,
|
|
2013
|
-
correctBlockFound: false,
|
|
2014
|
-
};
|
|
2015
|
-
},
|
|
2016
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the cues
|
|
2017
|
-
Infinity,
|
|
2018
|
-
options,
|
|
2019
|
-
);
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
|
2023
|
-
const timestampInTimescale = this.intoTimescale(timestamp);
|
|
2024
|
-
|
|
2025
|
-
return this.performClusterLookup(
|
|
2026
|
-
null,
|
|
2027
|
-
(cluster) => {
|
|
2028
|
-
const trackData = cluster.trackData.get(this.internalTrack.id);
|
|
2029
|
-
if (!trackData) {
|
|
2030
|
-
return { blockIndex: -1, correctBlockFound: false };
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
|
|
2034
|
-
const block = trackData.blocks[x.blockIndex]!;
|
|
2035
|
-
return block.isKeyFrame && x.timestamp <= timestampInTimescale;
|
|
2036
|
-
});
|
|
2037
|
-
|
|
2038
|
-
const blockIndex = index !== -1 ? trackData.presentationTimestamps[index]!.blockIndex : -1;
|
|
2039
|
-
const correctBlockFound = index !== -1 && timestampInTimescale < trackData.endTimestamp;
|
|
2040
|
-
|
|
2041
|
-
return { blockIndex, correctBlockFound };
|
|
2042
|
-
},
|
|
2043
|
-
timestampInTimescale,
|
|
2044
|
-
timestampInTimescale,
|
|
2045
|
-
options,
|
|
2046
|
-
);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
|
2050
|
-
const locationInCluster = this.packetToClusterLocation.get(packet);
|
|
2051
|
-
if (locationInCluster === undefined) {
|
|
2052
|
-
throw new Error('Packet was not created from this track.');
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
return this.performClusterLookup(
|
|
2056
|
-
locationInCluster.cluster,
|
|
2057
|
-
(cluster) => {
|
|
2058
|
-
if (cluster === locationInCluster.cluster) {
|
|
2059
|
-
const trackData = cluster.trackData.get(this.internalTrack.id)!;
|
|
2060
|
-
const nextKeyFrameIndex = trackData.blocks.findIndex(
|
|
2061
|
-
(x, i) => x.isKeyFrame && i > locationInCluster.blockIndex,
|
|
2062
|
-
);
|
|
2063
|
-
|
|
2064
|
-
if (nextKeyFrameIndex !== -1) {
|
|
2065
|
-
// We can simply take the next key frame in the cluster
|
|
2066
|
-
return {
|
|
2067
|
-
blockIndex: nextKeyFrameIndex,
|
|
2068
|
-
correctBlockFound: true,
|
|
2069
|
-
};
|
|
2070
|
-
}
|
|
2071
|
-
} else {
|
|
2072
|
-
const trackData = cluster.trackData.get(this.internalTrack.id);
|
|
2073
|
-
if (trackData && trackData.firstKeyFrameTimestamp !== null) {
|
|
2074
|
-
const keyFrameIndex = trackData.blocks.findIndex(x => x.isKeyFrame);
|
|
2075
|
-
assert(keyFrameIndex !== -1); // There must be one
|
|
2076
|
-
|
|
2077
|
-
return {
|
|
2078
|
-
blockIndex: keyFrameIndex,
|
|
2079
|
-
correctBlockFound: true,
|
|
2080
|
-
};
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
return {
|
|
2085
|
-
blockIndex: -1,
|
|
2086
|
-
correctBlockFound: false,
|
|
2087
|
-
};
|
|
2088
|
-
},
|
|
2089
|
-
-Infinity, // Use -Infinity as a search timestamp to avoid using the cues
|
|
2090
|
-
Infinity,
|
|
2091
|
-
options,
|
|
2092
|
-
);
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
private async fetchPacketInCluster(cluster: Cluster, blockIndex: number, options: PacketRetrievalOptions) {
|
|
2096
|
-
if (blockIndex === -1) {
|
|
2097
|
-
return null;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
const trackData = cluster.trackData.get(this.internalTrack.id)!;
|
|
2101
|
-
const block = trackData.blocks[blockIndex];
|
|
2102
|
-
assert(block);
|
|
2103
|
-
|
|
2104
|
-
// Perform lazy decoding if needed
|
|
2105
|
-
if (!block.decoded) {
|
|
2106
|
-
block.data = this.internalTrack.demuxer.decodeBlockData(this.internalTrack, block.data);
|
|
2107
|
-
block.decoded = true;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data;
|
|
2111
|
-
const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor;
|
|
2112
|
-
const duration = block.duration / this.internalTrack.segment.timestampFactor;
|
|
2113
|
-
|
|
2114
|
-
const sideData: EncodedPacketSideData = {};
|
|
2115
|
-
if (block.mainAdditional && this.internalTrack.info?.type === 'video' && this.internalTrack.info.alphaMode) {
|
|
2116
|
-
sideData.alpha = options.metadataOnly ? PLACEHOLDER_DATA : block.mainAdditional;
|
|
2117
|
-
sideData.alphaByteLength = block.mainAdditional.byteLength;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
const packet = new EncodedPacket(
|
|
2121
|
-
data,
|
|
2122
|
-
block.isKeyFrame ? 'key' : 'delta',
|
|
2123
|
-
timestamp,
|
|
2124
|
-
duration,
|
|
2125
|
-
cluster.dataStartPos + blockIndex,
|
|
2126
|
-
block.data.byteLength,
|
|
2127
|
-
sideData,
|
|
2128
|
-
);
|
|
2129
|
-
|
|
2130
|
-
this.packetToClusterLocation.set(packet, { cluster, blockIndex });
|
|
2131
|
-
|
|
2132
|
-
return packet;
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
/** Looks for a packet in the clusters while trying to load as few clusters as possible to retrieve it. */
|
|
2136
|
-
private async performClusterLookup(
|
|
2137
|
-
// The cluster where we start looking
|
|
2138
|
-
startCluster: Cluster | null,
|
|
2139
|
-
// This function returns the best-matching block in a given cluster
|
|
2140
|
-
getMatchInCluster: (cluster: Cluster) => { blockIndex: number; correctBlockFound: boolean },
|
|
2141
|
-
// The timestamp with which we can search the lookup table
|
|
2142
|
-
searchTimestamp: number,
|
|
2143
|
-
// The timestamp for which we know the correct block will not come after it
|
|
2144
|
-
latestTimestamp: number,
|
|
2145
|
-
options: PacketRetrievalOptions,
|
|
2146
|
-
): Promise<EncodedPacket | null> {
|
|
2147
|
-
const { demuxer, segment } = this.internalTrack;
|
|
2148
|
-
|
|
2149
|
-
let currentCluster: Cluster | null = null;
|
|
2150
|
-
let bestCluster: Cluster | null = null;
|
|
2151
|
-
let bestBlockIndex = -1;
|
|
2152
|
-
|
|
2153
|
-
if (startCluster) {
|
|
2154
|
-
const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster);
|
|
2155
|
-
|
|
2156
|
-
if (correctBlockFound) {
|
|
2157
|
-
return this.fetchPacketInCluster(startCluster, blockIndex, options);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
if (blockIndex !== -1) {
|
|
2161
|
-
bestCluster = startCluster;
|
|
2162
|
-
bestBlockIndex = blockIndex;
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
// Search for a cue point; this way, we won't need to start searching from the start of the file
|
|
2167
|
-
// but can jump right into the correct cluster (or at least nearby).
|
|
2168
|
-
const cuePointIndex = binarySearchLessOrEqual(
|
|
2169
|
-
this.internalTrack.cuePoints,
|
|
2170
|
-
searchTimestamp,
|
|
2171
|
-
x => x.time,
|
|
2172
|
-
);
|
|
2173
|
-
const cuePoint = cuePointIndex !== -1
|
|
2174
|
-
? this.internalTrack.cuePoints[cuePointIndex]!
|
|
2175
|
-
: null;
|
|
2176
|
-
|
|
2177
|
-
// Also check the position cache
|
|
2178
|
-
const positionCacheIndex = binarySearchLessOrEqual(
|
|
2179
|
-
this.internalTrack.clusterPositionCache,
|
|
2180
|
-
searchTimestamp,
|
|
2181
|
-
x => x.startTimestamp,
|
|
2182
|
-
);
|
|
2183
|
-
const positionCacheEntry = positionCacheIndex !== -1
|
|
2184
|
-
? this.internalTrack.clusterPositionCache[positionCacheIndex]!
|
|
2185
|
-
: null;
|
|
2186
|
-
|
|
2187
|
-
const lookupEntryPosition = Math.max(
|
|
2188
|
-
cuePoint?.clusterPosition ?? 0,
|
|
2189
|
-
positionCacheEntry?.elementStartPos ?? 0,
|
|
2190
|
-
) || null;
|
|
2191
|
-
|
|
2192
|
-
let currentPos: number;
|
|
2193
|
-
|
|
2194
|
-
if (!startCluster) {
|
|
2195
|
-
currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos;
|
|
2196
|
-
} else {
|
|
2197
|
-
if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) {
|
|
2198
|
-
currentPos = startCluster.elementEndPos;
|
|
2199
|
-
currentCluster = startCluster;
|
|
2200
|
-
} else {
|
|
2201
|
-
// Use the lookup entry
|
|
2202
|
-
currentPos = lookupEntryPosition;
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) {
|
|
2207
|
-
if (currentCluster) {
|
|
2208
|
-
const trackData = currentCluster.trackData.get(this.internalTrack.id);
|
|
2209
|
-
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
|
2210
|
-
// We're already past the upper bound, no need to keep searching
|
|
2211
|
-
break;
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
// Load the header
|
|
2216
|
-
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
|
2217
|
-
if (slice instanceof Promise) slice = await slice;
|
|
2218
|
-
if (!slice) break;
|
|
2219
|
-
|
|
2220
|
-
const elementStartPos = currentPos;
|
|
2221
|
-
const elementHeader = readElementHeader(slice);
|
|
2222
|
-
|
|
2223
|
-
if (
|
|
2224
|
-
!elementHeader
|
|
2225
|
-
|| (!LEVEL_1_EBML_IDS.includes(elementHeader.id) && elementHeader.id !== EBMLId.Void)
|
|
2226
|
-
) {
|
|
2227
|
-
// There's an element here that shouldn't be here. Might be garbage. In this case, let's
|
|
2228
|
-
// try and resync to the next valid element.
|
|
2229
|
-
const nextPos = await resync(
|
|
2230
|
-
demuxer.reader,
|
|
2231
|
-
elementStartPos,
|
|
2232
|
-
LEVEL_1_EBML_IDS,
|
|
2233
|
-
Math.min(segment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH),
|
|
2234
|
-
);
|
|
2235
|
-
|
|
2236
|
-
if (nextPos) {
|
|
2237
|
-
currentPos = nextPos;
|
|
2238
|
-
continue;
|
|
2239
|
-
} else {
|
|
2240
|
-
break; // Resync failed
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
const id = elementHeader.id;
|
|
2245
|
-
let size = elementHeader.size;
|
|
2246
|
-
const dataStartPos = slice.filePos;
|
|
2247
|
-
|
|
2248
|
-
if (id === EBMLId.Cluster) {
|
|
2249
|
-
currentCluster = await demuxer.readCluster(elementStartPos, segment);
|
|
2250
|
-
// readCluster computes the proper size even if it's undefined in the header, so let's use that instead
|
|
2251
|
-
size = currentCluster.elementEndPos - dataStartPos;
|
|
2252
|
-
|
|
2253
|
-
const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster);
|
|
2254
|
-
if (correctBlockFound) {
|
|
2255
|
-
return this.fetchPacketInCluster(currentCluster, blockIndex, options);
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
if (blockIndex !== -1) {
|
|
2259
|
-
bestCluster = currentCluster;
|
|
2260
|
-
bestBlockIndex = blockIndex;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
if (size === null) {
|
|
2265
|
-
// Undefined element size (can happen in livestreamed files). In this case, we need to do some
|
|
2266
|
-
// searching to determine the actual size of the element.
|
|
2267
|
-
|
|
2268
|
-
assert(id !== EBMLId.Cluster); // Undefined cluster sizes are fixed further up
|
|
2269
|
-
|
|
2270
|
-
// Search for the next element at level 0 or 1
|
|
2271
|
-
const nextElementPos = await searchForNextElementId(
|
|
2272
|
-
demuxer.reader,
|
|
2273
|
-
dataStartPos,
|
|
2274
|
-
LEVEL_0_AND_1_EBML_IDS,
|
|
2275
|
-
segment.elementEndPos,
|
|
2276
|
-
);
|
|
2277
|
-
|
|
2278
|
-
size = nextElementPos.pos - dataStartPos;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
const endPos = dataStartPos + size;
|
|
2282
|
-
if (segment.elementEndPos === null) {
|
|
2283
|
-
// Check the next element. If it's a new segment, we know this segment ends here. The new
|
|
2284
|
-
// segment is just ignored, since we're likely in a livestreamed file and thus only care about
|
|
2285
|
-
// the first segment.
|
|
2286
|
-
|
|
2287
|
-
let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
|
2288
|
-
if (slice instanceof Promise) slice = await slice;
|
|
2289
|
-
if (!slice) break;
|
|
2290
|
-
|
|
2291
|
-
const elementId = readElementId(slice);
|
|
2292
|
-
if (elementId === EBMLId.Segment) {
|
|
2293
|
-
segment.elementEndPos = endPos; // We now know the segment's size
|
|
2294
|
-
break;
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
currentPos = endPos;
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
// Catch faulty cue points
|
|
2302
|
-
if (cuePoint && (!bestCluster || bestCluster.elementStartPos < cuePoint.clusterPosition)) {
|
|
2303
|
-
// The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this
|
|
2304
|
-
// case, let's search again but using the cue point before that.
|
|
2305
|
-
const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1];
|
|
2306
|
-
assert(!previousCuePoint || previousCuePoint.time < cuePoint.time);
|
|
2307
|
-
|
|
2308
|
-
const newSearchTimestamp = previousCuePoint?.time ?? -Infinity;
|
|
2309
|
-
return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options);
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
if (bestCluster) {
|
|
2313
|
-
// If we finished looping but didn't find a perfect match, still return the best match we found
|
|
2314
|
-
return this.fetchPacketInCluster(bestCluster, bestBlockIndex, options);
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
return null;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVideoTrackBacking {
|
|
2322
|
-
override internalTrack: InternalVideoTrack;
|
|
2323
|
-
decoderConfigPromise: Promise<VideoDecoderConfig> | null = null;
|
|
2324
|
-
|
|
2325
|
-
constructor(internalTrack: InternalVideoTrack) {
|
|
2326
|
-
super(internalTrack);
|
|
2327
|
-
this.internalTrack = internalTrack;
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
override getCodec(): VideoCodec | null {
|
|
2331
|
-
return this.internalTrack.info.codec;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
getCodedWidth() {
|
|
2335
|
-
return this.internalTrack.info.width;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
getCodedHeight() {
|
|
2339
|
-
return this.internalTrack.info.height;
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
getRotation() {
|
|
2343
|
-
return this.internalTrack.info.rotation;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
async getColorSpace(): Promise<VideoColorSpaceInit> {
|
|
2347
|
-
return {
|
|
2348
|
-
primaries: this.internalTrack.info.colorSpace?.primaries,
|
|
2349
|
-
transfer: this.internalTrack.info.colorSpace?.transfer,
|
|
2350
|
-
matrix: this.internalTrack.info.colorSpace?.matrix,
|
|
2351
|
-
fullRange: this.internalTrack.info.colorSpace?.fullRange,
|
|
2352
|
-
};
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
async canBeTransparent() {
|
|
2356
|
-
return this.internalTrack.info.alphaMode;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
async getDecoderConfig(): Promise<VideoDecoderConfig | null> {
|
|
2360
|
-
if (!this.internalTrack.info.codec) {
|
|
2361
|
-
return null;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
return this.decoderConfigPromise ??= (async (): Promise<VideoDecoderConfig> => {
|
|
2365
|
-
let firstPacket: EncodedPacket | null = null;
|
|
2366
|
-
const needsPacketForAdditionalInfo
|
|
2367
|
-
= this.internalTrack.info.codec === 'vp9'
|
|
2368
|
-
|| this.internalTrack.info.codec === 'av1'
|
|
2369
|
-
// Packets are in Annex B format:
|
|
2370
|
-
|| (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription)
|
|
2371
|
-
// Packets are in Annex B format:
|
|
2372
|
-
|| (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription);
|
|
2373
|
-
|
|
2374
|
-
if (needsPacketForAdditionalInfo) {
|
|
2375
|
-
firstPacket = await this.getFirstPacket({});
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
return {
|
|
2379
|
-
codec: extractVideoCodecString({
|
|
2380
|
-
width: this.internalTrack.info.width,
|
|
2381
|
-
height: this.internalTrack.info.height,
|
|
2382
|
-
codec: this.internalTrack.info.codec,
|
|
2383
|
-
codecDescription: this.internalTrack.info.codecDescription,
|
|
2384
|
-
colorSpace: this.internalTrack.info.colorSpace,
|
|
2385
|
-
avcType: 1, // We don't know better (or do we?) so just assume 'avc1'
|
|
2386
|
-
avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket
|
|
2387
|
-
? extractAvcDecoderConfigurationRecord(firstPacket.data)
|
|
2388
|
-
: null,
|
|
2389
|
-
hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket
|
|
2390
|
-
? extractHevcDecoderConfigurationRecord(firstPacket.data)
|
|
2391
|
-
: null,
|
|
2392
|
-
vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket
|
|
2393
|
-
? extractVp9CodecInfoFromPacket(firstPacket.data)
|
|
2394
|
-
: null,
|
|
2395
|
-
av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket
|
|
2396
|
-
? extractAv1CodecInfoFromPacket(firstPacket.data)
|
|
2397
|
-
: null,
|
|
2398
|
-
}),
|
|
2399
|
-
codedWidth: this.internalTrack.info.width,
|
|
2400
|
-
codedHeight: this.internalTrack.info.height,
|
|
2401
|
-
description: this.internalTrack.info.codecDescription ?? undefined,
|
|
2402
|
-
colorSpace: this.internalTrack.info.colorSpace ?? undefined,
|
|
2403
|
-
};
|
|
2404
|
-
})();
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
class MatroskaAudioTrackBacking extends MatroskaTrackBacking implements InputAudioTrackBacking {
|
|
2409
|
-
override internalTrack: InternalAudioTrack;
|
|
2410
|
-
decoderConfig: AudioDecoderConfig | null = null;
|
|
2411
|
-
|
|
2412
|
-
constructor(internalTrack: InternalAudioTrack) {
|
|
2413
|
-
super(internalTrack);
|
|
2414
|
-
this.internalTrack = internalTrack;
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
override getCodec(): AudioCodec | null {
|
|
2418
|
-
return this.internalTrack.info.codec;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
getNumberOfChannels() {
|
|
2422
|
-
return this.internalTrack.info.numberOfChannels;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
getSampleRate() {
|
|
2426
|
-
return this.internalTrack.info.sampleRate;
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
async getDecoderConfig(): Promise<AudioDecoderConfig | null> {
|
|
2430
|
-
if (!this.internalTrack.info.codec) {
|
|
2431
|
-
return null;
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
return this.decoderConfig ??= {
|
|
2435
|
-
codec: extractAudioCodecString({
|
|
2436
|
-
codec: this.internalTrack.info.codec,
|
|
2437
|
-
codecDescription: this.internalTrack.info.codecDescription,
|
|
2438
|
-
aacCodecInfo: this.internalTrack.info.aacCodecInfo,
|
|
2439
|
-
}),
|
|
2440
|
-
numberOfChannels: this.internalTrack.info.numberOfChannels,
|
|
2441
|
-
sampleRate: this.internalTrack.info.sampleRate,
|
|
2442
|
-
description: this.internalTrack.info.codecDescription ?? undefined,
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
class MatroskaSubtitleTrackBacking extends MatroskaTrackBacking implements InputSubtitleTrackBacking {
|
|
2448
|
-
override internalTrack: InternalSubtitleTrack;
|
|
2449
|
-
|
|
2450
|
-
constructor(internalTrack: InternalSubtitleTrack) {
|
|
2451
|
-
super(internalTrack);
|
|
2452
|
-
this.internalTrack = internalTrack;
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
override getCodec(): SubtitleCodec | null {
|
|
2456
|
-
return this.internalTrack.info.codec;
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
getCodecPrivate(): string | null {
|
|
2460
|
-
return this.internalTrack.info.codecPrivateText;
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
async *getCues(): AsyncGenerator<SubtitleCue> {
|
|
2464
|
-
// Use the existing packet reading infrastructure
|
|
2465
|
-
let packet = await this.getFirstPacket({});
|
|
2466
|
-
|
|
2467
|
-
while (packet) {
|
|
2468
|
-
// Decode subtitle data as UTF-8 text
|
|
2469
|
-
const decoder = new TextDecoder('utf-8');
|
|
2470
|
-
const text = decoder.decode(packet.data);
|
|
2471
|
-
|
|
2472
|
-
yield {
|
|
2473
|
-
timestamp: packet.timestamp,
|
|
2474
|
-
duration: packet.duration,
|
|
2475
|
-
text,
|
|
2476
|
-
};
|
|
2477
|
-
|
|
2478
|
-
packet = await this.getNextPacket(packet, {});
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
}
|