@kenzuya/mediabunny 1.26.0 → 1.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +1 -1
  2. package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21390
  3. package/dist/bundles/mediabunny.min.js +490 -0
  4. package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
  5. package/dist/modules/src/adts/adts-demuxer.d.ts +6 -6
  6. package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
  7. package/dist/modules/src/adts/adts-muxer.d.ts +4 -4
  8. package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
  9. package/dist/modules/src/adts/adts-reader.d.ts +1 -1
  10. package/dist/modules/src/adts/adts-reader.d.ts.map +1 -1
  11. package/dist/modules/src/avi/avi-demuxer.d.ts +44 -0
  12. package/dist/modules/src/avi/avi-demuxer.d.ts.map +1 -0
  13. package/dist/modules/src/avi/avi-misc.d.ts +88 -0
  14. package/dist/modules/src/avi/avi-misc.d.ts.map +1 -0
  15. package/dist/modules/src/avi/avi-muxer.d.ts +45 -0
  16. package/dist/modules/src/avi/avi-muxer.d.ts.map +1 -0
  17. package/dist/modules/src/avi/riff-writer.d.ts +26 -0
  18. package/dist/modules/src/avi/riff-writer.d.ts.map +1 -0
  19. package/dist/modules/src/codec-data.d.ts +8 -3
  20. package/dist/modules/src/codec-data.d.ts.map +1 -1
  21. package/dist/modules/src/codec.d.ts +10 -10
  22. package/dist/modules/src/codec.d.ts.map +1 -1
  23. package/dist/modules/src/conversion.d.ts +33 -16
  24. package/dist/modules/src/conversion.d.ts.map +1 -1
  25. package/dist/modules/src/custom-coder.d.ts +8 -8
  26. package/dist/modules/src/custom-coder.d.ts.map +1 -1
  27. package/dist/modules/src/demuxer.d.ts +3 -3
  28. package/dist/modules/src/demuxer.d.ts.map +1 -1
  29. package/dist/modules/src/encode.d.ts +8 -8
  30. package/dist/modules/src/encode.d.ts.map +1 -1
  31. package/dist/modules/src/flac/flac-demuxer.d.ts +7 -7
  32. package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
  33. package/dist/modules/src/flac/flac-misc.d.ts +3 -3
  34. package/dist/modules/src/flac/flac-misc.d.ts.map +1 -1
  35. package/dist/modules/src/flac/flac-muxer.d.ts +5 -5
  36. package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
  37. package/dist/modules/src/id3.d.ts +3 -3
  38. package/dist/modules/src/id3.d.ts.map +1 -1
  39. package/dist/modules/src/index.d.ts +20 -20
  40. package/dist/modules/src/index.d.ts.map +1 -1
  41. package/dist/modules/src/input-format.d.ts +22 -0
  42. package/dist/modules/src/input-format.d.ts.map +1 -1
  43. package/dist/modules/src/input-track.d.ts +8 -8
  44. package/dist/modules/src/input-track.d.ts.map +1 -1
  45. package/dist/modules/src/input.d.ts +12 -12
  46. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -2
  47. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  48. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +12 -12
  49. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  50. package/dist/modules/src/isobmff/isobmff-misc.d.ts.map +1 -1
  51. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +11 -11
  52. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  53. package/dist/modules/src/isobmff/isobmff-reader.d.ts +2 -2
  54. package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
  55. package/dist/modules/src/matroska/ebml.d.ts +3 -3
  56. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  57. package/dist/modules/src/matroska/matroska-demuxer.d.ts +13 -13
  58. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  59. package/dist/modules/src/matroska/matroska-input.d.ts +33 -0
  60. package/dist/modules/src/matroska/matroska-input.d.ts.map +1 -0
  61. package/dist/modules/src/matroska/matroska-misc.d.ts.map +1 -1
  62. package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -5
  63. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  64. package/dist/modules/src/media-sink.d.ts +5 -5
  65. package/dist/modules/src/media-sink.d.ts.map +1 -1
  66. package/dist/modules/src/media-source.d.ts +22 -4
  67. package/dist/modules/src/media-source.d.ts.map +1 -1
  68. package/dist/modules/src/metadata.d.ts +2 -2
  69. package/dist/modules/src/metadata.d.ts.map +1 -1
  70. package/dist/modules/src/misc.d.ts +5 -4
  71. package/dist/modules/src/misc.d.ts.map +1 -1
  72. package/dist/modules/src/mp3/mp3-demuxer.d.ts +7 -7
  73. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  74. package/dist/modules/src/mp3/mp3-muxer.d.ts +4 -4
  75. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  76. package/dist/modules/src/mp3/mp3-reader.d.ts +2 -2
  77. package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
  78. package/dist/modules/src/mp3/mp3-writer.d.ts +1 -1
  79. package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
  80. package/dist/modules/src/muxer.d.ts +4 -4
  81. package/dist/modules/src/muxer.d.ts.map +1 -1
  82. package/dist/modules/src/ogg/ogg-demuxer.d.ts +7 -7
  83. package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
  84. package/dist/modules/src/ogg/ogg-misc.d.ts +1 -1
  85. package/dist/modules/src/ogg/ogg-misc.d.ts.map +1 -1
  86. package/dist/modules/src/ogg/ogg-muxer.d.ts +5 -5
  87. package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
  88. package/dist/modules/src/ogg/ogg-reader.d.ts +1 -1
  89. package/dist/modules/src/ogg/ogg-reader.d.ts.map +1 -1
  90. package/dist/modules/src/output-format.d.ts +51 -6
  91. package/dist/modules/src/output-format.d.ts.map +1 -1
  92. package/dist/modules/src/output.d.ts +13 -13
  93. package/dist/modules/src/output.d.ts.map +1 -1
  94. package/dist/modules/src/packet.d.ts +1 -1
  95. package/dist/modules/src/packet.d.ts.map +1 -1
  96. package/dist/modules/src/pcm.d.ts.map +1 -1
  97. package/dist/modules/src/reader.d.ts +2 -2
  98. package/dist/modules/src/reader.d.ts.map +1 -1
  99. package/dist/modules/src/sample.d.ts +57 -15
  100. package/dist/modules/src/sample.d.ts.map +1 -1
  101. package/dist/modules/src/source.d.ts +3 -3
  102. package/dist/modules/src/source.d.ts.map +1 -1
  103. package/dist/modules/src/subtitles.d.ts +1 -1
  104. package/dist/modules/src/subtitles.d.ts.map +1 -1
  105. package/dist/modules/src/target.d.ts +2 -2
  106. package/dist/modules/src/target.d.ts.map +1 -1
  107. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  108. package/dist/modules/src/wave/riff-writer.d.ts +1 -1
  109. package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
  110. package/dist/modules/src/wave/wave-demuxer.d.ts +6 -6
  111. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  112. package/dist/modules/src/wave/wave-muxer.d.ts +4 -4
  113. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  114. package/dist/modules/src/writer.d.ts +1 -1
  115. package/dist/modules/src/writer.d.ts.map +1 -1
  116. package/dist/packages/eac3/eac3.wasm +0 -0
  117. package/dist/packages/eac3/mediabunny-eac3.js +1058 -0
  118. package/dist/packages/eac3/mediabunny-eac3.min.js +44 -0
  119. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.js +694 -0
  120. package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.min.js +58 -0
  121. package/dist/packages/mpeg4/mediabunny-mpeg4.js +1198 -0
  122. package/dist/packages/mpeg4/mediabunny-mpeg4.min.js +44 -0
  123. package/dist/packages/mpeg4/xvid.wasm +0 -0
  124. package/package.json +18 -57
  125. package/dist/bundles/mediabunny.cjs +0 -26140
  126. package/dist/bundles/mediabunny.min.cjs +0 -147
  127. package/dist/bundles/mediabunny.min.mjs +0 -146
  128. package/dist/mediabunny.d.ts +0 -3319
  129. package/dist/modules/shared/mp3-misc.js +0 -147
  130. package/dist/modules/src/adts/adts-demuxer.js +0 -239
  131. package/dist/modules/src/adts/adts-muxer.js +0 -80
  132. package/dist/modules/src/adts/adts-reader.js +0 -63
  133. package/dist/modules/src/codec-data.js +0 -1730
  134. package/dist/modules/src/codec.js +0 -869
  135. package/dist/modules/src/conversion.js +0 -1459
  136. package/dist/modules/src/custom-coder.js +0 -117
  137. package/dist/modules/src/demuxer.js +0 -12
  138. package/dist/modules/src/encode.js +0 -442
  139. package/dist/modules/src/flac/flac-demuxer.js +0 -504
  140. package/dist/modules/src/flac/flac-misc.js +0 -135
  141. package/dist/modules/src/flac/flac-muxer.js +0 -222
  142. package/dist/modules/src/id3.js +0 -848
  143. package/dist/modules/src/index.js +0 -28
  144. package/dist/modules/src/input-format.js +0 -480
  145. package/dist/modules/src/input-track.js +0 -372
  146. package/dist/modules/src/input.js +0 -188
  147. package/dist/modules/src/isobmff/isobmff-boxes.js +0 -1480
  148. package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -2618
  149. package/dist/modules/src/isobmff/isobmff-misc.js +0 -20
  150. package/dist/modules/src/isobmff/isobmff-muxer.js +0 -966
  151. package/dist/modules/src/isobmff/isobmff-reader.js +0 -72
  152. package/dist/modules/src/matroska/ebml.js +0 -653
  153. package/dist/modules/src/matroska/matroska-demuxer.js +0 -2133
  154. package/dist/modules/src/matroska/matroska-misc.js +0 -20
  155. package/dist/modules/src/matroska/matroska-muxer.js +0 -1017
  156. package/dist/modules/src/media-sink.js +0 -1736
  157. package/dist/modules/src/media-source.js +0 -1825
  158. package/dist/modules/src/metadata.js +0 -193
  159. package/dist/modules/src/misc.js +0 -623
  160. package/dist/modules/src/mp3/mp3-demuxer.js +0 -285
  161. package/dist/modules/src/mp3/mp3-muxer.js +0 -123
  162. package/dist/modules/src/mp3/mp3-reader.js +0 -26
  163. package/dist/modules/src/mp3/mp3-writer.js +0 -78
  164. package/dist/modules/src/muxer.js +0 -50
  165. package/dist/modules/src/node.d.ts +0 -9
  166. package/dist/modules/src/node.d.ts.map +0 -1
  167. package/dist/modules/src/node.js +0 -9
  168. package/dist/modules/src/ogg/ogg-demuxer.js +0 -763
  169. package/dist/modules/src/ogg/ogg-misc.js +0 -78
  170. package/dist/modules/src/ogg/ogg-muxer.js +0 -353
  171. package/dist/modules/src/ogg/ogg-reader.js +0 -65
  172. package/dist/modules/src/output-format.js +0 -527
  173. package/dist/modules/src/output.js +0 -300
  174. package/dist/modules/src/packet.js +0 -182
  175. package/dist/modules/src/pcm.js +0 -85
  176. package/dist/modules/src/reader.js +0 -236
  177. package/dist/modules/src/sample.js +0 -1056
  178. package/dist/modules/src/source.js +0 -1182
  179. package/dist/modules/src/subtitles.js +0 -575
  180. package/dist/modules/src/target.js +0 -140
  181. package/dist/modules/src/wave/riff-writer.js +0 -30
  182. package/dist/modules/src/wave/wave-demuxer.js +0 -447
  183. package/dist/modules/src/wave/wave-muxer.js +0 -318
  184. package/dist/modules/src/writer.js +0 -370
  185. package/src/adts/adts-demuxer.ts +0 -331
  186. package/src/adts/adts-muxer.ts +0 -111
  187. package/src/adts/adts-reader.ts +0 -85
  188. package/src/codec-data.ts +0 -2078
  189. package/src/codec.ts +0 -1092
  190. package/src/conversion.ts +0 -2112
  191. package/src/custom-coder.ts +0 -197
  192. package/src/demuxer.ts +0 -24
  193. package/src/encode.ts +0 -739
  194. package/src/flac/flac-demuxer.ts +0 -730
  195. package/src/flac/flac-misc.ts +0 -164
  196. package/src/flac/flac-muxer.ts +0 -320
  197. package/src/id3.ts +0 -925
  198. package/src/index.ts +0 -221
  199. package/src/input-format.ts +0 -541
  200. package/src/input-track.ts +0 -529
  201. package/src/input.ts +0 -235
  202. package/src/isobmff/isobmff-boxes.ts +0 -1719
  203. package/src/isobmff/isobmff-demuxer.ts +0 -3190
  204. package/src/isobmff/isobmff-misc.ts +0 -29
  205. package/src/isobmff/isobmff-muxer.ts +0 -1348
  206. package/src/isobmff/isobmff-reader.ts +0 -91
  207. package/src/matroska/ebml.ts +0 -730
  208. package/src/matroska/matroska-demuxer.ts +0 -2481
  209. package/src/matroska/matroska-misc.ts +0 -29
  210. package/src/matroska/matroska-muxer.ts +0 -1276
  211. package/src/media-sink.ts +0 -2179
  212. package/src/media-source.ts +0 -2243
  213. package/src/metadata.ts +0 -320
  214. package/src/misc.ts +0 -798
  215. package/src/mp3/mp3-demuxer.ts +0 -383
  216. package/src/mp3/mp3-muxer.ts +0 -166
  217. package/src/mp3/mp3-reader.ts +0 -34
  218. package/src/mp3/mp3-writer.ts +0 -120
  219. package/src/muxer.ts +0 -88
  220. package/src/node.ts +0 -11
  221. package/src/ogg/ogg-demuxer.ts +0 -1053
  222. package/src/ogg/ogg-misc.ts +0 -116
  223. package/src/ogg/ogg-muxer.ts +0 -497
  224. package/src/ogg/ogg-reader.ts +0 -93
  225. package/src/output-format.ts +0 -945
  226. package/src/output.ts +0 -488
  227. package/src/packet.ts +0 -263
  228. package/src/pcm.ts +0 -112
  229. package/src/reader.ts +0 -323
  230. package/src/sample.ts +0 -1461
  231. package/src/source.ts +0 -1688
  232. package/src/subtitles.ts +0 -711
  233. package/src/target.ts +0 -204
  234. package/src/tsconfig.json +0 -16
  235. package/src/wave/riff-writer.ts +0 -36
  236. package/src/wave/wave-demuxer.ts +0 -529
  237. package/src/wave/wave-muxer.ts +0 -371
  238. package/src/writer.ts +0 -490
