@kenzuya/mediabunny 1.26.0 → 1.28.6

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