@kenzuya/mediabunny 1.26.0 → 1.28.5

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