@@ -1,966 +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 { free, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff-boxes.js';
9
- import { Muxer } from '../muxer.js';
10
- import { BufferTargetWriter } from '../writer.js';
11
- import { assert, computeRationalApproximation, last, promiseWithResolvers } from '../misc.js';
12
- import { MovOutputFormat } from '../output-format.js';
13
- import { inlineTimestampRegex } from '../subtitles.js';
14
- import { parsePcmCodec, PCM_AUDIO_CODECS, validateAudioChunkMetadata, validateSubtitleMetadata, validateVideoChunkMetadata, } from '../codec.js';
15
- import { BufferTarget } from '../target.js';
16
- import { extractAvcDecoderConfigurationRecord, extractHevcDecoderConfigurationRecord, serializeAvcDecoderConfigurationRecord, serializeHevcDecoderConfigurationRecord, transformAnnexBToLengthPrefixed, } from '../codec-data.js';
17
- import { buildIsobmffMimeType } from './isobmff-misc.js';
18
- import { MAX_BOX_HEADER_SIZE, MIN_BOX_HEADER_SIZE } from './isobmff-reader.js';
19
- export const GLOBAL_TIMESCALE = 1000;
20
- const TIMESTAMP_OFFSET = 2_082_844_800; // Seconds between Jan 1 1904 and Jan 1 1970
21
- export const getTrackMetadata = (trackData) => {
22
- const metadata = {};
23
- const track = trackData.track;
24
- if (track.metadata.name !== undefined) {
25
- metadata.name = track.metadata.name;
26
- }
27
- return metadata;
28
- };
29
- export const intoTimescale = (timeInSeconds, timescale, round = true) => {
30
- const value = timeInSeconds * timescale;
31
- return round ? Math.round(value) : value;
32
- };
33
- export class IsobmffMuxer extends Muxer {
34
- constructor(output, format) {
35
- super(output);
36
- this.auxTarget = new BufferTarget();
37
- this.auxWriter = this.auxTarget._createWriter();
38
- this.auxBoxWriter = new IsobmffBoxWriter(this.auxWriter);
39
- this.mdat = null;
40
- this.ftypSize = null;
41
- this.trackDatas = [];
42
- this.allTracksKnown = promiseWithResolvers();
43
- this.creationTime = Math.floor(Date.now() / 1000) + TIMESTAMP_OFFSET;
44
- this.finalizedChunks = [];
45
- this.nextFragmentNumber = 1;
46
- // Only relevant for fragmented files, to make sure new fragments start with the highest timestamp seen so far
47
- this.maxWrittenTimestamp = -Infinity;
48
- this.format = format;
49
- this.writer = output._writer;
50
- this.boxWriter = new IsobmffBoxWriter(this.writer);
51
- this.isQuickTime = format instanceof MovOutputFormat;
52
- // If the fastStart option isn't defined, enable in-memory fast start if the target is an ArrayBuffer, as the
53
- // memory usage remains identical
54
- const fastStartDefault = this.writer instanceof BufferTargetWriter ? 'in-memory' : false;
55
- this.fastStart = format._options.fastStart ?? fastStartDefault;
56
- this.isFragmented = this.fastStart === 'fragmented';
57
- if (this.fastStart === 'in-memory' || this.isFragmented) {
58
- this.writer.ensureMonotonicity = true;
59
- }
60
- this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? 1;
61
- }
62
- async start() {
63
- const release = await this.mutex.acquire();
64
- const holdsAvc = this.output._tracks.some(x => x.type === 'video' && x.source._codec === 'avc');
65
- // Write the header
66
- {
67
- if (this.format._options.onFtyp) {
68
- this.writer.startTrackingWrites();
69
- }
70
- this.boxWriter.writeBox(ftyp({
71
- isQuickTime: this.isQuickTime,
72
- holdsAvc: holdsAvc,
73
- fragmented: this.isFragmented,
74
- }));
75
- if (this.format._options.onFtyp) {
76
- const { data, start } = this.writer.stopTrackingWrites();
77
- this.format._options.onFtyp(data, start);
78
- }
79
- }
80
- this.ftypSize = this.writer.getPos();
81
- if (this.fastStart === 'in-memory') {
82
- // We're write at finalization
83
- }
84
- else if (this.fastStart === 'reserve') {
85
- // Validate that all tracks have set maximumPacketCount
86
- for (const track of this.output._tracks) {
87
- if (track.metadata.maximumPacketCount === undefined) {
88
- throw new Error('All tracks must specify maximumPacketCount in their metadata when using'
89
- + ' fastStart: \'reserve\'.');
90
- }
91
- }
92
- // We'll start writing once we know all tracks
93
- }
94
- else if (this.isFragmented) {
95
- // We write the moov box once we write out the first fragment to make sure we get the decoder configs
96
- }
97
- else {
98
- if (this.format._options.onMdat) {
99
- this.writer.startTrackingWrites();
100
- }
101
- this.mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
102
- this.boxWriter.writeBox(this.mdat);
103
- }
104
- await this.writer.flush();
105
- release();
106
- }
107
- allTracksAreKnown() {
108
- for (const track of this.output._tracks) {
109
- if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
110
- return false; // We haven't seen a sample from this open track yet
111
- }
112
- }
113
- return true;
114
- }
115
- async getMimeType() {
116
- await this.allTracksKnown.promise;
117
- const codecStrings = this.trackDatas.map((trackData) => {
118
- if (trackData.type === 'video') {
119
- return trackData.info.decoderConfig.codec;
120
- }
121
- else if (trackData.type === 'audio') {
122
- return trackData.info.decoderConfig.codec;
123
- }
124
- else {
125
- const map = {
126
- webvtt: 'wvtt',
127
- tx3g: 'tx3g',
128
- ttml: 'stpp',
129
- srt: 'wvtt', // MP4 stores SRT as WebVTT
130
- ass: 'wvtt', // MP4 stores ASS as WebVTT
131
- ssa: 'wvtt', // MP4 stores SSA as WebVTT
132
- };
133
- return map[trackData.track.source._codec];
134
- }
135
- });
136
- return buildIsobmffMimeType({
137
- isQuickTime: this.isQuickTime,
138
- hasVideo: this.trackDatas.some(x => x.type === 'video'),
139
- hasAudio: this.trackDatas.some(x => x.type === 'audio'),
140
- codecStrings,
141
- });
142
- }
143
- getVideoTrackData(track, packet, meta) {
144
- const existingTrackData = this.trackDatas.find(x => x.track === track);
145
- if (existingTrackData) {
146
- return existingTrackData;
147
- }
148
- validateVideoChunkMetadata(meta);
149
- assert(meta);
150
- assert(meta.decoderConfig);
151
- const decoderConfig = { ...meta.decoderConfig };
152
- assert(decoderConfig.codedWidth !== undefined);
153
- assert(decoderConfig.codedHeight !== undefined);
154
- let requiresAnnexBTransformation = false;
155
- if (track.source._codec === 'avc' && !decoderConfig.description) {
156
- // ISOBMFF can only hold AVC in the AVCC format, not in Annex B, but the missing description indicates
157
- // Annex B. This means we'll need to do some converterino.
158
- const decoderConfigurationRecord = extractAvcDecoderConfigurationRecord(packet.data);
159
- if (!decoderConfigurationRecord) {
160
- throw new Error('Couldn\'t extract an AVCDecoderConfigurationRecord from the AVC packet. Make sure the packets are'
161
- + ' in Annex B format (as specified in ITU-T-REC-H.264) when not providing a description, or'
162
- + ' provide a description (must be an AVCDecoderConfigurationRecord as specified in ISO 14496-15)'
163
- + ' and ensure the packets are in AVCC format.');
164
- }
165
- decoderConfig.description = serializeAvcDecoderConfigurationRecord(decoderConfigurationRecord);
166
- requiresAnnexBTransformation = true;
167
- }
168
- else if (track.source._codec === 'hevc' && !decoderConfig.description) {
169
- // ISOBMFF can only hold HEVC in the HEVC format, not in Annex B, but the missing description indicates
170
- // Annex B. This means we'll need to do some converterino.
171
- const decoderConfigurationRecord = extractHevcDecoderConfigurationRecord(packet.data);
172
- if (!decoderConfigurationRecord) {
173
- throw new Error('Couldn\'t extract an HEVCDecoderConfigurationRecord from the HEVC packet. Make sure the packets'
174
- + ' are in Annex B format (as specified in ITU-T-REC-H.265) when not providing a description, or'
175
- + ' provide a description (must be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15)'
176
- + ' and ensure the packets are in HEVC format.');
177
- }
178
- decoderConfig.description = serializeHevcDecoderConfigurationRecord(decoderConfigurationRecord);
179
- requiresAnnexBTransformation = true;
180
- }
181
- // The frame rate set by the user may not be an integer. Since timescale is an integer, we'll approximate the
182
- // frame time (inverse of frame rate) with a rational number, then use that approximation's denominator
183
- // as the timescale.
184
- const timescale = computeRationalApproximation(1 / (track.metadata.frameRate ?? 57600), 1e6).denominator;
185
- const newTrackData = {
186
- muxer: this,
187
- track,
188
- type: 'video',
189
- info: {
190
- width: decoderConfig.codedWidth,
191
- height: decoderConfig.codedHeight,
192
- decoderConfig: decoderConfig,
193
- requiresAnnexBTransformation,
194
- },
195
- timescale,
196
- samples: [],
197
- sampleQueue: [],
198
- timestampProcessingQueue: [],
199
- timeToSampleTable: [],
200
- compositionTimeOffsetTable: [],
201
- lastTimescaleUnits: null,
202
- lastSample: null,
203
- finalizedChunks: [],
204
- currentChunk: null,
205
- compactlyCodedChunkTable: [],
206
- };
207
- this.trackDatas.push(newTrackData);
208
- this.trackDatas.sort((a, b) => a.track.id - b.track.id);
209
- if (this.allTracksAreKnown()) {
210
- this.allTracksKnown.resolve();
211
- }
212
- return newTrackData;
213
- }
214
- getAudioTrackData(track, meta) {
215
- const existingTrackData = this.trackDatas.find(x => x.track === track);
216
- if (existingTrackData) {
217
- return existingTrackData;
218
- }
219
- validateAudioChunkMetadata(meta);
220
- assert(meta);
221
- assert(meta.decoderConfig);
222
- const newTrackData = {
223
- muxer: this,
224
- track,
225
- type: 'audio',
226
- info: {
227
- numberOfChannels: meta.decoderConfig.numberOfChannels,
228
- sampleRate: meta.decoderConfig.sampleRate,
229
- decoderConfig: meta.decoderConfig,
230
- requiresPcmTransformation: !this.isFragmented
231
- && PCM_AUDIO_CODECS.includes(track.source._codec),
232
- },
233
- timescale: meta.decoderConfig.sampleRate,
234
- samples: [],
235
- sampleQueue: [],
236
- timestampProcessingQueue: [],
237
- timeToSampleTable: [],
238
- compositionTimeOffsetTable: [],
239
- lastTimescaleUnits: null,
240
- lastSample: null,
241
- finalizedChunks: [],
242
- currentChunk: null,
243
- compactlyCodedChunkTable: [],
244
- };
245
- this.trackDatas.push(newTrackData);
246
- this.trackDatas.sort((a, b) => a.track.id - b.track.id);
247
- if (this.allTracksAreKnown()) {
248
- this.allTracksKnown.resolve();
249
- }
250
- return newTrackData;
251
- }
252
- getSubtitleTrackData(track, meta) {
253
- const existingTrackData = this.trackDatas.find(x => x.track === track);
254
- if (existingTrackData) {
255
- return existingTrackData;
256
- }
257
- validateSubtitleMetadata(meta);
258
- assert(meta);
259
- assert(meta.config);
260
- const newTrackData = {
261
- muxer: this,
262
- track,
263
- type: 'subtitle',
264
- info: {
265
- config: meta.config,
266
- },
267
- timescale: 1000, // Reasonable
268
- samples: [],
269
- sampleQueue: [],
270
- timestampProcessingQueue: [],
271
- timeToSampleTable: [],
272
- compositionTimeOffsetTable: [],
273
- lastTimescaleUnits: null,
274
- lastSample: null,
275
- finalizedChunks: [],
276
- currentChunk: null,
277
- compactlyCodedChunkTable: [],
278
- lastCueEndTimestamp: 0,
279
- cueQueue: [],
280
- nextSourceId: 0,
281
- cueToSourceId: new WeakMap(),
282
- };
283
- this.trackDatas.push(newTrackData);
284
- this.trackDatas.sort((a, b) => a.track.id - b.track.id);
285
- if (this.allTracksAreKnown()) {
286
- this.allTracksKnown.resolve();
287
- }
288
- return newTrackData;
289
- }
290
- async addEncodedVideoPacket(track, packet, meta) {
291
- const release = await this.mutex.acquire();
292
- try {
293
- const trackData = this.getVideoTrackData(track, packet, meta);
294
- let packetData = packet.data;
295
- if (trackData.info.requiresAnnexBTransformation) {
296
- const transformedData = transformAnnexBToLengthPrefixed(packetData);
297
- if (!transformedData) {
298
- throw new Error('Failed to transform packet data. Make sure all packets are provided in Annex B format, as'
299
- + ' specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.');
300
- }
301
- packetData = transformedData;
302
- }
303
- const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
304
- const internalSample = this.createSampleForTrack(trackData, packetData, timestamp, packet.duration, packet.type);
305
- await this.registerSample(trackData, internalSample);
306
- }
307
- finally {
308
- release();
309
- }
310
- }
311
- async addEncodedAudioPacket(track, packet, meta) {
312
- const release = await this.mutex.acquire();
313
- try {
314
- const trackData = this.getAudioTrackData(track, meta);
315
- const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
316
- const internalSample = this.createSampleForTrack(trackData, packet.data, timestamp, packet.duration, packet.type);
317
- if (trackData.info.requiresPcmTransformation) {
318
- await this.maybePadWithSilence(trackData, timestamp);
319
- }
320
- await this.registerSample(trackData, internalSample);
321
- }
322
- finally {
323
- release();
324
- }
325
- }
326
- async maybePadWithSilence(trackData, untilTimestamp) {
327
- // The PCM transformation assumes that all samples are contiguous. This is not something that is enforced, so
328
- // we need to pad the "holes" in between samples (and before the first sample) with additional
329
- // "silence samples".
330
- const lastSample = last(trackData.samples);
331
- const lastEndTimestamp = lastSample
332
- ? lastSample.timestamp + lastSample.duration
333
- : 0;
334
- const delta = untilTimestamp - lastEndTimestamp;
335
- const deltaInTimescale = intoTimescale(delta, trackData.timescale);
336
- if (deltaInTimescale > 0) {
337
- const { sampleSize, silentValue } = parsePcmCodec(trackData.info.decoderConfig.codec);
338
- const samplesNeeded = deltaInTimescale * trackData.info.numberOfChannels;
339
- const data = new Uint8Array(sampleSize * samplesNeeded).fill(silentValue);
340
- const paddingSample = this.createSampleForTrack(trackData, new Uint8Array(data.buffer), lastEndTimestamp, delta, 'key');
341
- await this.registerSample(trackData, paddingSample);
342
- }
343
- }
344
- async addSubtitleCue(track, cue, meta) {
345
- const release = await this.mutex.acquire();
346
- try {
347
- const trackData = this.getSubtitleTrackData(track, meta);
348
- this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true);
349
- if (track.source._codec === 'webvtt') {
350
- trackData.cueQueue.push(cue);
351
- await this.processWebVTTCues(trackData, cue.timestamp);
352
- }
353
- else {
354
- throw new Error(`${track.source._codec} subtitles are not supported in ${this.format._name}. Only WebVTT is supported.`);
355
- }
356
- }
357
- finally {
358
- release();
359
- }
360
- }
361
- async processWebVTTCues(trackData, until) {
362
- // WebVTT cues need to undergo special processing as empty sections need to be padded out with samples, and
363
- // overlapping samples require special logic. The algorithm produces the format specified in ISO 14496-30.
364
- while (trackData.cueQueue.length > 0) {
365
- const timestamps = new Set([]);
366
- for (const cue of trackData.cueQueue) {
367
- assert(cue.timestamp <= until);
368
- assert(trackData.lastCueEndTimestamp <= cue.timestamp + cue.duration);
369
- timestamps.add(Math.max(cue.timestamp, trackData.lastCueEndTimestamp)); // Start timestamp
370
- timestamps.add(cue.timestamp + cue.duration); // End timestamp
371
- }
372
- const sortedTimestamps = [...timestamps].sort((a, b) => a - b);
373
- // These are the timestamps of the next sample we'll create:
374
- const sampleStart = sortedTimestamps[0];
375
- const sampleEnd = sortedTimestamps[1] ?? sampleStart;
376
- if (until < sampleEnd) {
377
- break;
378
- }
379
- // We may need to pad out empty space with an vtte box
380
- if (trackData.lastCueEndTimestamp < sampleStart) {
381
- this.auxWriter.seek(0);
382
- const box = vtte();
383
- this.auxBoxWriter.writeBox(box);
384
- const body = this.auxWriter.getSlice(0, this.auxWriter.getPos());
385
- const sample = this.createSampleForTrack(trackData, body, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, 'key');
386
- await this.registerSample(trackData, sample);
387
- trackData.lastCueEndTimestamp = sampleStart;
388
- }
389
- this.auxWriter.seek(0);
390
- for (let i = 0; i < trackData.cueQueue.length; i++) {
391
- const cue = trackData.cueQueue[i];
392
- if (cue.timestamp >= sampleEnd) {
393
- break;
394
- }
395
- inlineTimestampRegex.lastIndex = 0;
396
- const containsTimestamp = inlineTimestampRegex.test(cue.text);
397
- const endTimestamp = cue.timestamp + cue.duration;
398
- let sourceId = trackData.cueToSourceId.get(cue);
399
- if (sourceId === undefined && sampleEnd < endTimestamp) {
400
- // We know this cue will appear in more than one sample, therefore we need to mark it with a
401
- // unique ID
402
- sourceId = trackData.nextSourceId++;
403
- trackData.cueToSourceId.set(cue, sourceId);
404
- }
405
- if (cue.notes) {
406
- // Any notes/comments are included in a special vtta box
407
- const box = vtta(cue.notes);
408
- this.auxBoxWriter.writeBox(box);
409
- }
410
- const box = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
411
- this.auxBoxWriter.writeBox(box);
412
- if (endTimestamp === sampleEnd) {
413
- // The cue won't appear in any future sample, so we're done with it
414
- trackData.cueQueue.splice(i--, 1);
415
- }
416
- }
417
- const body = this.auxWriter.getSlice(0, this.auxWriter.getPos());
418
- const sample = this.createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, 'key');
419
- await this.registerSample(trackData, sample);
420
- trackData.lastCueEndTimestamp = sampleEnd;
421
- }
422
- }
423
- createSampleForTrack(trackData, data, timestamp, duration, type) {
424
- const sample = {
425
- timestamp,
426
- decodeTimestamp: timestamp, // This may be refined later
427
- duration,
428
- data,
429
- size: data.byteLength,
430
- type,
431
- timescaleUnitsToNextSample: intoTimescale(duration, trackData.timescale), // Will be refined
432
- };
433
- return sample;
434
- }
435
- processTimestamps(trackData, nextSample) {
436
- if (trackData.timestampProcessingQueue.length === 0) {
437
- return;
438
- }
439
- if (trackData.type === 'audio' && trackData.info.requiresPcmTransformation) {
440
- let totalDuration = 0;
441
- // Compute the total duration in the track timescale (which is equal to the amount of PCM audio samples)
442
- // and simply say that's how many new samples there are.
443
- for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
444
- const sample = trackData.timestampProcessingQueue[i];
445
- const duration = intoTimescale(sample.duration, trackData.timescale);
446
- totalDuration += duration;
447
- }
448
- if (trackData.timeToSampleTable.length === 0) {
449
- trackData.timeToSampleTable.push({
450
- sampleCount: totalDuration,
451
- sampleDelta: 1,
452
- });
453
- }
454
- else {
455
- const lastEntry = last(trackData.timeToSampleTable);
456
- lastEntry.sampleCount += totalDuration;
457
- }
458
- trackData.timestampProcessingQueue.length = 0;
459
- return;
460
- }
461
- const sortedTimestamps = trackData.timestampProcessingQueue.map(x => x.timestamp).sort((a, b) => a - b);
462
- for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
463
- const sample = trackData.timestampProcessingQueue[i];
464
- // Since the user only supplies presentation time, but these may be out of order, we reverse-engineer from
465
- // that a sensible decode timestamp. The notion of a decode timestamp doesn't really make sense
466
- // (presentation timestamp & decode order are all you need), but it is a concept in ISOBMFF so we need to
467
- // model it.
468
- sample.decodeTimestamp = sortedTimestamps[i];
469
- if (!this.isFragmented && trackData.lastTimescaleUnits === null) {
470
- // In non-fragmented files, the first decode timestamp is always zero. If the first presentation
471
- // timestamp isn't zero, we'll simply use the composition time offset to achieve it.
472
- sample.decodeTimestamp = 0;
473
- }
474
- const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale);
475
- const durationInTimescale = intoTimescale(sample.duration, trackData.timescale);
476
- if (trackData.lastTimescaleUnits !== null) {
477
- assert(trackData.lastSample);
478
- const timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
479
- const delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
480
- assert(delta >= 0);
481
- trackData.lastTimescaleUnits += delta;
482
- trackData.lastSample.timescaleUnitsToNextSample = delta;
483
- if (!this.isFragmented) {
484
- let lastTableEntry = last(trackData.timeToSampleTable);
485
- assert(lastTableEntry);
486
- if (lastTableEntry.sampleCount === 1) {
487
- lastTableEntry.sampleDelta = delta;
488
- const entryBefore = trackData.timeToSampleTable[trackData.timeToSampleTable.length - 2];
489
- if (entryBefore && entryBefore.sampleDelta === delta) {
490
- // If the delta is the same as the previous one, merge the two entries
491
- entryBefore.sampleCount++;
492
- trackData.timeToSampleTable.pop();
493
- lastTableEntry = entryBefore;
494
- }
495
- }
496
- else if (lastTableEntry.sampleDelta !== delta) {
497
- // The delta has changed, so we need a new entry to reach the current sample
498
- lastTableEntry.sampleCount--;
499
- trackData.timeToSampleTable.push(lastTableEntry = {
500
- sampleCount: 1,
501
- sampleDelta: delta,
502
- });
503
- }
504
- if (lastTableEntry.sampleDelta === durationInTimescale) {
505
- // The sample's duration matches the delta, so we can increment the count
506
- lastTableEntry.sampleCount++;
507
- }
508
- else {
509
- // Add a new entry in order to maintain the last sample's true duration
510
- trackData.timeToSampleTable.push({
511
- sampleCount: 1,
512
- sampleDelta: durationInTimescale,
513
- });
514
- }
515
- const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable);
516
- assert(lastCompositionTimeOffsetTableEntry);
517
- if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) {
518
- // Simply increment the count
519
- lastCompositionTimeOffsetTableEntry.sampleCount++;
520
- }
521
- else {
522
- // The composition time offset has changed, so create a new entry with the new composition time
523
- // offset
524
- trackData.compositionTimeOffsetTable.push({
525
- sampleCount: 1,
526
- sampleCompositionTimeOffset: sampleCompositionTimeOffset,
527
- });
528
- }
529
- }
530
- }
531
- else {
532
- // Decode timestamp of the first sample
533
- trackData.lastTimescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
534
- if (!this.isFragmented) {
535
- trackData.timeToSampleTable.push({
536
- sampleCount: 1,
537
- sampleDelta: durationInTimescale,
538
- });
539
- trackData.compositionTimeOffsetTable.push({
540
- sampleCount: 1,
541
- sampleCompositionTimeOffset: sampleCompositionTimeOffset,
542
- });
543
- }
544
- }
545
- trackData.lastSample = sample;
546
- }
547
- trackData.timestampProcessingQueue.length = 0;
548
- assert(trackData.lastSample);
549
- assert(trackData.lastTimescaleUnits !== null);
550
- if (nextSample !== undefined && trackData.lastSample.timescaleUnitsToNextSample === 0) {
551
- assert(nextSample.type === 'key');
552
- // Given the next sample, we can make a guess about the duration of the last sample. This avoids having
553
- // the last sample's duration in each fragment be "0" for fragmented files. The guess we make here is
554
- // actually correct most of the time, since typically, no delta frame with a lower timestamp follows the key
555
- // frame (although it can happen).
556
- const timescaleUnits = intoTimescale(nextSample.timestamp, trackData.timescale, false);
557
- const delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
558
- trackData.lastSample.timescaleUnitsToNextSample = delta;
559
- }
560
- }
561
- async registerSample(trackData, sample) {
562
- if (sample.type === 'key') {
563
- this.processTimestamps(trackData, sample);
564
- }
565
- trackData.timestampProcessingQueue.push(sample);
566
- if (this.isFragmented) {
567
- trackData.sampleQueue.push(sample);
568
- await this.interleaveSamples();
569
- }
570
- else if (this.fastStart === 'reserve') {
571
- await this.registerSampleFastStartReserve(trackData, sample);
572
- }
573
- else {
574
- await this.addSampleToTrack(trackData, sample);
575
- }
576
- }
577
- async addSampleToTrack(trackData, sample) {
578
- if (!this.isFragmented) {
579
- trackData.samples.push(sample);
580
- if (this.fastStart === 'reserve') {
581
- const maximumPacketCount = trackData.track.metadata.maximumPacketCount;
582
- assert(maximumPacketCount !== undefined);
583
- if (trackData.samples.length > maximumPacketCount) {
584
- throw new Error(`Track #${trackData.track.id} has already reached the maximum packet count`
585
- + ` (${maximumPacketCount}). Either add less packets or increase the maximum packet count.`);
586
- }
587
- }
588
- }
589
- let beginNewChunk = false;
590
- if (!trackData.currentChunk) {
591
- beginNewChunk = true;
592
- }
593
- else {
594
- // Timestamp don't need to be monotonic (think B-frames), so we may need to update the start timestamp of
595
- // the chunk
596
- trackData.currentChunk.startTimestamp = Math.min(trackData.currentChunk.startTimestamp, sample.timestamp);
597
- const currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
598
- if (this.isFragmented) {
599
- // We can only finalize this fragment (and begin a new one) if we know that each track will be able to
600
- // start the new one with a key frame.
601
- const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
602
- if (trackData === otherTrackData) {
603
- return sample.type === 'key';
604
- }
605
- const firstQueuedSample = otherTrackData.sampleQueue[0];
606
- if (firstQueuedSample) {
607
- return firstQueuedSample.type === 'key';
608
- }
609
- return otherTrackData.track.source._closed;
610
- });
611
- if (currentChunkDuration >= this.minimumFragmentDuration
612
- && keyFrameQueuedEverywhere
613
- && sample.timestamp > this.maxWrittenTimestamp) {
614
- beginNewChunk = true;
615
- await this.finalizeFragment();
616
- }
617
- }
618
- else {
619
- beginNewChunk = currentChunkDuration >= 0.5; // Chunk is long enough, we need a new one
620
- }
621
- }
622
- if (beginNewChunk) {
623
- if (trackData.currentChunk) {
624
- await this.finalizeCurrentChunk(trackData);
625
- }
626
- trackData.currentChunk = {
627
- startTimestamp: sample.timestamp,
628
- samples: [],
629
- offset: null,
630
- moofOffset: null,
631
- };
632
- }
633
- assert(trackData.currentChunk);
634
- trackData.currentChunk.samples.push(sample);
635
- if (this.isFragmented) {
636
- this.maxWrittenTimestamp = Math.max(this.maxWrittenTimestamp, sample.timestamp);
637
- }
638
- }
639
- async finalizeCurrentChunk(trackData) {
640
- assert(!this.isFragmented);
641
- if (!trackData.currentChunk)
642
- return;
643
- trackData.finalizedChunks.push(trackData.currentChunk);
644
- this.finalizedChunks.push(trackData.currentChunk);
645
- let sampleCount = trackData.currentChunk.samples.length;
646
- if (trackData.type === 'audio' && trackData.info.requiresPcmTransformation) {
647
- sampleCount = trackData.currentChunk.samples
648
- .reduce((acc, sample) => acc + intoTimescale(sample.duration, trackData.timescale), 0);
649
- }
650
- if (trackData.compactlyCodedChunkTable.length === 0
651
- || last(trackData.compactlyCodedChunkTable).samplesPerChunk !== sampleCount) {
652
- trackData.compactlyCodedChunkTable.push({
653
- firstChunk: trackData.finalizedChunks.length, // 1-indexed
654
- samplesPerChunk: sampleCount,
655
- });
656
- }
657
- if (this.fastStart === 'in-memory') {
658
- trackData.currentChunk.offset = 0; // We'll compute the proper offset when finalizing
659
- return;
660
- }
661
- // Write out the data
662
- trackData.currentChunk.offset = this.writer.getPos();
663
- for (const sample of trackData.currentChunk.samples) {
664
- assert(sample.data);
665
- this.writer.write(sample.data);
666
- sample.data = null; // Can be GC'd
667
- }
668
- await this.writer.flush();
669
- }
670
- async interleaveSamples(isFinalCall = false) {
671
- assert(this.isFragmented);
672
- if (!isFinalCall && !this.allTracksAreKnown()) {
673
- return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
674
- }
675
- outer: while (true) {
676
- let trackWithMinTimestamp = null;
677
- let minTimestamp = Infinity;
678
- for (const trackData of this.trackDatas) {
679
- if (!isFinalCall && trackData.sampleQueue.length === 0 && !trackData.track.source._closed) {
680
- break outer;
681
- }
682
- if (trackData.sampleQueue.length > 0 && trackData.sampleQueue[0].timestamp < minTimestamp) {
683
- trackWithMinTimestamp = trackData;
684
- minTimestamp = trackData.sampleQueue[0].timestamp;
685
- }
686
- }
687
- if (!trackWithMinTimestamp) {
688
- break;
689
- }
690
- const sample = trackWithMinTimestamp.sampleQueue.shift();
691
- await this.addSampleToTrack(trackWithMinTimestamp, sample);
692
- }
693
- }
694
- async finalizeFragment(flushWriter = true) {
695
- assert(this.isFragmented);
696
- const fragmentNumber = this.nextFragmentNumber++;
697
- if (fragmentNumber === 1) {
698
- if (this.format._options.onMoov) {
699
- this.writer.startTrackingWrites();
700
- }
701
- // Write the moov box now that we have all decoder configs
702
- const movieBox = moov(this);
703
- this.boxWriter.writeBox(movieBox);
704
- if (this.format._options.onMoov) {
705
- const { data, start } = this.writer.stopTrackingWrites();
706
- this.format._options.onMoov(data, start);
707
- }
708
- }
709
- // Not all tracks need to be present in every fragment
710
- const tracksInFragment = this.trackDatas.filter(x => x.currentChunk);
711
- // Create an initial moof box and measure it; we need this to know where the following mdat box will begin
712
- const moofBox = moof(fragmentNumber, tracksInFragment);
713
- const moofOffset = this.writer.getPos();
714
- const mdatStartPos = moofOffset + this.boxWriter.measureBox(moofBox);
715
- let currentPos = mdatStartPos + MIN_BOX_HEADER_SIZE;
716
- let fragmentStartTimestamp = Infinity;
717
- for (const trackData of tracksInFragment) {
718
- trackData.currentChunk.offset = currentPos;
719
- trackData.currentChunk.moofOffset = moofOffset;
720
- for (const sample of trackData.currentChunk.samples) {
721
- currentPos += sample.size;
722
- }
723
- fragmentStartTimestamp = Math.min(fragmentStartTimestamp, trackData.currentChunk.startTimestamp);
724
- }
725
- const mdatSize = currentPos - mdatStartPos;
726
- const needsLargeMdatSize = mdatSize >= 2 ** 32;
727
- if (needsLargeMdatSize) {
728
- // Shift all offsets by 8. Previously, all chunks were shifted assuming the large box size, but due to what
729
- // I suspect is a bug in WebKit, it failed in Safari (when livestreaming with MSE, not for static playback).
730
- for (const trackData of tracksInFragment) {
731
- trackData.currentChunk.offset += MAX_BOX_HEADER_SIZE - MIN_BOX_HEADER_SIZE;
732
- }
733
- }
734
- if (this.format._options.onMoof) {
735
- this.writer.startTrackingWrites();
736
- }
737
- const newMoofBox = moof(fragmentNumber, tracksInFragment);
738
- this.boxWriter.writeBox(newMoofBox);
739
- if (this.format._options.onMoof) {
740
- const { data, start } = this.writer.stopTrackingWrites();
741
- this.format._options.onMoof(data, start, fragmentStartTimestamp);
742
- }
743
- assert(this.writer.getPos() === mdatStartPos);
744
- if (this.format._options.onMdat) {
745
- this.writer.startTrackingWrites();
746
- }
747
- const mdatBox = mdat(needsLargeMdatSize);
748
- mdatBox.size = mdatSize;
749
- this.boxWriter.writeBox(mdatBox);
750
- this.writer.seek(mdatStartPos + (needsLargeMdatSize ? MAX_BOX_HEADER_SIZE : MIN_BOX_HEADER_SIZE));
751
- // Write sample data
752
- for (const trackData of tracksInFragment) {
753
- for (const sample of trackData.currentChunk.samples) {
754
- this.writer.write(sample.data);
755
- sample.data = null; // Can be GC'd
756
- }
757
- }
758
- if (this.format._options.onMdat) {
759
- const { data, start } = this.writer.stopTrackingWrites();
760
- this.format._options.onMdat(data, start);
761
- }
762
- for (const trackData of tracksInFragment) {
763
- trackData.finalizedChunks.push(trackData.currentChunk);
764
- this.finalizedChunks.push(trackData.currentChunk);
765
- trackData.currentChunk = null;
766
- }
767
- if (flushWriter) {
768
- await this.writer.flush();
769
- }
770
- }
771
- async registerSampleFastStartReserve(trackData, sample) {
772
- if (this.allTracksAreKnown()) {
773
- if (!this.mdat) {
774
- // We finally know all tracks, let's reserve space for the moov box
775
- const moovBox = moov(this);
776
- const moovSize = this.boxWriter.measureBox(moovBox);
777
- const reservedSize = moovSize
778
- + this.computeSampleTableSizeUpperBound()
779
- + 4096; // Just a little extra headroom
780
- assert(this.ftypSize !== null);
781
- this.writer.seek(this.ftypSize + reservedSize);
782
- if (this.format._options.onMdat) {
783
- this.writer.startTrackingWrites();
784
- }
785
- this.mdat = mdat(true);
786
- this.boxWriter.writeBox(this.mdat);
787
- // Now write everything that was queued
788
- for (const trackData of this.trackDatas) {
789
- for (const sample of trackData.sampleQueue) {
790
- await this.addSampleToTrack(trackData, sample);
791
- }
792
- trackData.sampleQueue.length = 0;
793
- }
794
- }
795
- await this.addSampleToTrack(trackData, sample);
796
- }
797
- else {
798
- // Queue it for when we know all tracks
799
- trackData.sampleQueue.push(sample);
800
- }
801
- }
802
- computeSampleTableSizeUpperBound() {
803
- assert(this.fastStart === 'reserve');
804
- let upperBound = 0;
805
- for (const trackData of this.trackDatas) {
806
- const n = trackData.track.metadata.maximumPacketCount;
807
- assert(n !== undefined); // We validated this earlier
808
- // Given the max allowed packet count, compute the space they'll take up in the Sample Table Box, assuming
809
- // the worst case for each individual box:
810
- // stts box - since it is compactly coded, the maximum length of this table will be 2/3n
811
- upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
812
- // stss box - 1 entry per sample
813
- upperBound += 4 * n;
814
- // ctts box - since it is compactly coded, the maximum length of this table will be 2/3n
815
- upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
816
- // stsc box - since it is compactly coded, the maximum length of this table will be 2/3n
817
- upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
818
- // stsz box - 1 entry per sample
819
- upperBound += 4 * n;
820
- // co64 box - we assume 1 sample per chunk and 64-bit chunk offsets (co64 instead of stco)
821
- upperBound += 8 * n;
822
- }
823
- return upperBound;
824
- }
825
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
826
- async onTrackClose(track) {
827
- const release = await this.mutex.acquire();
828
- if (track.type === 'subtitle' && track.source._codec === 'webvtt') {
829
- const trackData = this.trackDatas.find(x => x.track === track);
830
- if (trackData) {
831
- await this.processWebVTTCues(trackData, Infinity);
832
- }
833
- }
834
- if (this.allTracksAreKnown()) {
835
- this.allTracksKnown.resolve();
836
- }
837
- if (this.isFragmented) {
838
- // Since a track is now closed, we may be able to write out chunks that were previously waiting
839
- await this.interleaveSamples();
840
- }
841
- release();
842
- }
843
- /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
844
- async finalize() {
845
- const release = await this.mutex.acquire();
846
- this.allTracksKnown.resolve();
847
- for (const trackData of this.trackDatas) {
848
- if (trackData.type === 'subtitle' && trackData.track.source._codec === 'webvtt') {
849
- await this.processWebVTTCues(trackData, Infinity);
850
- }
851
- }
852
- if (this.isFragmented) {
853
- await this.interleaveSamples(true);
854
- for (const trackData of this.trackDatas) {
855
- this.processTimestamps(trackData);
856
- }
857
- await this.finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box
858
- }
859
- else {
860
- for (const trackData of this.trackDatas) {
861
- this.processTimestamps(trackData);
862
- await this.finalizeCurrentChunk(trackData);
863
- }
864
- }
865
- if (this.fastStart === 'in-memory') {
866
- this.mdat = mdat(false);
867
- let mdatSize;
868
- // We know how many chunks there are, but computing the chunk positions requires an iterative approach:
869
- // In order to know where the first chunk should go, we first need to know the size of the moov box. But we
870
- // cannot write a proper moov box without first knowing all chunk positions. So, we generate a tentative
871
- // moov box with placeholder values (0) for the chunk offsets to be able to compute its size. If it then
872
- // turns out that appending all chunks exceeds 4 GiB, we need to repeat this process, now with the co64 box
873
- // being used in the moov box instead, which will make it larger. After that, we definitely know the final
874
- // size of the moov box and can compute the proper chunk positions.
875
- for (let i = 0; i < 2; i++) {
876
- const movieBox = moov(this);
877
- const movieBoxSize = this.boxWriter.measureBox(movieBox);
878
- mdatSize = this.boxWriter.measureBox(this.mdat);
879
- let currentChunkPos = this.writer.getPos() + movieBoxSize + mdatSize;
880
- for (const chunk of this.finalizedChunks) {
881
- chunk.offset = currentChunkPos;
882
- for (const { data } of chunk.samples) {
883
- assert(data);
884
- currentChunkPos += data.byteLength;
885
- mdatSize += data.byteLength;
886
- }
887
- }
888
- if (currentChunkPos < 2 ** 32)
889
- break;
890
- if (mdatSize >= 2 ** 32)
891
- this.mdat.largeSize = true;
892
- }
893
- if (this.format._options.onMoov) {
894
- this.writer.startTrackingWrites();
895
- }
896
- const movieBox = moov(this);
897
- this.boxWriter.writeBox(movieBox);
898
- if (this.format._options.onMoov) {
899
- const { data, start } = this.writer.stopTrackingWrites();
900
- this.format._options.onMoov(data, start);
901
- }
902
- if (this.format._options.onMdat) {
903
- this.writer.startTrackingWrites();
904
- }
905
- this.mdat.size = mdatSize;
906
- this.boxWriter.writeBox(this.mdat);
907
- for (const chunk of this.finalizedChunks) {
908
- for (const sample of chunk.samples) {
909
- assert(sample.data);
910
- this.writer.write(sample.data);
911
- sample.data = null;
912
- }
913
- }
914
- if (this.format._options.onMdat) {
915
- const { data, start } = this.writer.stopTrackingWrites();
916
- this.format._options.onMdat(data, start);
917
- }
918
- }
919
- else if (this.isFragmented) {
920
- // Append the mfra box to the end of the file for better random access
921
- const startPos = this.writer.getPos();
922
- const mfraBox = mfra(this.trackDatas);
923
- this.boxWriter.writeBox(mfraBox);
924
- // Patch the 'size' field of the mfro box at the end of the mfra box now that we know its actual size
925
- const mfraBoxSize = this.writer.getPos() - startPos;
926
- this.writer.seek(this.writer.getPos() - 4);
927
- this.boxWriter.writeU32(mfraBoxSize);
928
- }
929
- else {
930
- assert(this.mdat);
931
- const mdatPos = this.boxWriter.offsets.get(this.mdat);
932
- assert(mdatPos !== undefined);
933
- const mdatSize = this.writer.getPos() - mdatPos;
934
- this.mdat.size = mdatSize;
935
- this.mdat.largeSize = mdatSize >= 2 ** 32; // Only use the large size if we need it
936
- this.boxWriter.patchBox(this.mdat);
937
- if (this.format._options.onMdat) {
938
- const { data, start } = this.writer.stopTrackingWrites();
939
- this.format._options.onMdat(data, start);
940
- }
941
- const movieBox = moov(this);
942
- if (this.fastStart === 'reserve') {
943
- assert(this.ftypSize !== null);
944
- this.writer.seek(this.ftypSize);
945
- if (this.format._options.onMoov) {
946
- this.writer.startTrackingWrites();
947
- }
948
- this.boxWriter.writeBox(movieBox);
949
- // Fill the remaining space with a free box. If there are less than 8 bytes left, sucks I guess
950
- const remainingSpace = this.boxWriter.offsets.get(this.mdat) - this.writer.getPos();
951
- this.boxWriter.writeBox(free(remainingSpace));
952
- }
953
- else {
954
- if (this.format._options.onMoov) {
955
- this.writer.startTrackingWrites();
956
- }
957
- this.boxWriter.writeBox(movieBox);
958
- }
959
- if (this.format._options.onMoov) {
960
- const { data, start } = this.writer.stopTrackingWrites();
961
- this.format._options.onMoov(data, start);
962
- }
963
- }
964
- release();
965
- }
966
- }