@kenzuya/mediabunny 1.26.0 → 1.28.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +1 -1
  2. package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21388
  3. package/dist/bundles/mediabunny.min.js +490 -0
  4. package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
  5. package/dist/modules/src/adts/adts-demuxer.d.ts +6 -6
  6. package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
  7. package/dist/modules/src/adts/adts-muxer.d.ts +4 -4
  8. package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
  9. package/dist/modules/src/adts/adts-reader.d.ts +1 -1
  10. package/dist/modules/src/adts/adts-reader.d.ts.map +1 -1
  11. package/dist/modules/src/avi/avi-demuxer.d.ts +44 -0
  12. package/dist/modules/src/avi/avi-demuxer.d.ts.map +1 -0
  13. package/dist/modules/src/avi/avi-misc.d.ts +88 -0
  14. package/dist/modules/src/avi/avi-misc.d.ts.map +1 -0
  15. package/dist/modules/src/avi/avi-muxer.d.ts +45 -0
  16. package/dist/modules/src/avi/avi-muxer.d.ts.map +1 -0
  17. package/dist/modules/src/avi/riff-writer.d.ts +26 -0
  18. package/dist/modules/src/avi/riff-writer.d.ts.map +1 -0
  19. package/dist/modules/src/codec-data.d.ts +8 -3
  20. package/dist/modules/src/codec-data.d.ts.map +1 -1
  21. package/dist/modules/src/codec.d.ts +10 -10
  22. package/dist/modules/src/codec.d.ts.map +1 -1
  23. package/dist/modules/src/conversion.d.ts +33 -16
  24. package/dist/modules/src/conversion.d.ts.map +1 -1
  25. package/dist/modules/src/custom-coder.d.ts +8 -8
  26. package/dist/modules/src/custom-coder.d.ts.map +1 -1
  27. package/dist/modules/src/demuxer.d.ts +3 -3
  28. package/dist/modules/src/demuxer.d.ts.map +1 -1
  29. package/dist/modules/src/encode.d.ts +8 -8
  30. package/dist/modules/src/encode.d.ts.map +1 -1
  31. package/dist/modules/src/flac/flac-demuxer.d.ts +7 -7
  32. package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
  33. package/dist/modules/src/flac/flac-misc.d.ts +3 -3
  34. package/dist/modules/src/flac/flac-misc.d.ts.map +1 -1
  35. package/dist/modules/src/flac/flac-muxer.d.ts +5 -5
  36. package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
  37. package/dist/modules/src/id3.d.ts +3 -3
  38. package/dist/modules/src/id3.d.ts.map +1 -1
  39. package/dist/modules/src/index.d.ts +20 -20
  40. package/dist/modules/src/index.d.ts.map +1 -1
  41. package/dist/modules/src/input-format.d.ts +22 -0
  42. package/dist/modules/src/input-format.d.ts.map +1 -1
  43. package/dist/modules/src/input-track.d.ts +8 -8
  44. package/dist/modules/src/input-track.d.ts.map +1 -1
  45. package/dist/modules/src/input.d.ts +12 -12
  46. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -2
  47. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  48. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +12 -12
  49. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  50. package/dist/modules/src/isobmff/isobmff-misc.d.ts.map +1 -1
  51. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +11 -11
  52. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  53. package/dist/modules/src/isobmff/isobmff-reader.d.ts +2 -2
  54. package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
  55. package/dist/modules/src/matroska/ebml.d.ts +3 -3
  56. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  57. package/dist/modules/src/matroska/matroska-demuxer.d.ts +13 -13
  58. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  59. package/dist/modules/src/matroska/matroska-input.d.ts +33 -0
  60. package/dist/modules/src/matroska/matroska-input.d.ts.map +1 -0
  61. package/dist/modules/src/matroska/matroska-misc.d.ts.map +1 -1
  62. package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -5
  63. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  64. package/dist/modules/src/media-sink.d.ts +5 -5
  65. package/dist/modules/src/media-sink.d.ts.map +1 -1
  66. package/dist/modules/src/media-source.d.ts +22 -4
  67. package/dist/modules/src/media-source.d.ts.map +1 -1
  68. package/dist/modules/src/metadata.d.ts +2 -2
  69. package/dist/modules/src/metadata.d.ts.map +1 -1
  70. package/dist/modules/src/misc.d.ts +5 -4
  71. package/dist/modules/src/misc.d.ts.map +1 -1
  72. package/dist/modules/src/mp3/mp3-demuxer.d.ts +7 -7
  73. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  74. package/dist/modules/src/mp3/mp3-muxer.d.ts +4 -4
  75. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  76. package/dist/modules/src/mp3/mp3-reader.d.ts +2 -2
  77. package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
  78. package/dist/modules/src/mp3/mp3-writer.d.ts +1 -1
  79. package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
  80. package/dist/modules/src/muxer.d.ts +4 -4
  81. package/dist/modules/src/muxer.d.ts.map +1 -1
  82. package/dist/modules/src/node.d.ts +1 -1
  83. package/dist/modules/src/ogg/ogg-demuxer.d.ts +7 -7
  84. package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
  85. package/dist/modules/src/ogg/ogg-misc.d.ts +1 -1
  86. package/dist/modules/src/ogg/ogg-misc.d.ts.map +1 -1
  87. package/dist/modules/src/ogg/ogg-muxer.d.ts +5 -5
  88. package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
  89. package/dist/modules/src/ogg/ogg-reader.d.ts +1 -1
  90. package/dist/modules/src/ogg/ogg-reader.d.ts.map +1 -1
  91. package/dist/modules/src/output-format.d.ts +51 -6
  92. package/dist/modules/src/output-format.d.ts.map +1 -1
  93. package/dist/modules/src/output.d.ts +13 -13
  94. package/dist/modules/src/output.d.ts.map +1 -1
  95. package/dist/modules/src/packet.d.ts +1 -1
  96. package/dist/modules/src/packet.d.ts.map +1 -1
  97. package/dist/modules/src/pcm.d.ts.map +1 -1
  98. package/dist/modules/src/reader.d.ts +2 -2
  99. package/dist/modules/src/reader.d.ts.map +1 -1
  100. package/dist/modules/src/sample.d.ts +57 -15
  101. package/dist/modules/src/sample.d.ts.map +1 -1
  102. package/dist/modules/src/source.d.ts +3 -3
  103. package/dist/modules/src/source.d.ts.map +1 -1
  104. package/dist/modules/src/subtitles.d.ts +1 -1
  105. package/dist/modules/src/subtitles.d.ts.map +1 -1
  106. package/dist/modules/src/target.d.ts +2 -2
  107. package/dist/modules/src/target.d.ts.map +1 -1
  108. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  109. package/dist/modules/src/wave/riff-writer.d.ts +1 -1
  110. package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
  111. package/dist/modules/src/wave/wave-demuxer.d.ts +6 -6
  112. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  113. package/dist/modules/src/wave/wave-muxer.d.ts +4 -4
  114. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  115. package/dist/modules/src/writer.d.ts +1 -1
  116. package/dist/modules/src/writer.d.ts.map +1 -1
  117. package/dist/packages/eac3/eac3.wasm +0 -0
  118. package/dist/packages/eac3/mediabunny-eac3.js +1058 -0
  119. package/dist/packages/eac3/mediabunny-eac3.min.js +44 -0
  120. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.js +694 -0
  121. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.min.js +58 -0
  122. package/dist/packages/mpeg4/mediabunny-mpeg4.js +1198 -0
  123. package/dist/packages/mpeg4/mediabunny-mpeg4.min.js +44 -0
  124. package/dist/packages/mpeg4/xvid.wasm +0 -0
  125. package/package.json +18 -57
  126. package/dist/bundles/mediabunny.cjs +0 -26140
  127. package/dist/bundles/mediabunny.min.cjs +0 -147
  128. package/dist/bundles/mediabunny.min.mjs +0 -146
  129. package/dist/mediabunny.d.ts +0 -3319
  130. package/dist/modules/shared/mp3-misc.js +0 -147
  131. package/dist/modules/src/adts/adts-demuxer.js +0 -239
  132. package/dist/modules/src/adts/adts-muxer.js +0 -80
  133. package/dist/modules/src/adts/adts-reader.js +0 -63
  134. package/dist/modules/src/codec-data.js +0 -1730
  135. package/dist/modules/src/codec.js +0 -869
  136. package/dist/modules/src/conversion.js +0 -1459
  137. package/dist/modules/src/custom-coder.js +0 -117
  138. package/dist/modules/src/demuxer.js +0 -12
  139. package/dist/modules/src/encode.js +0 -442
  140. package/dist/modules/src/flac/flac-demuxer.js +0 -504
  141. package/dist/modules/src/flac/flac-misc.js +0 -135
  142. package/dist/modules/src/flac/flac-muxer.js +0 -222
  143. package/dist/modules/src/id3.js +0 -848
  144. package/dist/modules/src/index.js +0 -28
  145. package/dist/modules/src/input-format.js +0 -480
  146. package/dist/modules/src/input-track.js +0 -372
  147. package/dist/modules/src/input.js +0 -188
  148. package/dist/modules/src/isobmff/isobmff-boxes.js +0 -1480
  149. package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -2618
  150. package/dist/modules/src/isobmff/isobmff-misc.js +0 -20
  151. package/dist/modules/src/isobmff/isobmff-muxer.js +0 -966
  152. package/dist/modules/src/isobmff/isobmff-reader.js +0 -72
  153. package/dist/modules/src/matroska/ebml.js +0 -653
  154. package/dist/modules/src/matroska/matroska-demuxer.js +0 -2133
  155. package/dist/modules/src/matroska/matroska-misc.js +0 -20
  156. package/dist/modules/src/matroska/matroska-muxer.js +0 -1017
  157. package/dist/modules/src/media-sink.js +0 -1736
  158. package/dist/modules/src/media-source.js +0 -1825
  159. package/dist/modules/src/metadata.js +0 -193
  160. package/dist/modules/src/misc.js +0 -623
  161. package/dist/modules/src/mp3/mp3-demuxer.js +0 -285
  162. package/dist/modules/src/mp3/mp3-muxer.js +0 -123
  163. package/dist/modules/src/mp3/mp3-reader.js +0 -26
  164. package/dist/modules/src/mp3/mp3-writer.js +0 -78
  165. package/dist/modules/src/muxer.js +0 -50
  166. package/dist/modules/src/node.js +0 -9
  167. package/dist/modules/src/ogg/ogg-demuxer.js +0 -763
  168. package/dist/modules/src/ogg/ogg-misc.js +0 -78
  169. package/dist/modules/src/ogg/ogg-muxer.js +0 -353
  170. package/dist/modules/src/ogg/ogg-reader.js +0 -65
  171. package/dist/modules/src/output-format.js +0 -527
  172. package/dist/modules/src/output.js +0 -300
  173. package/dist/modules/src/packet.js +0 -182
  174. package/dist/modules/src/pcm.js +0 -85
  175. package/dist/modules/src/reader.js +0 -236
  176. package/dist/modules/src/sample.js +0 -1056
  177. package/dist/modules/src/source.js +0 -1182
  178. package/dist/modules/src/subtitles.js +0 -575
  179. package/dist/modules/src/target.js +0 -140
  180. package/dist/modules/src/wave/riff-writer.js +0 -30
  181. package/dist/modules/src/wave/wave-demuxer.js +0 -447
  182. package/dist/modules/src/wave/wave-muxer.js +0 -318
  183. package/dist/modules/src/writer.js +0 -370
  184. package/src/adts/adts-demuxer.ts +0 -331
  185. package/src/adts/adts-muxer.ts +0 -111
  186. package/src/adts/adts-reader.ts +0 -85
  187. package/src/codec-data.ts +0 -2078
  188. package/src/codec.ts +0 -1092
  189. package/src/conversion.ts +0 -2112
  190. package/src/custom-coder.ts +0 -197
  191. package/src/demuxer.ts +0 -24
  192. package/src/encode.ts +0 -739
  193. package/src/flac/flac-demuxer.ts +0 -730
  194. package/src/flac/flac-misc.ts +0 -164
  195. package/src/flac/flac-muxer.ts +0 -320
  196. package/src/id3.ts +0 -925
  197. package/src/index.ts +0 -221
  198. package/src/input-format.ts +0 -541
  199. package/src/input-track.ts +0 -529
  200. package/src/input.ts +0 -235
  201. package/src/isobmff/isobmff-boxes.ts +0 -1719
  202. package/src/isobmff/isobmff-demuxer.ts +0 -3190
  203. package/src/isobmff/isobmff-misc.ts +0 -29
  204. package/src/isobmff/isobmff-muxer.ts +0 -1348
  205. package/src/isobmff/isobmff-reader.ts +0 -91
  206. package/src/matroska/ebml.ts +0 -730
  207. package/src/matroska/matroska-demuxer.ts +0 -2481
  208. package/src/matroska/matroska-misc.ts +0 -29
  209. package/src/matroska/matroska-muxer.ts +0 -1276
  210. package/src/media-sink.ts +0 -2179
  211. package/src/media-source.ts +0 -2243
  212. package/src/metadata.ts +0 -320
  213. package/src/misc.ts +0 -798
  214. package/src/mp3/mp3-demuxer.ts +0 -383
  215. package/src/mp3/mp3-muxer.ts +0 -166
  216. package/src/mp3/mp3-reader.ts +0 -34
  217. package/src/mp3/mp3-writer.ts +0 -120
  218. package/src/muxer.ts +0 -88
  219. package/src/node.ts +0 -11
  220. package/src/ogg/ogg-demuxer.ts +0 -1053
  221. package/src/ogg/ogg-misc.ts +0 -116
  222. package/src/ogg/ogg-muxer.ts +0 -497
  223. package/src/ogg/ogg-reader.ts +0 -93
  224. package/src/output-format.ts +0 -945
  225. package/src/output.ts +0 -488
  226. package/src/packet.ts +0 -263
  227. package/src/pcm.ts +0 -112
  228. package/src/reader.ts +0 -323
  229. package/src/sample.ts +0 -1461
  230. package/src/source.ts +0 -1688
  231. package/src/subtitles.ts +0 -711
  232. package/src/target.ts +0 -204
  233. package/src/tsconfig.json +0 -16
  234. package/src/wave/riff-writer.ts +0 -36
  235. package/src/wave/wave-demuxer.ts +0 -529
  236. package/src/wave/wave-muxer.ts +0 -371
  237. package/src/writer.ts +0 -490
@@ -1,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
- }