mediabunny 1.52.3 → 1.53.1

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 (92) hide show
  1. package/dist/bundles/mediabunny.cjs +974 -397
  2. package/dist/bundles/mediabunny.min.cjs +9 -9
  3. package/dist/bundles/mediabunny.min.mjs +9 -9
  4. package/dist/bundles/mediabunny.mjs +974 -397
  5. package/dist/bundles/mediabunny.node.cjs +974 -397
  6. package/dist/mediabunny.d.ts +26 -2
  7. package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
  8. package/dist/modules/src/adts/adts-muxer.js +4 -1
  9. package/dist/modules/src/codec.d.ts +2 -2
  10. package/dist/modules/src/codec.d.ts.map +1 -1
  11. package/dist/modules/src/codec.js +10 -2
  12. package/dist/modules/src/conversion.js +2 -2
  13. package/dist/modules/src/encode.d.ts.map +1 -1
  14. package/dist/modules/src/encode.js +16 -4
  15. package/dist/modules/src/flac/flac-muxer.d.ts +1 -0
  16. package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
  17. package/dist/modules/src/flac/flac-muxer.js +58 -34
  18. package/dist/modules/src/hls/hls-muxer.d.ts +17 -2
  19. package/dist/modules/src/hls/hls-muxer.d.ts.map +1 -1
  20. package/dist/modules/src/hls/hls-muxer.js +205 -97
  21. package/dist/modules/src/input.d.ts.map +1 -1
  22. package/dist/modules/src/input.js +4 -2
  23. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +1 -1
  24. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  25. package/dist/modules/src/isobmff/isobmff-boxes.js +6 -4
  26. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +1 -0
  27. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  28. package/dist/modules/src/isobmff/isobmff-demuxer.js +65 -54
  29. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +9 -3
  30. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  31. package/dist/modules/src/isobmff/isobmff-muxer.js +125 -66
  32. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  33. package/dist/modules/src/matroska/matroska-muxer.js +15 -3
  34. package/dist/modules/src/media-sink.d.ts.map +1 -1
  35. package/dist/modules/src/media-sink.js +3 -11
  36. package/dist/modules/src/media-source.d.ts.map +1 -1
  37. package/dist/modules/src/media-source.js +18 -6
  38. package/dist/modules/src/mp3/mp3-demuxer.d.ts +2 -0
  39. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  40. package/dist/modules/src/mp3/mp3-demuxer.js +12 -0
  41. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  42. package/dist/modules/src/mp3/mp3-muxer.js +89 -11
  43. package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
  44. package/dist/modules/src/mp3/mp3-writer.js +2 -1
  45. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts.map +1 -1
  46. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +4 -0
  47. package/dist/modules/src/mpeg-ts/mpeg-ts-muxer.js +2 -2
  48. package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
  49. package/dist/modules/src/ogg/ogg-muxer.js +7 -1
  50. package/dist/modules/src/output-format.d.ts.map +1 -1
  51. package/dist/modules/src/output-format.js +13 -5
  52. package/dist/modules/src/output.d.ts +25 -1
  53. package/dist/modules/src/output.d.ts.map +1 -1
  54. package/dist/modules/src/output.js +24 -0
  55. package/dist/modules/src/sample.d.ts.map +1 -1
  56. package/dist/modules/src/sample.js +120 -55
  57. package/dist/modules/src/segmented-input.js +36 -8
  58. package/dist/modules/src/source.d.ts +2 -1
  59. package/dist/modules/src/source.d.ts.map +1 -1
  60. package/dist/modules/src/source.js +166 -52
  61. package/dist/modules/src/target.d.ts.map +1 -1
  62. package/dist/modules/src/target.js +11 -2
  63. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  64. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  65. package/dist/modules/src/wave/wave-muxer.js +15 -2
  66. package/package.json +1 -1
  67. package/src/adts/adts-muxer.ts +6 -1
  68. package/src/codec.ts +22 -2
  69. package/src/conversion.ts +2 -2
  70. package/src/encode.ts +14 -4
  71. package/src/flac/flac-muxer.ts +72 -39
  72. package/src/hls/hls-muxer.ts +261 -104
  73. package/src/input.ts +5 -2
  74. package/src/isobmff/isobmff-boxes.ts +8 -4
  75. package/src/isobmff/isobmff-demuxer.ts +70 -57
  76. package/src/isobmff/isobmff-muxer.ts +162 -76
  77. package/src/matroska/matroska-muxer.ts +29 -6
  78. package/src/media-sink.ts +3 -12
  79. package/src/media-source.ts +18 -6
  80. package/src/mp3/mp3-demuxer.ts +14 -0
  81. package/src/mp3/mp3-muxer.ts +105 -11
  82. package/src/mp3/mp3-writer.ts +3 -1
  83. package/src/mpeg-ts/mpeg-ts-demuxer.ts +5 -0
  84. package/src/mpeg-ts/mpeg-ts-muxer.ts +2 -2
  85. package/src/ogg/ogg-muxer.ts +8 -1
  86. package/src/output-format.ts +15 -5
  87. package/src/output.ts +48 -1
  88. package/src/sample.ts +160 -67
  89. package/src/segmented-input.ts +44 -8
  90. package/src/source.ts +208 -71
  91. package/src/target.ts +12 -2
  92. package/src/wave/wave-muxer.ts +20 -2
@@ -2166,7 +2166,7 @@ var Mediabunny = (() => {
2166
2166
  var HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
2167
2167
  var VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
2168
2168
  var AV1_CODEC_STRING_REGEX = /^av01\.\d\.\d{2}[MH]\.\d{2}(?:\.\d\.\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d)?$/;
2169
- var validateVideoChunkMetadata = (metadata) => {
2169
+ var validateVideoChunkMetadata = (metadata, trackCodec) => {
2170
2170
  if (!metadata) {
2171
2171
  throw new TypeError("Video chunk metadata must be provided.");
2172
2172
  }
@@ -2285,6 +2285,11 @@ var Mediabunny = (() => {
2285
2285
  );
2286
2286
  }
2287
2287
  }
2288
+ if (trackCodec !== null && inferCodecFromCodecString(metadata.decoderConfig.codec) !== trackCodec) {
2289
+ throw new TypeError(
2290
+ `Video chunk metadata decoder configuration codec string '${metadata.decoderConfig.codec}' does not fit to the track codec '${trackCodec}'.`
2291
+ );
2292
+ }
2288
2293
  };
2289
2294
  var VALID_AUDIO_CODEC_STRING_PREFIXES = [
2290
2295
  "mp4a",
@@ -2298,7 +2303,7 @@ var Mediabunny = (() => {
2298
2303
  "ac-3",
2299
2304
  "ec-3"
2300
2305
  ];
2301
- var validateAudioChunkMetadata = (metadata) => {
2306
+ var validateAudioChunkMetadata = (metadata, trackCodec) => {
2302
2307
  if (!metadata) {
2303
2308
  throw new TypeError("Audio chunk metadata must be provided.");
2304
2309
  }
@@ -2392,6 +2397,11 @@ var Mediabunny = (() => {
2392
2397
  );
2393
2398
  }
2394
2399
  }
2400
+ if (trackCodec !== null && inferCodecFromCodecString(metadata.decoderConfig.codec) !== trackCodec) {
2401
+ throw new TypeError(
2402
+ `Audio chunk metadata decoder configuration codec string '${metadata.decoderConfig.codec}' does not fit to the track codec '${trackCodec}'.`
2403
+ );
2404
+ }
2395
2405
  };
2396
2406
  var validateSubtitleMetadata = (metadata) => {
2397
2407
  if (!metadata) {
@@ -5579,6 +5589,7 @@ var Mediabunny = (() => {
5579
5589
  return this.metadataPromise ??= (async () => {
5580
5590
  let currentPos = 0;
5581
5591
  let lookForMfraBox = false;
5592
+ let foundMovieBoxes = false;
5582
5593
  while (true) {
5583
5594
  let slice = this.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
5584
5595
  if (slice instanceof Promise) slice = await slice;
@@ -5602,6 +5613,7 @@ var Mediabunny = (() => {
5602
5613
  track.editListOffset -= Math.round(previousSegmentDurationsInSeconds * track.timescale);
5603
5614
  }
5604
5615
  lookForMfraBox = this.isFragmented && this.reader.fileSize !== null && this.reader.fileSize > startPos + boxInfo.totalSize;
5616
+ foundMovieBoxes = true;
5605
5617
  break;
5606
5618
  } else if (boxInfo.name === "moof") {
5607
5619
  if (!this.input._initInput) {
@@ -5609,61 +5621,16 @@ var Mediabunny = (() => {
5609
5621
  '"moof" box encountered with no "moov" box present; this file is likely a Segment as described in ISO/IEC 14496-12 Section 8.16. A separate init file that contains a "moov" box is required to read this file, please provide it using InputOptions.initInput.'
5610
5622
  );
5611
5623
  }
5612
- const initDemuxer = await this.input._initInput._getDemuxer();
5613
- if (initDemuxer.constructor !== _IsobmffDemuxer) {
5614
- throw new Error("Init input must match the input's format.");
5615
- }
5616
- await initDemuxer.readMetadata();
5617
- this.movieTimescale = initDemuxer.movieTimescale;
5618
- this.movieDurationInTimescale = initDemuxer.movieDurationInTimescale;
5619
- this.metadataTags = initDemuxer.metadataTags;
5620
- this.isFragmented = true;
5621
- this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
5622
- this.psshBoxes = initDemuxer.psshBoxes;
5623
- for (const foreignTrack of initDemuxer.tracks) {
5624
- const track = {
5625
- id: foreignTrack.id,
5626
- demuxer: this,
5627
- trackBacking: null,
5628
- disposition: foreignTrack.disposition,
5629
- timescale: foreignTrack.timescale,
5630
- durationInMediaTimescale: foreignTrack.durationInMediaTimescale,
5631
- durationInMovieTimescale: foreignTrack.durationInMovieTimescale,
5632
- rotation: foreignTrack.rotation,
5633
- internalCodecId: foreignTrack.internalCodecId,
5634
- name: foreignTrack.name,
5635
- languageCode: foreignTrack.languageCode,
5636
- sampleTableByteOffset: null,
5637
- sampleTable: null,
5638
- fragmentLookupTable: [],
5639
- currentFragmentState: null,
5640
- fragmentPositionCache: [],
5641
- editListPreviousSegmentDurations: foreignTrack.editListPreviousSegmentDurations,
5642
- editListOffset: foreignTrack.editListOffset,
5643
- encryptionInfo: foreignTrack.encryptionInfo,
5644
- encryptionAuxInfo: null,
5645
- frmaCodecString: null,
5646
- info: foreignTrack.info
5647
- };
5648
- if (foreignTrack.trackBacking) {
5649
- assert(track.info);
5650
- if (track.info.type === "video" && track.info.width !== -1) {
5651
- const videoTrack = track;
5652
- track.trackBacking = new IsobmffVideoTrackBacking(videoTrack);
5653
- this.tracks.push(track);
5654
- } else if (track.info.type === "audio" && track.info.numberOfChannels !== -1) {
5655
- const audioTrack = track;
5656
- track.trackBacking = new IsobmffAudioTrackBacking(audioTrack);
5657
- this.tracks.push(track);
5658
- }
5659
- } else {
5660
- }
5661
- }
5624
+ await this.copyMetadataFromInitInput(this.input._initInput);
5662
5625
  lookForMfraBox = false;
5626
+ foundMovieBoxes = true;
5663
5627
  break;
5664
5628
  }
5665
5629
  currentPos = startPos + boxInfo.totalSize;
5666
5630
  }
5631
+ if (!foundMovieBoxes && this.input._initInput) {
5632
+ await this.copyMetadataFromInitInput(this.input._initInput);
5633
+ }
5667
5634
  if (lookForMfraBox) {
5668
5635
  assert(this.reader.fileSize !== null);
5669
5636
  let lastWordSlice = this.reader.requestSlice(this.reader.fileSize - 4, 4);
@@ -5692,6 +5659,58 @@ var Mediabunny = (() => {
5692
5659
  }
5693
5660
  })();
5694
5661
  }
5662
+ async copyMetadataFromInitInput(initInput) {
5663
+ const initDemuxer = await initInput._getDemuxer();
5664
+ if (initDemuxer.constructor !== _IsobmffDemuxer) {
5665
+ throw new Error("Init input must match the input's format.");
5666
+ }
5667
+ await initDemuxer.readMetadata();
5668
+ this.movieTimescale = initDemuxer.movieTimescale;
5669
+ this.movieDurationInTimescale = initDemuxer.movieDurationInTimescale;
5670
+ this.metadataTags = initDemuxer.metadataTags;
5671
+ this.isFragmented = true;
5672
+ this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
5673
+ this.psshBoxes = initDemuxer.psshBoxes;
5674
+ for (const foreignTrack of initDemuxer.tracks) {
5675
+ const track = {
5676
+ id: foreignTrack.id,
5677
+ demuxer: this,
5678
+ trackBacking: null,
5679
+ disposition: foreignTrack.disposition,
5680
+ timescale: foreignTrack.timescale,
5681
+ durationInMediaTimescale: foreignTrack.durationInMediaTimescale,
5682
+ durationInMovieTimescale: foreignTrack.durationInMovieTimescale,
5683
+ rotation: foreignTrack.rotation,
5684
+ internalCodecId: foreignTrack.internalCodecId,
5685
+ name: foreignTrack.name,
5686
+ languageCode: foreignTrack.languageCode,
5687
+ sampleTableByteOffset: null,
5688
+ sampleTable: null,
5689
+ fragmentLookupTable: [],
5690
+ currentFragmentState: null,
5691
+ fragmentPositionCache: [],
5692
+ editListPreviousSegmentDurations: foreignTrack.editListPreviousSegmentDurations,
5693
+ editListOffset: foreignTrack.editListOffset,
5694
+ encryptionInfo: foreignTrack.encryptionInfo,
5695
+ encryptionAuxInfo: null,
5696
+ frmaCodecString: null,
5697
+ info: foreignTrack.info
5698
+ };
5699
+ if (foreignTrack.trackBacking) {
5700
+ assert(track.info);
5701
+ if (track.info.type === "video" && track.info.width !== -1) {
5702
+ const videoTrack = track;
5703
+ track.trackBacking = new IsobmffVideoTrackBacking(videoTrack);
5704
+ this.tracks.push(track);
5705
+ } else if (track.info.type === "audio" && track.info.numberOfChannels !== -1) {
5706
+ const audioTrack = track;
5707
+ track.trackBacking = new IsobmffAudioTrackBacking(audioTrack);
5708
+ this.tracks.push(track);
5709
+ }
5710
+ } else {
5711
+ }
5712
+ }
5713
+ }
5695
5714
  getSampleTableForTrack(internalTrack) {
5696
5715
  if (internalTrack.sampleTable) {
5697
5716
  return internalTrack.sampleTable;
@@ -11102,6 +11121,8 @@ var Mediabunny = (() => {
11102
11121
  this.metadataPromise = null;
11103
11122
  this.firstFrameHeader = null;
11104
11123
  this.firstFrameHeaderPos = null;
11124
+ this.xingFrameHeader = null;
11125
+ this.xingFrameHeaderPos = null;
11105
11126
  this.loadedSamples = [];
11106
11127
  // All samples from the start of the file to lastLoadedPos
11107
11128
  this.metadataTags = null;
@@ -11118,6 +11139,10 @@ var Mediabunny = (() => {
11118
11139
  while (!this.firstFrameHeader && !this.lastSampleLoaded) {
11119
11140
  await this.advanceReader();
11120
11141
  }
11142
+ if (!this.firstFrameHeader && this.xingFrameHeader) {
11143
+ this.firstFrameHeader = this.xingFrameHeader;
11144
+ this.firstFrameHeaderPos = this.xingFrameHeaderPos;
11145
+ }
11121
11146
  if (!this.firstFrameHeader) {
11122
11147
  throw new Error("No valid MP3 frame found.");
11123
11148
  }
@@ -11159,6 +11184,10 @@ var Mediabunny = (() => {
11159
11184
  const word = readU32Be(slice);
11160
11185
  const isXing = word === XING || word === INFO;
11161
11186
  if (isXing) {
11187
+ if (!this.xingFrameHeader) {
11188
+ this.xingFrameHeader = header;
11189
+ this.xingFrameHeaderPos = result.startPos;
11190
+ }
11162
11191
  if (!this.xingData) {
11163
11192
  let xingDataSlice = this.reader.requestSlice(result.startPos + xingOffset + 4, 12);
11164
11193
  if (xingDataSlice instanceof Promise) xingDataSlice = await xingDataSlice;
@@ -14157,6 +14186,9 @@ var Mediabunny = (() => {
14157
14186
  throw new Error("No Program Map Table found in the file.");
14158
14187
  }
14159
14188
  for (const stream of this.elementaryStreams) {
14189
+ if (!stream.initialized) {
14190
+ continue;
14191
+ }
14160
14192
  if (stream.info.type === "video") {
14161
14193
  this.trackBackingEntries.push(
14162
14194
  new MpegTsVideoTrackBacking(stream)
@@ -15619,6 +15651,7 @@ var Mediabunny = (() => {
15619
15651
  this.packetInfos = /* @__PURE__ */ new WeakMap();
15620
15652
  this.hydrationPromise = null;
15621
15653
  this.firstInputTrack = null;
15654
+ this.firstSegment = null;
15622
15655
  this.segmentedInput = segmentedInput;
15623
15656
  this.decl = decl;
15624
15657
  this.number = number;
@@ -15629,13 +15662,22 @@ var Mediabunny = (() => {
15629
15662
  if (!this.segmentedInput.firstSegment) {
15630
15663
  throw new Error("Missing first segment, can't retrieve track.");
15631
15664
  }
15632
- const input = this.segmentedInput.getInputForSegment(this.segmentedInput.firstSegment);
15633
- const inputTracks = await input.getTracks();
15634
- const track = inputTracks.find((x) => x.type === this.decl.type && x.number === this.number);
15665
+ let currentSegment = this.segmentedInput.firstSegment;
15666
+ let track = null;
15667
+ while (currentSegment) {
15668
+ const input = this.segmentedInput.getInputForSegment(currentSegment);
15669
+ const inputTracks = await input.getTracks();
15670
+ track = inputTracks.find((x) => x.type === this.decl.type && x.number === this.number) ?? null;
15671
+ if (track) {
15672
+ break;
15673
+ }
15674
+ currentSegment = await this.segmentedInput.getNextSegment(currentSegment, {});
15675
+ }
15635
15676
  if (!track) {
15636
15677
  throw new Error("No matching track found in underlying media data.");
15637
15678
  }
15638
15679
  this.firstInputTrack = track;
15680
+ this.firstSegment = currentSegment;
15639
15681
  })();
15640
15682
  }
15641
15683
  getId() {
@@ -15724,13 +15766,28 @@ var Mediabunny = (() => {
15724
15766
  }
15725
15767
  async getFirstPacket(options) {
15726
15768
  await this.hydrate();
15727
- assert(this.segmentedInput.firstSegment);
15728
15769
  assert(this.firstInputTrack);
15729
- const packet = await this.firstInputTrack._backing.getFirstPacket(options);
15730
- if (!packet) {
15731
- return null;
15770
+ assert(this.firstSegment);
15771
+ let currentTrack = this.firstInputTrack;
15772
+ let currentSegment = this.firstSegment;
15773
+ while (true) {
15774
+ if (currentTrack) {
15775
+ const packet = await currentTrack._backing.getFirstPacket(options);
15776
+ if (packet) {
15777
+ return this.createAdjustedPacket(packet, currentSegment, currentTrack);
15778
+ }
15779
+ }
15780
+ currentSegment = await this.segmentedInput.getNextSegment(currentSegment, {
15781
+ skipLiveWait: options.skipLiveWait
15782
+ });
15783
+ if (!currentSegment) {
15784
+ break;
15785
+ }
15786
+ const nextInput = this.segmentedInput.getInputForSegment(currentSegment);
15787
+ const nextTracks = await nextInput.getTracks();
15788
+ currentTrack = nextTracks.find((t) => t.type === this.firstInputTrack.type && t.number === this.firstInputTrack.number) ?? null;
15732
15789
  }
15733
- return this.createAdjustedPacket(packet, this.segmentedInput.firstSegment, this.firstInputTrack);
15790
+ return null;
15734
15791
  }
15735
15792
  getNextPacket(packet, options) {
15736
15793
  return this._getNextInternal(packet, options, false);
@@ -16291,6 +16348,12 @@ var Mediabunny = (() => {
16291
16348
  * @internal
16292
16349
  */
16293
16350
  this._fileSizeDetermined = false;
16351
+ /**
16352
+ * When the server doesn't support range requests, we abandon the orchestrator and instead defer to an internal
16353
+ * ReadableStreamSource wrapping the response body, which pulls new data only when reads demand it.
16354
+ * @internal
16355
+ */
16356
+ this._sequentialBacking = null;
16294
16357
  this._url = url2;
16295
16358
  this._options = options;
16296
16359
  this._getRetryDelay = options.getRetryDelay ?? DEFAULT_RETRY_DELAY;
@@ -16334,7 +16397,7 @@ var Mediabunny = (() => {
16334
16397
  if (!this._fileSizeDetermined) {
16335
16398
  return this._length !== null ? this._length : void 0;
16336
16399
  }
16337
- const baseSize = this._orchestrator.fileSize;
16400
+ const baseSize = this._sequentialBacking ? this._sequentialBacking._endIndex : this._orchestrator.fileSize;
16338
16401
  if (baseSize === null) {
16339
16402
  return this._length !== null ? this._length : null;
16340
16403
  }
@@ -16346,7 +16409,7 @@ var Mediabunny = (() => {
16346
16409
  return null;
16347
16410
  }
16348
16411
  const offset = this._offset;
16349
- const result = this._orchestrator.read(
16412
+ const result = this._sequentialBacking ? this._sequentialBacking._read(offset + start, offset + end) : this._orchestrator.read(
16350
16413
  offset + start,
16351
16414
  offset + end,
16352
16415
  Math.max(offset + minReadPosition, offset),
@@ -16400,11 +16463,21 @@ var Mediabunny = (() => {
16400
16463
  }
16401
16464
  const contentLength = response.headers.get("Content-Length");
16402
16465
  if (contentLength) {
16403
- this._orchestrator.supplyFileSize(worker.currentPos + Number(contentLength));
16466
+ const basePos = response.status === 206 ? worker.currentPos : 0;
16467
+ this._orchestrator.supplyFileSize(basePos + Number(contentLength));
16404
16468
  }
16405
16469
  }
16406
16470
  this._fileSizeDetermined = true;
16471
+ if (!response.body) {
16472
+ throw new Error(
16473
+ "Missing HTTP response body stream. The used fetch function must provide the response body as a ReadableStream."
16474
+ );
16475
+ }
16407
16476
  if (response.status !== 206) {
16477
+ if (this._sequentialBacking) {
16478
+ void response.body.cancel();
16479
+ return;
16480
+ }
16408
16481
  if (!this._usedForHls) {
16409
16482
  const url2 = new URL(
16410
16483
  this._url instanceof Request ? this._url.url : this._url,
@@ -16413,26 +16486,14 @@ var Mediabunny = (() => {
16413
16486
  if (url2.origin !== "null" && !(url2.pathname.endsWith(".m3u8") || url2.pathname.endsWith(".m3u"))) {
16414
16487
  if (!warnedOrigins.has(url2.origin)) {
16415
16488
  Logging._warn(
16416
- `HTTP server (origin ${url2.origin}) did not respond to a range request with 206 Partial Content, meaning the entire resource will now be downloaded. To enable efficient media file streaming across a network, please make sure your server supports range requests.`
16489
+ `HTTP server (origin ${url2.origin}) did not respond to a range request with 206 Partial Content, meaning the resource will now be streamed sequentially, with old data being evicted from the cache. Reads into evicted regions will throw. To enable efficient media file streaming across a network, please make sure your server supports range requests. Alternatively, set maxCacheSize to Infinity in the UrlSource options to keep the entire resource in memory.`
16417
16490
  );
16418
16491
  warnedOrigins.add(url2.origin);
16419
16492
  }
16420
16493
  }
16421
16494
  }
16422
- worker.currentPos = 0;
16423
- this._orchestrator.options.maxCacheSize = Infinity;
16424
- if (this._orchestrator.fileSize !== null) {
16425
- worker.targetPos = this._orchestrator.fileSize;
16426
- } else {
16427
- worker.targetPos = Infinity;
16428
- worker.strictTarget = false;
16429
- }
16430
- this._orchestrator.consolidateEverythingIntoOneWorker(worker);
16431
- }
16432
- if (!response.body) {
16433
- throw new Error(
16434
- "Missing HTTP response body stream. The used fetch function must provide the response body as a ReadableStream."
16435
- );
16495
+ this._transitionToSequentialMode(response.body);
16496
+ return;
16436
16497
  }
16437
16498
  const reader = response.body.getReader();
16438
16499
  while (true) {
@@ -16479,8 +16540,119 @@ var Mediabunny = (() => {
16479
16540
  }
16480
16541
  }
16481
16542
  /** @internal */
16543
+ _transitionToSequentialMode(body) {
16544
+ let currentReader = body.getReader();
16545
+ let streamPosition = 0;
16546
+ let skipRemaining = 0;
16547
+ const wrappedStream = new ReadableStream({
16548
+ pull: async (controller) => {
16549
+ while (true) {
16550
+ let readResult;
16551
+ try {
16552
+ readResult = await currentReader.read();
16553
+ } catch (error) {
16554
+ if (this._disposed) {
16555
+ throw error;
16556
+ }
16557
+ const retryDelayInSeconds = this._getRetryDelay(1, error, this._url);
16558
+ if (retryDelayInSeconds === null) {
16559
+ throw error;
16560
+ }
16561
+ Logging._error("Error while reading response stream. Attempting to resume.", error);
16562
+ await wait(1e3 * retryDelayInSeconds);
16563
+ const newResponse = await retriedFetch(
16564
+ this._options.fetchFn ?? fetch,
16565
+ this._url,
16566
+ mergeRequestInit(this._requestInit, {
16567
+ headers: {
16568
+ // Who knows, maybe the server honors range requests this time
16569
+ Range: `bytes=${streamPosition}-`
16570
+ }
16571
+ }),
16572
+ this._getRetryDelay,
16573
+ () => this._disposed
16574
+ );
16575
+ if (!newResponse.ok) {
16576
+ throw new Error(
16577
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
16578
+ `Error fetching ${String(this._url)}: ${newResponse.status} ${newResponse.statusText}`
16579
+ );
16580
+ }
16581
+ if (!newResponse.body) {
16582
+ throw new Error(
16583
+ "Missing HTTP response body stream. The used fetch function must provide the response body as a ReadableStream."
16584
+ );
16585
+ }
16586
+ currentReader = newResponse.body.getReader();
16587
+ skipRemaining = newResponse.status === 206 ? 0 : streamPosition;
16588
+ continue;
16589
+ }
16590
+ if (readResult.done) {
16591
+ controller.close();
16592
+ return;
16593
+ }
16594
+ let chunk = readResult.value;
16595
+ if (skipRemaining > 0) {
16596
+ const skippedAmount = Math.min(skipRemaining, chunk.length);
16597
+ skipRemaining -= skippedAmount;
16598
+ chunk = chunk.subarray(skippedAmount);
16599
+ }
16600
+ if (chunk.length === 0) {
16601
+ continue;
16602
+ }
16603
+ streamPosition += chunk.length;
16604
+ controller.enqueue(chunk);
16605
+ return;
16606
+ }
16607
+ },
16608
+ cancel: () => currentReader.cancel()
16609
+ });
16610
+ const backing = new ReadableStreamSource(wrappedStream, {
16611
+ maxCacheSize: this._orchestrator.options.maxCacheSize
16612
+ });
16613
+ backing._endIndex = this._orchestrator.fileSize;
16614
+ backing._cacheMissErrorMessage = "Attempted to read data from an already-evicted part of the cache. Because the HTTP server did not honor the range request, data can only be read sequentially, with old data being evicted from the cache. To fix this issue, either ensure your server responds to range requests with 206 Partial Content, or set maxCacheSize to Infinity in the UrlSource options. Note that the latter will store the entire file in the cache if needed, no matter how large.";
16615
+ backing.on("read", ({ start, end }) => this._dispatchRead(start, end));
16616
+ this._sequentialBacking = backing;
16617
+ const uniqueSlices = /* @__PURE__ */ new Set();
16618
+ for (const otherWorker of this._orchestrator.workers) {
16619
+ for (const slice of otherWorker.pendingSlices) {
16620
+ uniqueSlices.add(slice);
16621
+ }
16622
+ otherWorker.aborted = true;
16623
+ otherWorker.pendingSlices.length = 0;
16624
+ }
16625
+ for (const queuedRead of this._orchestrator.queuedReads) {
16626
+ for (const slice of queuedRead.pendingSlices) {
16627
+ uniqueSlices.add(slice);
16628
+ }
16629
+ }
16630
+ this._orchestrator.workers.length = 0;
16631
+ this._orchestrator.queuedReads.length = 0;
16632
+ for (const slice of uniqueSlices) {
16633
+ const result = backing._read(slice.start, slice.start + slice.bytes.length);
16634
+ if (result instanceof Promise) {
16635
+ result.then((readResult) => {
16636
+ if (readResult) {
16637
+ assert(readResult.offset === slice.start);
16638
+ slice.resolve(readResult.bytes);
16639
+ } else {
16640
+ slice.resolve(null);
16641
+ }
16642
+ }, (error) => slice.reject(error));
16643
+ } else {
16644
+ assert(result === null);
16645
+ slice.resolve(null);
16646
+ }
16647
+ }
16648
+ }
16649
+ /** @internal */
16482
16650
  _dispose() {
16483
16651
  this._orchestrator.dispose();
16652
+ if (this._sequentialBacking) {
16653
+ this._sequentialBacking._disposed = true;
16654
+ this._sequentialBacking._dispose();
16655
+ }
16484
16656
  }
16485
16657
  };
16486
16658
  var BYTE_RANGE_REGEX = /^bytes=(\d+)-(\d*)$/;
@@ -16699,6 +16871,11 @@ var Mediabunny = (() => {
16699
16871
  this._endIndex = null;
16700
16872
  /** @internal */
16701
16873
  this._pulling = false;
16874
+ /**
16875
+ * Overridable for internal use.
16876
+ * @internal
16877
+ */
16878
+ this._cacheMissErrorMessage = "Attempted to read data from an already-evicted part of the cache. With ReadableStreamSource, you must access the data more sequentially or increase the size of its cache.";
16702
16879
  this._stream = stream;
16703
16880
  this._maxCacheSize = options.maxCacheSize ?? 32 * 2 ** 20;
16704
16881
  }
@@ -16778,9 +16955,7 @@ var Mediabunny = (() => {
16778
16955
  }
16779
16956
  /** @internal */
16780
16957
  _throwDueToCacheMiss() {
16781
- throw new Error(
16782
- "Read is before the cached region. With ReadableStreamSource, you must access the data more sequentially or increase the size of its cache."
16783
- );
16958
+ throw new Error(this._cacheMissErrorMessage);
16784
16959
  }
16785
16960
  /** @internal */
16786
16961
  async _pull() {
@@ -17170,30 +17345,6 @@ var Mediabunny = (() => {
17170
17345
  }
17171
17346
  });
17172
17347
  }
17173
- consolidateEverythingIntoOneWorker(worker) {
17174
- const uniqueSlices = new Set(worker.pendingSlices);
17175
- for (let i = 0; i < this.workers.length; i++) {
17176
- const otherWorker = this.workers[i];
17177
- if (otherWorker === worker) {
17178
- continue;
17179
- }
17180
- for (const slice of otherWorker.pendingSlices) {
17181
- uniqueSlices.add(slice);
17182
- }
17183
- otherWorker.aborted = true;
17184
- otherWorker.pendingSlices.length = 0;
17185
- this.workers.splice(i, 1);
17186
- i--;
17187
- }
17188
- for (let i = 0; i < this.queuedReads.length; i++) {
17189
- const queuedRead = this.queuedReads[i];
17190
- for (const slice of queuedRead.pendingSlices) {
17191
- uniqueSlices.add(slice);
17192
- }
17193
- }
17194
- worker.pendingSlices = [...uniqueSlices];
17195
- this.queuedReads.length = 0;
17196
- }
17197
17348
  /** Called by a worker when it has read some data. */
17198
17349
  supplyWorkerData(worker, bytes2) {
17199
17350
  assert(!worker.aborted);
@@ -19504,12 +19655,22 @@ var Mediabunny = (() => {
19504
19655
  if (init.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
19505
19656
  throw new TypeError("init.duration, when provided, must be a non-negative number.");
19506
19657
  }
19658
+ if (init.visibleRect !== void 0) {
19659
+ validateRectangle(init.visibleRect, "init.visibleRect");
19660
+ }
19507
19661
  if (typeof VideoFrame !== "undefined") {
19508
19662
  return new _VideoSample(
19509
19663
  new VideoFrame(data, {
19510
19664
  timestamp: Math.trunc(init.timestamp * SECOND_TO_MICROSECOND_FACTOR),
19511
19665
  // Drag 0 to undefined
19512
- duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR) || void 0
19666
+ duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR) || void 0,
19667
+ // WebCodecs wants DOMRectInit
19668
+ visibleRect: init.visibleRect && {
19669
+ x: init.visibleRect.left,
19670
+ y: init.visibleRect.top,
19671
+ width: init.visibleRect.width,
19672
+ height: init.visibleRect.height
19673
+ }
19513
19674
  }),
19514
19675
  init
19515
19676
  );
@@ -19529,7 +19690,8 @@ var Mediabunny = (() => {
19529
19690
  if (!width || !height) {
19530
19691
  throw new TypeError("Could not determine dimensions.");
19531
19692
  }
19532
- const canvas = new OffscreenCanvas(width, height);
19693
+ const visibleRect = init.visibleRect ?? { left: 0, top: 0, width, height };
19694
+ const canvas = new OffscreenCanvas(visibleRect.width, visibleRect.height);
19533
19695
  const context = canvas.getContext("2d", {
19534
19696
  alpha: isFirefox(),
19535
19697
  // Firefox has VideoFrame glitches with opaque canvases
@@ -19540,13 +19702,13 @@ var Mediabunny = (() => {
19540
19702
  "OffscreenCanvas must have support for the '2d' context in order to create a VideoSample from this data."
19541
19703
  );
19542
19704
  }
19543
- context.drawImage(data, 0, 0);
19705
+ context.drawImage(data, -visibleRect.left, -visibleRect.top);
19544
19706
  this._data = canvas;
19545
19707
  this._layout = null;
19546
19708
  this.format = "RGBX";
19547
- this.visibleRect = { left: 0, top: 0, width, height };
19548
- this.squarePixelWidth = width;
19549
- this.squarePixelHeight = height;
19709
+ this.visibleRect = { left: 0, top: 0, width: visibleRect.width, height: visibleRect.height };
19710
+ this.squarePixelWidth = visibleRect.width;
19711
+ this.squarePixelHeight = visibleRect.height;
19550
19712
  this.rotation = init.rotation ?? 0;
19551
19713
  this.timestamp = init.timestamp;
19552
19714
  this.duration = init.duration ?? 0;
@@ -20123,6 +20285,61 @@ var Mediabunny = (() => {
20123
20285
  }
20124
20286
  return { sx, sy, sWidth, sHeight };
20125
20287
  }
20288
+ /**
20289
+ * Draws the sample onto the target canvas with fit behavior, manually mipmapping on strong downscales for quality.
20290
+ * @internal
20291
+ */
20292
+ _drawWithFitAndMipmapping(targetCanvas, targetContext, options) {
20293
+ const targetWidth = targetCanvas.width;
20294
+ const targetHeight = targetCanvas.height;
20295
+ const [rotatedWidth, rotatedHeight] = options.rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
20296
+ const sourceWidth = options.crop ? options.crop.width : rotatedWidth;
20297
+ const sourceHeight = options.crop ? options.crop.height : rotatedHeight;
20298
+ let mipLevels = 0;
20299
+ if (2 * targetWidth < sourceWidth && 2 * targetHeight < sourceHeight) {
20300
+ mipLevels = Math.floor(Math.log2(
20301
+ Math.min(sourceWidth / targetWidth, sourceHeight / targetHeight)
20302
+ ));
20303
+ }
20304
+ const drawWidth = targetWidth * 2 ** mipLevels;
20305
+ const drawHeight = targetHeight * 2 ** mipLevels;
20306
+ const { canvas, context, isNew } = mipLevels > 0 ? getTransformationCanvas(drawWidth, drawHeight) : { canvas: targetCanvas, context: targetContext, isNew: options.targetIsFresh };
20307
+ context.imageSmoothingQuality = "high";
20308
+ if (options.fillBlack) {
20309
+ context.fillStyle = "black";
20310
+ context.fillRect(0, 0, drawWidth, drawHeight);
20311
+ } else if (!isNew) {
20312
+ context.clearRect(0, 0, drawWidth, drawHeight);
20313
+ }
20314
+ this.drawWithFit(context, {
20315
+ fit: options.fit,
20316
+ rotation: options.rotation,
20317
+ crop: options.crop
20318
+ });
20319
+ context.globalCompositeOperation = "copy";
20320
+ for (let i = mipLevels; i > 1; i--) {
20321
+ const levelWidth = targetWidth * 2 ** i;
20322
+ const levelHeight = targetHeight * 2 ** i;
20323
+ context.drawImage(canvas, 0, 0, levelWidth, levelHeight, 0, 0, levelWidth / 2, levelHeight / 2);
20324
+ }
20325
+ context.globalCompositeOperation = "source-over";
20326
+ if (mipLevels > 0) {
20327
+ targetContext.imageSmoothingQuality = "high";
20328
+ targetContext.globalCompositeOperation = "copy";
20329
+ targetContext.drawImage(
20330
+ canvas,
20331
+ 0,
20332
+ 0,
20333
+ 2 * targetWidth,
20334
+ 2 * targetHeight,
20335
+ 0,
20336
+ 0,
20337
+ targetWidth,
20338
+ targetHeight
20339
+ );
20340
+ targetContext.globalCompositeOperation = "source-over";
20341
+ }
20342
+ }
20126
20343
  /**
20127
20344
  * Converts this video sample to a
20128
20345
  * [`CanvasImageSource`](https://udn.realityripple.com/docs/Web/API/CanvasImageSource) for drawing to a canvas.
@@ -20226,56 +20443,13 @@ var Mediabunny = (() => {
20226
20443
  return result;
20227
20444
  }
20228
20445
  }
20229
- let canvas = null;
20230
- let canvasIsNew = false;
20231
- for (const entry of transformationCanvasCache) {
20232
- if (entry.canvas.width === description.width && entry.canvas.height === description.height) {
20233
- canvas = entry.canvas;
20234
- entry.age = transformationCanvasCacheNextAge++;
20235
- break;
20236
- }
20237
- }
20238
- if (canvas === null) {
20239
- if (typeof OffscreenCanvas !== "undefined") {
20240
- canvas = new OffscreenCanvas(description.width, description.height);
20241
- } else {
20242
- if (typeof window === "undefined" || typeof document === "undefined") {
20243
- throw new Error(
20244
- "Cannot transform VideoSamples in this environment. Either run in an environment with OffscreenCanvas or HTMLCanvasElement, or supply a custom VideoSample transformer using registerVideoSampleTransformer()."
20245
- );
20246
- }
20247
- canvas = document.createElement("canvas");
20248
- canvas.width = description.width;
20249
- canvas.height = description.height;
20250
- }
20251
- canvasIsNew = true;
20252
- if (transformationCanvasCache.length >= TRANSFORMATION_CANVAS_CACHE_MAX_SIZE) {
20253
- transformationCanvasCache.splice(arrayArgmin(transformationCanvasCache, (x) => x.age), 1);
20254
- }
20255
- transformationCanvasCache.push({
20256
- canvas,
20257
- age: transformationCanvasCacheNextAge++
20258
- });
20259
- }
20260
- const context = canvas.getContext("2d", {
20261
- alpha: true
20262
- });
20263
- if (!context) {
20264
- throw new Error(
20265
- "The '2d' canvas context is required to transform VideoSamples. Register a custom transformer using registerVideoSampleTransformer to work around this limitation."
20266
- );
20267
- }
20268
- context.imageSmoothingQuality = "high";
20269
- if (description.alpha === "discard") {
20270
- context.fillStyle = "black";
20271
- context.fillRect(0, 0, description.width, description.height);
20272
- } else if (!canvasIsNew) {
20273
- context.clearRect(0, 0, description.width, description.height);
20274
- }
20275
- this.drawWithFit(context, {
20446
+ const { canvas, context, isNew } = getTransformationCanvas(description.width, description.height);
20447
+ this._drawWithFitAndMipmapping(canvas, context, {
20276
20448
  fit: description.fit,
20277
20449
  rotation: description.rotation,
20278
- crop: description.crop
20450
+ crop: description.crop,
20451
+ targetIsFresh: isNew,
20452
+ fillBlack: description.alpha === "discard"
20279
20453
  });
20280
20454
  return new _VideoSample(canvas, {
20281
20455
  timestamp: this.timestamp,
@@ -20327,6 +20501,45 @@ var Mediabunny = (() => {
20327
20501
  var TRANSFORMATION_CANVAS_CACHE_MAX_SIZE = 3;
20328
20502
  var transformationCanvasCache = [];
20329
20503
  var transformationCanvasCacheNextAge = 0;
20504
+ var getTransformationCanvas = (width, height) => {
20505
+ for (const entry of transformationCanvasCache) {
20506
+ if (entry.canvas.width === width && entry.canvas.height === height) {
20507
+ entry.age = transformationCanvasCacheNextAge++;
20508
+ return { canvas: entry.canvas, context: entry.context, isNew: false };
20509
+ }
20510
+ }
20511
+ let canvas;
20512
+ if (typeof OffscreenCanvas !== "undefined") {
20513
+ canvas = new OffscreenCanvas(width, height);
20514
+ } else {
20515
+ if (typeof window === "undefined" || typeof document === "undefined") {
20516
+ throw new Error(
20517
+ "Cannot transform VideoSamples in this environment. Either run in an environment with OffscreenCanvas or HTMLCanvasElement, or supply a custom VideoSample transformer using registerVideoSampleTransformer()."
20518
+ );
20519
+ }
20520
+ canvas = document.createElement("canvas");
20521
+ canvas.width = width;
20522
+ canvas.height = height;
20523
+ }
20524
+ const context = canvas.getContext("2d", {
20525
+ alpha: true,
20526
+ willReadFrequently: false
20527
+ });
20528
+ if (!context) {
20529
+ throw new Error(
20530
+ "The '2d' canvas context is required to transform VideoSamples. Register a custom transformer using registerVideoSampleTransformer to work around this limitation."
20531
+ );
20532
+ }
20533
+ if (transformationCanvasCache.length >= TRANSFORMATION_CANVAS_CACHE_MAX_SIZE) {
20534
+ transformationCanvasCache.splice(arrayArgmin(transformationCanvasCache, (x) => x.age), 1);
20535
+ }
20536
+ transformationCanvasCache.push({
20537
+ canvas,
20538
+ context,
20539
+ age: transformationCanvasCacheNextAge++
20540
+ });
20541
+ return { canvas, context, isNew: true };
20542
+ };
20330
20543
  var VideoSampleColorSpace = class {
20331
20544
  /** Creates a new VideoSampleColorSpace. */
20332
20545
  constructor(init) {
@@ -21894,8 +22107,12 @@ var Mediabunny = (() => {
21894
22107
  return false;
21895
22108
  }
21896
22109
  for (const { config, quantizer } of candidates) {
21897
- const support = await VideoEncoder.isConfigSupported(config);
21898
- if (!support.supported) {
22110
+ try {
22111
+ const support = await VideoEncoder.isConfigSupported(config);
22112
+ if (!support.supported) {
22113
+ continue;
22114
+ }
22115
+ } catch {
21899
22116
  continue;
21900
22117
  }
21901
22118
  if (!isFirefox()) {
@@ -21987,8 +22204,12 @@ var Mediabunny = (() => {
21987
22204
  if (typeof AudioEncoder === "undefined") {
21988
22205
  return false;
21989
22206
  }
21990
- const support = await AudioEncoder.isConfigSupported(encoderConfig);
21991
- return support.supported === true;
22207
+ try {
22208
+ const support = await AudioEncoder.isConfigSupported(encoderConfig);
22209
+ return support.supported === true;
22210
+ } catch {
22211
+ return false;
22212
+ }
21992
22213
  })();
21993
22214
  canEncodeAudioMemo.set(key, promise);
21994
22215
  return promise;
@@ -23521,19 +23742,12 @@ var Mediabunny = (() => {
23521
23742
  // Firefox has VideoFrame glitches with opaque canvases
23522
23743
  });
23523
23744
  assert(context);
23524
- context.imageSmoothingQuality = "high";
23525
- if (!canvasIsNew) {
23526
- if (!this._alpha && isFirefox()) {
23527
- context.fillStyle = "black";
23528
- context.fillRect(0, 0, width, height);
23529
- } else {
23530
- context.clearRect(0, 0, width, height);
23531
- }
23532
- }
23533
- sample.drawWithFit(context, {
23745
+ sample._drawWithFitAndMipmapping(canvas, context, {
23534
23746
  fit: this._fit,
23535
23747
  rotation: this._rotation,
23536
- crop: this._crop
23748
+ crop: this._crop,
23749
+ targetIsFresh: canvasIsNew,
23750
+ fillBlack: !this._alpha && isFirefox()
23537
23751
  });
23538
23752
  const result = {
23539
23753
  canvas,
@@ -24971,8 +25185,9 @@ var Mediabunny = (() => {
24971
25185
  if (filtered.length === 0) {
24972
25186
  return 0;
24973
25187
  }
24974
- const firstTimestamps = await Promise.all(filtered.map((x) => x.getFirstTimestamp()));
24975
- return Math.min(...firstTimestamps);
25188
+ const firstPackets = await Promise.all(filtered.map((x) => x._backing.getFirstPacket({ metadataOnly: true })));
25189
+ const result = Math.min(...firstPackets.map((x) => x?.timestamp ?? Infinity));
25190
+ return result === Infinity ? 0 : result;
24976
25191
  }
24977
25192
  /**
24978
25193
  * Computes the duration of the input file, in seconds. More precisely, returns the largest end timestamp among
@@ -26478,7 +26693,7 @@ var Mediabunny = (() => {
26478
26693
  try {
26479
26694
  this.validateTimestamp(track, packet.timestamp, packet.type === "key");
26480
26695
  if (this.inputIsAdts === null) {
26481
- validateAudioChunkMetadata(meta);
26696
+ validateAudioChunkMetadata(meta, track.source._codec);
26482
26697
  const description = meta?.decoderConfig?.description;
26483
26698
  this.inputIsAdts = !description;
26484
26699
  if (!this.inputIsAdts) {
@@ -26518,6 +26733,9 @@ var Mediabunny = (() => {
26518
26733
  }
26519
26734
  async finalize() {
26520
26735
  const release = await this.mutex.acquire();
26736
+ if (this.inputIsAdts === null) {
26737
+ throw new Error("Cannot finalize an empty ADTS file: not a single packet was added.");
26738
+ }
26521
26739
  release();
26522
26740
  }
26523
26741
  };
@@ -26541,8 +26759,46 @@ var Mediabunny = (() => {
26541
26759
  const release = await this.mutex.acquire();
26542
26760
  this.writer = await this.output._getRootWriter(!!this.format._options.appendOnly);
26543
26761
  this.writer.write(FLAC_HEADER);
26762
+ const track = this.output.tracks[0];
26763
+ assert(track?.isAudioTrack());
26764
+ if (track.metadata.decoderConfig) {
26765
+ validateAudioChunkMetadata({ decoderConfig: track.metadata.decoderConfig }, track.source._codec);
26766
+ this.applyDecoderConfig(track.metadata.decoderConfig);
26767
+ }
26544
26768
  release();
26545
26769
  }
26770
+ applyDecoderConfig(decoderConfig) {
26771
+ assert(decoderConfig.description);
26772
+ this.sampleRate = decoderConfig.sampleRate;
26773
+ this.channels = decoderConfig.numberOfChannels;
26774
+ const descriptionBitstream = new Bitstream(
26775
+ toUint8Array(decoderConfig.description)
26776
+ );
26777
+ descriptionBitstream.skipBits(103 + 64);
26778
+ this.bitsPerSample = descriptionBitstream.readBits(5) + 1;
26779
+ if (this.format._options.appendOnly) {
26780
+ this.writeHeader({
26781
+ // https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
26782
+ // Per RFC 9639, min/max block sizes can be looser than
26783
+ // actual values, so we use the full valid range (16–65535).
26784
+ // "The actual max block size MAY be smaller than what's
26785
+ // listed, and the actual min (excluding last block) MAY be
26786
+ // larger. This is because the encoder has to write these
26787
+ // fields before receiving any input audio data and cannot
26788
+ // know beforehand what block sizes it will use."
26789
+ minimumBlockSize: 16,
26790
+ maximumBlockSize: 65535,
26791
+ // https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
26792
+ // "A value of 0 signifies that the value is not known."
26793
+ minimumFrameSize: 0,
26794
+ maximumFrameSize: 0,
26795
+ sampleRate: this.sampleRate,
26796
+ channels: this.channels,
26797
+ bitsPerSample: this.bitsPerSample,
26798
+ totalSamples: 0
26799
+ });
26800
+ }
26801
+ }
26546
26802
  writeHeader({
26547
26803
  bitsPerSample,
26548
26804
  minimumBlockSize,
@@ -26647,40 +26903,10 @@ var Mediabunny = (() => {
26647
26903
  packet.type === "key"
26648
26904
  );
26649
26905
  if (this.sampleRate === null) {
26650
- validateAudioChunkMetadata(meta);
26906
+ validateAudioChunkMetadata(meta, track.source._codec);
26651
26907
  assert(meta);
26652
26908
  assert(meta.decoderConfig);
26653
- assert(meta.decoderConfig.description);
26654
- this.sampleRate = meta.decoderConfig.sampleRate;
26655
- this.channels = meta.decoderConfig.numberOfChannels;
26656
- const descriptionBitstream = new Bitstream(
26657
- toUint8Array(meta.decoderConfig.description)
26658
- );
26659
- descriptionBitstream.skipBits(103 + 64);
26660
- const bitsPerSample = descriptionBitstream.readBits(5) + 1;
26661
- this.bitsPerSample = bitsPerSample;
26662
- if (this.format._options.appendOnly) {
26663
- this.writeHeader({
26664
- // https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
26665
- // Per RFC 9639, min/max block sizes can be looser than
26666
- // actual values, so we use the full valid range (16–65535).
26667
- // "The actual max block size MAY be smaller than what's
26668
- // listed, and the actual min (excluding last block) MAY be
26669
- // larger. This is because the encoder has to write these
26670
- // fields before receiving any input audio data and cannot
26671
- // know beforehand what block sizes it will use."
26672
- minimumBlockSize: 16,
26673
- maximumBlockSize: 65535,
26674
- // https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
26675
- // "A value of 0 signifies that the value is not known."
26676
- minimumFrameSize: 0,
26677
- maximumFrameSize: 0,
26678
- sampleRate: this.sampleRate,
26679
- channels: this.channels,
26680
- bitsPerSample: this.bitsPerSample,
26681
- totalSamples: 0
26682
- });
26683
- }
26909
+ this.applyDecoderConfig(meta.decoderConfig);
26684
26910
  }
26685
26911
  if (!this.metadataWritten) {
26686
26912
  this.writeVorbisCommentAndPictureBlock();
@@ -26714,6 +26940,14 @@ var Mediabunny = (() => {
26714
26940
  }
26715
26941
  async finalize() {
26716
26942
  const release = await this.mutex.acquire();
26943
+ if (this.sampleRate === null) {
26944
+ throw new Error(
26945
+ "Cannot finalize an empty FLAC file: no packets were added and the track specified no decoderConfig in its metadata, so there's no telling what the file should look like."
26946
+ );
26947
+ }
26948
+ if (!this.metadataWritten) {
26949
+ this.writeVorbisCommentAndPictureBlock();
26950
+ }
26717
26951
  if (!this.format._options.appendOnly) {
26718
26952
  let minimumBlockSize = Infinity;
26719
26953
  let maximumBlockSize = 0;
@@ -26731,7 +26965,12 @@ var Mediabunny = (() => {
26731
26965
  }
26732
26966
  minimumBlockSize = Math.min(minimumBlockSize, this.blockSizes[i]);
26733
26967
  }
26734
- assert(this.sampleRate !== null);
26968
+ if (this.blockSizes.length === 0) {
26969
+ minimumBlockSize = 16;
26970
+ maximumBlockSize = 65535;
26971
+ minimumFrameSize = 0;
26972
+ maximumFrameSize = 0;
26973
+ }
26735
26974
  assert(this.channels !== null);
26736
26975
  assert(this.bitsPerSample !== null);
26737
26976
  this.writer.seek(4);
@@ -27711,7 +27950,8 @@ var Mediabunny = (() => {
27711
27950
  ]);
27712
27951
  };
27713
27952
  var dac3 = (trackData) => {
27714
- const frameInfo = parseAc3SyncFrame(trackData.info.firstPacket.data);
27953
+ assert(trackData.info.primingPacket);
27954
+ const frameInfo = parseAc3SyncFrame(trackData.info.primingPacket.data);
27715
27955
  if (!frameInfo) {
27716
27956
  throw new Error(
27717
27957
  "Couldn't extract AC-3 frame info from the audio packet. Ensure the packets contain valid AC-3 sync frames (as specified in ETSI TS 102 366)."
@@ -27729,7 +27969,8 @@ var Mediabunny = (() => {
27729
27969
  return box("dac3", [...bytes2]);
27730
27970
  };
27731
27971
  var dec3 = (trackData) => {
27732
- const frameInfo = parseEac3SyncFrame(trackData.info.firstPacket.data);
27972
+ assert(trackData.info.primingPacket);
27973
+ const frameInfo = parseEac3SyncFrame(trackData.info.primingPacket.data);
27733
27974
  if (!frameInfo) {
27734
27975
  throw new Error(
27735
27976
  "Couldn't extract E-AC-3 frame info from the audio packet. Ensure the packets contain valid E-AC-3 sync frames (as specified in ETSI TS 102 366)."
@@ -28030,7 +28271,7 @@ var Mediabunny = (() => {
28030
28271
  mfro()
28031
28272
  ]);
28032
28273
  };
28033
- var tfra = (trackData, trackIndex) => {
28274
+ var tfra = (trackData) => {
28034
28275
  const version = 1;
28035
28276
  return fullBox("tfra", version, 0, [
28036
28277
  u32(trackData.track.id),
@@ -28044,7 +28285,7 @@ var Mediabunny = (() => {
28044
28285
  // Time (in presentation time)
28045
28286
  u64(chunk.moofOffset),
28046
28287
  // moof offset
28047
- u32(trackIndex + 1),
28288
+ u32(chunk.trafIndex + 1),
28048
28289
  // traf number
28049
28290
  u32(1),
28050
28291
  // trun number
@@ -28968,9 +29209,16 @@ var Mediabunny = (() => {
28968
29209
  if (this._monotonicity === true && position !== this._lastFlushEnd) {
28969
29210
  throw new Error("Internal error: Monotonicity violation.");
28970
29211
  }
29212
+ const isPartialView = section.start !== 0 || section.end !== chunk.data.byteLength;
29213
+ let data;
29214
+ if (isPartialView && isWebKit()) {
29215
+ data = chunk.data.slice(section.start, section.end);
29216
+ } else {
29217
+ data = chunk.data.subarray(section.start, section.end);
29218
+ }
28971
29219
  void this._streamWriter.write({
28972
29220
  type: "write",
28973
- data: chunk.data.subarray(section.start, section.end),
29221
+ data,
28974
29222
  position
28975
29223
  }).catch((error) => {
28976
29224
  this._writeError ??= error;
@@ -29213,6 +29461,7 @@ var Mediabunny = (() => {
29213
29461
  this.allTracksKnown = promiseWithResolvers();
29214
29462
  this.creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET;
29215
29463
  this.finalizedChunks = [];
29464
+ this.wroteFragmentedHeader = false;
29216
29465
  this.nextFragmentNumber = 1;
29217
29466
  // Only relevant for fragmented files, to make sure new fragments start with the highest timestamp seen so far
29218
29467
  this.maxWrittenTimestamp = -Infinity;
@@ -29220,17 +29469,18 @@ var Mediabunny = (() => {
29220
29469
  this.maxWrittenEndTimestamp = -Infinity;
29221
29470
  this.segmentHeaderSize = null;
29222
29471
  this.format = format;
29472
+ this.formatOptions = { ...format._options };
29223
29473
  this.isQuickTime = format instanceof MovOutputFormat;
29224
29474
  this.isCmaf = format instanceof CmafOutputFormat;
29225
- this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
29475
+ this.minimumFragmentDuration = this.formatOptions.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
29226
29476
  this.auxWriter.start();
29227
29477
  }
29228
29478
  async start() {
29229
29479
  const release = await this.mutex.acquire();
29230
29480
  if (!this.isCmaf) {
29231
- this.writer = await this.output._getRootWriter((target) => this.format._options.fastStart !== void 0 ? this.format._options.fastStart === "fragmented" : target instanceof BufferTarget);
29481
+ this.writer = await this.output._getRootWriter((target) => this.formatOptions.fastStart !== void 0 ? this.formatOptions.fastStart === "fragmented" : target instanceof BufferTarget);
29232
29482
  this.boxWriter = new IsobmffBoxWriter(this.writer);
29233
- this.fastStart = this.format._options.fastStart ?? (this.writer.target instanceof BufferTarget ? "in-memory" : false);
29483
+ this.fastStart = this.formatOptions.fastStart ?? (this.writer.target instanceof BufferTarget ? "in-memory" : false);
29234
29484
  this.isFragmented = this.fastStart === "fragmented";
29235
29485
  } else {
29236
29486
  this.fastStart = "fragmented";
@@ -29252,7 +29502,7 @@ var Mediabunny = (() => {
29252
29502
  {
29253
29503
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
29254
29504
  assert(boxWriter);
29255
- if (this.format._options.onFtyp) {
29505
+ if (this.formatOptions.onFtyp) {
29256
29506
  boxWriter.writer.startTrackingWrites();
29257
29507
  }
29258
29508
  boxWriter.writeBox(ftyp({
@@ -29261,9 +29511,9 @@ var Mediabunny = (() => {
29261
29511
  fragmented: this.isFragmented,
29262
29512
  cmaf: this.isCmaf
29263
29513
  }));
29264
- if (this.format._options.onFtyp) {
29514
+ if (this.formatOptions.onFtyp) {
29265
29515
  const { data, start } = boxWriter.writer.stopTrackingWrites();
29266
- this.format._options.onFtyp(data, start);
29516
+ this.formatOptions.onFtyp(data, start);
29267
29517
  }
29268
29518
  this.ftypSize = boxWriter.writer.getPos();
29269
29519
  if (this.isCmaf) {
@@ -29283,13 +29533,28 @@ var Mediabunny = (() => {
29283
29533
  } else {
29284
29534
  assert(this.writer);
29285
29535
  assert(this.boxWriter);
29286
- if (this.format._options.onMdat) {
29536
+ if (this.formatOptions.onMdat) {
29287
29537
  this.writer.startTrackingWrites();
29288
29538
  }
29289
29539
  this.mdat = mdat(true);
29290
29540
  this.boxWriter.writeBox(this.mdat);
29291
29541
  }
29292
29542
  await this.writer?.flush();
29543
+ for (const track of this.output.tracks) {
29544
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
29545
+ this.getVideoTrackData(
29546
+ track,
29547
+ track.metadata.primingPacket ?? null,
29548
+ { decoderConfig: track.metadata.decoderConfig }
29549
+ );
29550
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
29551
+ this.getAudioTrackData(
29552
+ track,
29553
+ track.metadata.primingPacket ?? null,
29554
+ { decoderConfig: track.metadata.decoderConfig }
29555
+ );
29556
+ }
29557
+ }
29293
29558
  release();
29294
29559
  }
29295
29560
  allTracksAreKnown() {
@@ -29326,7 +29591,7 @@ var Mediabunny = (() => {
29326
29591
  if (existingTrackData) {
29327
29592
  return existingTrackData;
29328
29593
  }
29329
- validateVideoChunkMetadata(meta);
29594
+ validateVideoChunkMetadata(meta, track.source._codec);
29330
29595
  assert(meta);
29331
29596
  assert(meta.decoderConfig);
29332
29597
  const decoderConfig = { ...meta.decoderConfig };
@@ -29334,6 +29599,9 @@ var Mediabunny = (() => {
29334
29599
  assert(decoderConfig.codedHeight !== void 0);
29335
29600
  let requiresAnnexBTransformation = false;
29336
29601
  if (track.source._codec === "avc" && !decoderConfig.description) {
29602
+ if (!packet) {
29603
+ throw new Error("No AVC description provided; you must therefore provide a priming packet.");
29604
+ }
29337
29605
  const decoderConfigurationRecord = extractAvcDecoderConfigurationRecord(packet.data);
29338
29606
  if (!decoderConfigurationRecord) {
29339
29607
  throw new Error(
@@ -29343,6 +29611,9 @@ var Mediabunny = (() => {
29343
29611
  decoderConfig.description = serializeAvcDecoderConfigurationRecord(decoderConfigurationRecord);
29344
29612
  requiresAnnexBTransformation = true;
29345
29613
  } else if (track.source._codec === "hevc" && !decoderConfig.description) {
29614
+ if (!packet) {
29615
+ throw new Error("No HEVC description provided; you must therefore provide a priming packet.");
29616
+ }
29346
29617
  const decoderConfigurationRecord = extractHevcDecoderConfigurationRecord(packet.data);
29347
29618
  if (!decoderConfigurationRecord) {
29348
29619
  throw new Error(
@@ -29401,12 +29672,15 @@ var Mediabunny = (() => {
29401
29672
  if (existingTrackData) {
29402
29673
  return existingTrackData;
29403
29674
  }
29404
- validateAudioChunkMetadata(meta);
29675
+ validateAudioChunkMetadata(meta, track.source._codec);
29405
29676
  assert(meta);
29406
29677
  assert(meta.decoderConfig);
29407
29678
  const decoderConfig = { ...meta.decoderConfig };
29408
29679
  let requiresAdtsStripping = false;
29409
29680
  if (track.source._codec === "aac" && !decoderConfig.description) {
29681
+ if (!packet) {
29682
+ throw new Error("No AAC description provided; you must therefore provide a priming packet.");
29683
+ }
29410
29684
  const adtsFrame = readAdtsFrameHeader(FileSlice4.tempFromBytes(packet.data));
29411
29685
  if (!adtsFrame) {
29412
29686
  throw new Error(
@@ -29425,6 +29699,11 @@ var Mediabunny = (() => {
29425
29699
  });
29426
29700
  requiresAdtsStripping = true;
29427
29701
  }
29702
+ if (track.source._codec === "ac3" || track.source._codec === "eac3") {
29703
+ if (!packet) {
29704
+ throw new Error("AC-3/E-AC-3 require a priming packet.");
29705
+ }
29706
+ }
29428
29707
  const newTrackData = {
29429
29708
  muxer: this,
29430
29709
  track,
@@ -29436,7 +29715,7 @@ var Mediabunny = (() => {
29436
29715
  requiresPcmTransformation: !this.isFragmented && PCM_AUDIO_CODECS.includes(track.source._codec),
29437
29716
  expectedNextPcmPacketTimestamp: null,
29438
29717
  requiresAdtsStripping,
29439
- firstPacket: packet
29718
+ primingPacket: packet
29440
29719
  },
29441
29720
  timescale: decoderConfig.sampleRate,
29442
29721
  samples: [],
@@ -29865,7 +30144,8 @@ var Mediabunny = (() => {
29865
30144
  startTimestamp: sample.timestamp,
29866
30145
  samples: [],
29867
30146
  offset: null,
29868
- moofOffset: null
30147
+ moofOffset: null,
30148
+ trafIndex: null
29869
30149
  };
29870
30150
  }
29871
30151
  assert(trackData.currentChunk);
@@ -29932,19 +30212,19 @@ var Mediabunny = (() => {
29932
30212
  }
29933
30213
  async finalizeFragment(flushWriter = !this.isCmaf) {
29934
30214
  assert(this.isFragmented);
29935
- const fragmentNumber = this.nextFragmentNumber++;
29936
- if (fragmentNumber === 1) {
30215
+ if (!this.wroteFragmentedHeader) {
30216
+ this.wroteFragmentedHeader = true;
29937
30217
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
29938
30218
  assert(boxWriter);
29939
- if (this.format._options.onMoov) {
30219
+ if (this.formatOptions.onMoov) {
29940
30220
  boxWriter.writer.startTrackingWrites();
29941
30221
  }
29942
30222
  this.ensureOneEnabledTrack();
29943
30223
  const movieBox = moov(this);
29944
30224
  boxWriter.writeBox(movieBox);
29945
- if (this.format._options.onMoov) {
30225
+ if (this.formatOptions.onMoov) {
29946
30226
  const { data, start } = boxWriter.writer.stopTrackingWrites();
29947
- this.format._options.onMoov(data, start);
30227
+ this.formatOptions.onMoov(data, start);
29948
30228
  }
29949
30229
  if (this.isCmaf) {
29950
30230
  assert(this.initWriter);
@@ -29961,14 +30241,23 @@ var Mediabunny = (() => {
29961
30241
  assert(this.writer);
29962
30242
  assert(this.boxWriter);
29963
30243
  const tracksInFragment = this.trackDatas.filter((x) => x.currentChunk);
30244
+ if (tracksInFragment.length === 0) {
30245
+ if (flushWriter) {
30246
+ await this.writer.flush();
30247
+ }
30248
+ return;
30249
+ }
30250
+ const fragmentNumber = this.nextFragmentNumber++;
29964
30251
  const moofBox = moof(fragmentNumber, tracksInFragment);
29965
30252
  const moofOffset = this.writer.getPos();
29966
30253
  const mdatStartPos = moofOffset + this.boxWriter.measureBox(moofBox);
29967
30254
  let currentPos = mdatStartPos + MIN_BOX_HEADER_SIZE;
29968
30255
  let fragmentStartTimestamp = Infinity;
29969
- for (const trackData of tracksInFragment) {
30256
+ for (let i = 0; i < tracksInFragment.length; i++) {
30257
+ const trackData = tracksInFragment[i];
29970
30258
  trackData.currentChunk.offset = currentPos;
29971
30259
  trackData.currentChunk.moofOffset = moofOffset;
30260
+ trackData.currentChunk.trafIndex = i;
29972
30261
  for (const sample of trackData.currentChunk.samples) {
29973
30262
  currentPos += sample.size;
29974
30263
  }
@@ -29981,17 +30270,17 @@ var Mediabunny = (() => {
29981
30270
  trackData.currentChunk.offset += MAX_BOX_HEADER_SIZE - MIN_BOX_HEADER_SIZE;
29982
30271
  }
29983
30272
  }
29984
- if (this.format._options.onMoof) {
30273
+ if (this.formatOptions.onMoof) {
29985
30274
  this.writer.startTrackingWrites();
29986
30275
  }
29987
30276
  const newMoofBox = moof(fragmentNumber, tracksInFragment);
29988
30277
  this.boxWriter.writeBox(newMoofBox);
29989
- if (this.format._options.onMoof) {
30278
+ if (this.formatOptions.onMoof) {
29990
30279
  const { data, start } = this.writer.stopTrackingWrites();
29991
- this.format._options.onMoof(data, start, fragmentStartTimestamp);
30280
+ this.formatOptions.onMoof(data, start, fragmentStartTimestamp);
29992
30281
  }
29993
30282
  assert(this.writer.getPos() === mdatStartPos);
29994
- if (this.format._options.onMdat) {
30283
+ if (this.formatOptions.onMdat) {
29995
30284
  this.writer.startTrackingWrites();
29996
30285
  }
29997
30286
  const mdatBox = mdat(needsLargeMdatSize);
@@ -30004,9 +30293,9 @@ var Mediabunny = (() => {
30004
30293
  sample.data = null;
30005
30294
  }
30006
30295
  }
30007
- if (this.format._options.onMdat) {
30296
+ if (this.formatOptions.onMdat) {
30008
30297
  const { data, start } = this.writer.stopTrackingWrites();
30009
- this.format._options.onMdat(data, start);
30298
+ this.formatOptions.onMdat(data, start);
30010
30299
  }
30011
30300
  for (const trackData of tracksInFragment) {
30012
30301
  trackData.finalizedChunks.push(trackData.currentChunk);
@@ -30018,33 +30307,36 @@ var Mediabunny = (() => {
30018
30307
  }
30019
30308
  }
30020
30309
  async registerSampleFastStartReserve(trackData, sample) {
30021
- assert(this.writer);
30022
- assert(this.boxWriter);
30023
30310
  if (this.allTracksAreKnown()) {
30024
30311
  if (!this.mdat) {
30025
- this.ensureOneEnabledTrack();
30026
- const moovBox = moov(this);
30027
- const moovSize = this.boxWriter.measureBox(moovBox);
30028
- const reservedSize = moovSize + this.computeSampleTableSizeUpperBound() + 4096;
30029
- assert(this.ftypSize !== null);
30030
- this.writer.seek(this.ftypSize + reservedSize);
30031
- if (this.format._options.onMdat) {
30032
- this.writer.startTrackingWrites();
30033
- }
30034
- this.mdat = mdat(true);
30035
- this.boxWriter.writeBox(this.mdat);
30036
- for (const trackData2 of this.trackDatas) {
30037
- for (const sample2 of trackData2.sampleQueue) {
30038
- await this.addSampleToTrack(trackData2, sample2);
30039
- }
30040
- trackData2.sampleQueue.length = 0;
30041
- }
30312
+ await this.createFastStartReserveMdat();
30042
30313
  }
30043
30314
  await this.addSampleToTrack(trackData, sample);
30044
30315
  } else {
30045
30316
  trackData.sampleQueue.push(sample);
30046
30317
  }
30047
30318
  }
30319
+ async createFastStartReserveMdat() {
30320
+ assert(this.writer);
30321
+ assert(this.boxWriter);
30322
+ this.ensureOneEnabledTrack();
30323
+ const moovBox = moov(this);
30324
+ const moovSize = this.boxWriter.measureBox(moovBox);
30325
+ const reservedSize = moovSize + this.computeSampleTableSizeUpperBound() + 4096;
30326
+ assert(this.ftypSize !== null);
30327
+ this.writer.seek(this.ftypSize + reservedSize);
30328
+ if (this.formatOptions.onMdat) {
30329
+ this.writer.startTrackingWrites();
30330
+ }
30331
+ this.mdat = mdat(true);
30332
+ this.boxWriter.writeBox(this.mdat);
30333
+ for (const trackData of this.trackDatas) {
30334
+ for (const sample of trackData.sampleQueue) {
30335
+ await this.addSampleToTrack(trackData, sample);
30336
+ }
30337
+ trackData.sampleQueue.length = 0;
30338
+ }
30339
+ }
30048
30340
  computeSampleTableSizeUpperBound() {
30049
30341
  assert(this.fastStart === "reserve");
30050
30342
  let upperBound = 0;
@@ -30095,11 +30387,31 @@ var Mediabunny = (() => {
30095
30387
  }
30096
30388
  }
30097
30389
  }
30390
+ /** Internal function for external callers who want to full control fragment boundaries. */
30391
+ async forceFragmentFinalization() {
30392
+ assert(this.isFragmented);
30393
+ const release = await this.mutex.acquire();
30394
+ try {
30395
+ for (const trackData of this.trackDatas) {
30396
+ if (trackData.type === "subtitle" && trackData.track.source._codec === "webvtt") {
30397
+ await this.processWebVTTCues(trackData, Infinity);
30398
+ }
30399
+ this.processTimestamps(trackData);
30400
+ }
30401
+ await this.interleaveSamples(true);
30402
+ await this.finalizeFragment();
30403
+ } finally {
30404
+ release();
30405
+ }
30406
+ }
30098
30407
  /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
30099
30408
  async finalize() {
30100
30409
  const release = await this.mutex.acquire();
30101
30410
  this.allTracksKnown.resolve();
30102
30411
  this.ensureOneEnabledTrack();
30412
+ if (!this.mdat && this.fastStart === "reserve") {
30413
+ await this.createFastStartReserveMdat();
30414
+ }
30103
30415
  for (const trackData of this.trackDatas) {
30104
30416
  trackData.closed = true;
30105
30417
  if (trackData.type === "subtitle" && trackData.track.source._codec === "webvtt") {
@@ -30113,11 +30425,12 @@ var Mediabunny = (() => {
30113
30425
  } else {
30114
30426
  for (const trackData of this.trackDatas) {
30115
30427
  await this.finalizeCurrentChunk(trackData);
30116
- assert(trackData.startTimestampOffset !== null);
30117
- for (let i = 0; i < trackData.samples.length; i++) {
30118
- const sample = trackData.samples[i];
30119
- sample.timestamp -= trackData.startTimestampOffset;
30120
- sample.decodeTimestamp -= trackData.startTimestampOffset;
30428
+ if (trackData.startTimestampOffset !== null) {
30429
+ for (let i = 0; i < trackData.samples.length; i++) {
30430
+ const sample = trackData.samples[i];
30431
+ sample.timestamp -= trackData.startTimestampOffset;
30432
+ sample.decodeTimestamp -= trackData.startTimestampOffset;
30433
+ }
30121
30434
  }
30122
30435
  }
30123
30436
  }
@@ -30142,16 +30455,16 @@ var Mediabunny = (() => {
30142
30455
  if (currentChunkPos < 2 ** 32) break;
30143
30456
  if (mdatSize >= 2 ** 32) this.mdat.largeSize = true;
30144
30457
  }
30145
- if (this.format._options.onMoov) {
30458
+ if (this.formatOptions.onMoov) {
30146
30459
  this.writer.startTrackingWrites();
30147
30460
  }
30148
30461
  const movieBox = moov(this);
30149
30462
  this.boxWriter.writeBox(movieBox);
30150
- if (this.format._options.onMoov) {
30463
+ if (this.formatOptions.onMoov) {
30151
30464
  const { data, start } = this.writer.stopTrackingWrites();
30152
- this.format._options.onMoov(data, start);
30465
+ this.formatOptions.onMoov(data, start);
30153
30466
  }
30154
- if (this.format._options.onMdat) {
30467
+ if (this.formatOptions.onMdat) {
30155
30468
  this.writer.startTrackingWrites();
30156
30469
  }
30157
30470
  this.mdat.size = mdatSize;
@@ -30163,9 +30476,9 @@ var Mediabunny = (() => {
30163
30476
  sample.data = null;
30164
30477
  }
30165
30478
  }
30166
- if (this.format._options.onMdat) {
30479
+ if (this.formatOptions.onMdat) {
30167
30480
  const { data, start } = this.writer.stopTrackingWrites();
30168
- this.format._options.onMdat(data, start);
30481
+ this.formatOptions.onMdat(data, start);
30169
30482
  }
30170
30483
  } else if (this.isFragmented) {
30171
30484
  if (this.isCmaf) {
@@ -30189,29 +30502,29 @@ var Mediabunny = (() => {
30189
30502
  this.mdat.size = mdatSize;
30190
30503
  this.mdat.largeSize = mdatSize >= 2 ** 32;
30191
30504
  this.boxWriter.patchBox(this.mdat);
30192
- if (this.format._options.onMdat) {
30505
+ if (this.formatOptions.onMdat) {
30193
30506
  const { data, start } = this.writer.stopTrackingWrites();
30194
- this.format._options.onMdat(data, start);
30507
+ this.formatOptions.onMdat(data, start);
30195
30508
  }
30196
30509
  const movieBox = moov(this);
30197
30510
  if (this.fastStart === "reserve") {
30198
30511
  assert(this.ftypSize !== null);
30199
30512
  this.writer.seek(this.ftypSize);
30200
- if (this.format._options.onMoov) {
30513
+ if (this.formatOptions.onMoov) {
30201
30514
  this.writer.startTrackingWrites();
30202
30515
  }
30203
30516
  this.boxWriter.writeBox(movieBox);
30204
30517
  const remainingSpace = this.boxWriter.offsets.get(this.mdat) - this.writer.getPos();
30205
30518
  this.boxWriter.writeBox(free(remainingSpace));
30206
30519
  } else {
30207
- if (this.format._options.onMoov) {
30520
+ if (this.formatOptions.onMoov) {
30208
30521
  this.writer.startTrackingWrites();
30209
30522
  }
30210
30523
  this.boxWriter.writeBox(movieBox);
30211
30524
  }
30212
- if (this.format._options.onMoov) {
30525
+ if (this.formatOptions.onMoov) {
30213
30526
  const { data, start } = this.writer.stopTrackingWrites();
30214
- this.format._options.onMoov(data, start);
30527
+ this.formatOptions.onMoov(data, start);
30215
30528
  }
30216
30529
  }
30217
30530
  release();
@@ -30258,6 +30571,21 @@ var Mediabunny = (() => {
30258
30571
  this.createSegmentInfo();
30259
30572
  this.createCues();
30260
30573
  await this.writer.flush();
30574
+ for (const track of this.output.tracks) {
30575
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
30576
+ this.getVideoTrackData(
30577
+ track,
30578
+ track.metadata.primingPacket ?? null,
30579
+ { decoderConfig: track.metadata.decoderConfig }
30580
+ );
30581
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
30582
+ this.getAudioTrackData(
30583
+ track,
30584
+ track.metadata.primingPacket ?? null,
30585
+ { decoderConfig: track.metadata.decoderConfig }
30586
+ );
30587
+ }
30588
+ }
30261
30589
  release();
30262
30590
  }
30263
30591
  writeEBMLHeader() {
@@ -30722,7 +31050,7 @@ var Mediabunny = (() => {
30722
31050
  if (existingTrackData) {
30723
31051
  return existingTrackData;
30724
31052
  }
30725
- validateVideoChunkMetadata(meta);
31053
+ validateVideoChunkMetadata(meta, track.source._codec);
30726
31054
  assert(meta);
30727
31055
  assert(meta.decoderConfig);
30728
31056
  assert(meta.decoderConfig.codedWidth !== void 0);
@@ -30741,8 +31069,7 @@ var Mediabunny = (() => {
30741
31069
  height: meta.decoderConfig.codedHeight,
30742
31070
  aspectRatio,
30743
31071
  decoderConfig: meta.decoderConfig,
30744
- alphaMode: !!packet.sideData.alpha
30745
- // The first packet determines if this track has alpha or not
31072
+ alphaMode: packet ? !!packet.sideData.alpha : null
30746
31073
  },
30747
31074
  chunkQueue: [],
30748
31075
  lastWrittenMsTimestamp: null,
@@ -30772,12 +31099,15 @@ var Mediabunny = (() => {
30772
31099
  if (existingTrackData) {
30773
31100
  return existingTrackData;
30774
31101
  }
30775
- validateAudioChunkMetadata(meta);
31102
+ validateAudioChunkMetadata(meta, track.source._codec);
30776
31103
  assert(meta);
30777
31104
  assert(meta.decoderConfig);
30778
31105
  const decoderConfig = { ...meta.decoderConfig };
30779
31106
  let requiresAdtsStripping = false;
30780
31107
  if (track.source._codec === "aac" && !decoderConfig.description) {
31108
+ if (!packet) {
31109
+ throw new Error("No AAC description provided; you must therefore provide a priming packet.");
31110
+ }
30781
31111
  const adtsFrame = readAdtsFrameHeader(FileSlice4.tempFromBytes(packet.data));
30782
31112
  if (!adtsFrame) {
30783
31113
  throw new Error(
@@ -30847,6 +31177,7 @@ var Mediabunny = (() => {
30847
31177
  const release = await this.mutex.acquire();
30848
31178
  try {
30849
31179
  const trackData = this.getVideoTrackData(track, packet, meta);
31180
+ trackData.info.alphaMode ??= !!packet.sideData.alpha;
30850
31181
  let packetData = packet.data;
30851
31182
  if (track.source._codec === "prores") {
30852
31183
  if (packetData.byteLength < 8) {
@@ -31237,7 +31568,7 @@ ${cue.notes ?? ""}`;
31237
31568
  data.sampleRate,
31238
31569
  padding
31239
31570
  );
31240
- this.writer.seek(startPos + frameSize);
31571
+ this.writer.write(new Uint8Array(startPos + frameSize - this.writer.getPos()));
31241
31572
  }
31242
31573
  };
31243
31574
 
@@ -31322,22 +31653,95 @@ ${cue.notes ?? ""}`;
31322
31653
  throw new Error("MP3 does not support subtitles.");
31323
31654
  }
31324
31655
  async finalize() {
31325
- if (!this.xingFrameData || this.xingFramePos === null) {
31326
- return;
31327
- }
31328
31656
  const release = await this.mutex.acquire();
31657
+ if (!this.xingFrameData && this.format._options.xingHeader === false) {
31658
+ throw new Error(
31659
+ "Cannot finalize an empty MP3 file: not a single packet was added and the Xing header is disabled, so there's no frame we could write."
31660
+ );
31661
+ }
31662
+ if (!this.xingFrameData) {
31663
+ const track = this.output.tracks[0];
31664
+ assert(track?.isAudioTrack());
31665
+ const primingPacket = track.metadata.primingPacket;
31666
+ if (primingPacket) {
31667
+ const view2 = toDataView(primingPacket.data);
31668
+ if (view2.byteLength < 4) {
31669
+ throw new Error("Invalid MP3 header in priming packet.");
31670
+ }
31671
+ const word = view2.getUint32(0, false);
31672
+ const header = readMp3FrameHeader(word, null).header;
31673
+ if (!header) {
31674
+ throw new Error("Invalid MP3 header in priming packet.");
31675
+ }
31676
+ this.xingFrameData = {
31677
+ mpegVersionId: header.mpegVersionId,
31678
+ layer: header.layer,
31679
+ frequencyIndex: header.frequencyIndex,
31680
+ sampleRate: header.sampleRate,
31681
+ channel: header.channel,
31682
+ modeExtension: header.modeExtension,
31683
+ copyright: header.copyright,
31684
+ original: header.original,
31685
+ emphasis: header.emphasis,
31686
+ frameCount: null,
31687
+ fileSize: null,
31688
+ toc: null
31689
+ };
31690
+ } else if (track.metadata.decoderConfig) {
31691
+ const { sampleRate, numberOfChannels } = track.metadata.decoderConfig;
31692
+ const mpegVersionIds = [3, 2, 0];
31693
+ let mpegVersionId = null;
31694
+ let frequencyIndex = -1;
31695
+ for (let i = 0; i < mpegVersionIds.length; i++) {
31696
+ frequencyIndex = SAMPLING_RATES.indexOf(sampleRate << i);
31697
+ if (frequencyIndex !== -1) {
31698
+ mpegVersionId = mpegVersionIds[i];
31699
+ break;
31700
+ }
31701
+ }
31702
+ if (mpegVersionId === null) {
31703
+ throw new Error(`${sampleRate} Hz is not a valid MP3 sample rate.`);
31704
+ }
31705
+ this.xingFrameData = {
31706
+ mpegVersionId,
31707
+ layer: 1,
31708
+ // Layer III
31709
+ frequencyIndex,
31710
+ sampleRate,
31711
+ channel: numberOfChannels === 1 ? 3 : 0,
31712
+ // 3 = single channel, 0 = stereo
31713
+ modeExtension: 0,
31714
+ copyright: 0,
31715
+ original: 0,
31716
+ emphasis: 0,
31717
+ frameCount: null,
31718
+ fileSize: null,
31719
+ toc: null
31720
+ };
31721
+ } else {
31722
+ throw new Error(
31723
+ "Cannot finalize an empty MP3 file: no packets were added and the track specified neither a decoderConfig nor a primingPacket in its metadata, so there's no telling what the file should look like."
31724
+ );
31725
+ }
31726
+ this.xingFramePos = this.writer.getPos();
31727
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
31728
+ this.frameCount++;
31729
+ }
31730
+ assert(this.xingFramePos !== null);
31329
31731
  const endPos = this.writer.getPos();
31330
31732
  const audioDataEndPos = endPos - this.xingFramePos;
31331
31733
  this.writer.seek(this.xingFramePos);
31332
- const toc = new Uint8Array(100);
31333
- for (let i = 0; i < 100; i++) {
31334
- const index = Math.floor(this.framePositions.length * (i / 100));
31335
- const byteOffset = this.framePositions[index] - this.xingFramePos;
31336
- toc[i] = 256 * (byteOffset / audioDataEndPos);
31734
+ if (this.framePositions.length > 0) {
31735
+ const toc = new Uint8Array(100);
31736
+ for (let i = 0; i < 100; i++) {
31737
+ const index = Math.floor(this.framePositions.length * (i / 100));
31738
+ const byteOffset = this.framePositions[index] - this.xingFramePos;
31739
+ toc[i] = 256 * (byteOffset / audioDataEndPos);
31740
+ }
31741
+ this.xingFrameData.toc = toc;
31337
31742
  }
31338
31743
  this.xingFrameData.frameCount = this.frameCount;
31339
31744
  this.xingFrameData.fileSize = audioDataEndPos;
31340
- this.xingFrameData.toc = toc;
31341
31745
  if (this.format._options.onXingFrame) {
31342
31746
  this.writer.startTrackingWrites();
31343
31747
  }
@@ -31365,6 +31769,12 @@ ${cue.notes ?? ""}`;
31365
31769
  async start() {
31366
31770
  const release = await this.mutex.acquire();
31367
31771
  this.writer = await this.output._getRootWriter(true);
31772
+ for (const track of this.output.tracks) {
31773
+ assert(track.isAudioTrack());
31774
+ if (track.metadata.decoderConfig) {
31775
+ this.getTrackData(track, { decoderConfig: track.metadata.decoderConfig });
31776
+ }
31777
+ }
31368
31778
  release();
31369
31779
  }
31370
31780
  async getMimeType() {
@@ -31386,7 +31796,7 @@ ${cue.notes ?? ""}`;
31386
31796
  serialNumber = Math.floor(2 ** 32 * Math.random());
31387
31797
  } while (this.trackDatas.some((td) => td.serialNumber === serialNumber));
31388
31798
  assert(track.source._codec === "vorbis" || track.source._codec === "opus");
31389
- validateAudioChunkMetadata(meta);
31799
+ validateAudioChunkMetadata(meta, track.source._codec);
31390
31800
  assert(meta);
31391
31801
  assert(meta.decoderConfig);
31392
31802
  const newTrackData = {
@@ -31738,7 +32148,7 @@ ${cue.notes ?? ""}`;
31738
32148
  if (existingTrackData) {
31739
32149
  return existingTrackData;
31740
32150
  }
31741
- validateVideoChunkMetadata(meta);
32151
+ validateVideoChunkMetadata(meta, track.source._codec);
31742
32152
  assert(meta?.decoderConfig);
31743
32153
  const codec = track.source._codec;
31744
32154
  assert(codec === "avc" || codec === "hevc");
@@ -31773,7 +32183,7 @@ ${cue.notes ?? ""}`;
31773
32183
  if (existingTrackData) {
31774
32184
  return existingTrackData;
31775
32185
  }
31776
- validateAudioChunkMetadata(meta);
32186
+ validateAudioChunkMetadata(meta, track.source._codec);
31777
32187
  assert(meta?.decoderConfig);
31778
32188
  const codec = track.source._codec;
31779
32189
  assert(codec === "aac" || codec === "mp3" || codec === "ac3" || codec === "eac3");
@@ -32325,6 +32735,14 @@ ${cue.notes ?? ""}`;
32325
32735
  const release = await this.mutex.acquire();
32326
32736
  this.writer = await this.output._getRootWriter(false);
32327
32737
  this.riffWriter = new RiffWriter(this.writer);
32738
+ const track = this.output.tracks[0];
32739
+ assert(track?.isAudioTrack());
32740
+ if (track.metadata.decoderConfig) {
32741
+ validateAudioChunkMetadata({ decoderConfig: track.metadata.decoderConfig }, track.source._codec);
32742
+ this.writeHeader(track, track.metadata.decoderConfig);
32743
+ this.sampleRate = track.metadata.decoderConfig.sampleRate;
32744
+ this.headerWritten = true;
32745
+ }
32328
32746
  release();
32329
32747
  }
32330
32748
  async getMimeType() {
@@ -32337,7 +32755,7 @@ ${cue.notes ?? ""}`;
32337
32755
  const release = await this.mutex.acquire();
32338
32756
  try {
32339
32757
  if (!this.headerWritten) {
32340
- validateAudioChunkMetadata(meta);
32758
+ validateAudioChunkMetadata(meta, track.source._codec);
32341
32759
  assert(meta);
32342
32760
  assert(meta.decoderConfig);
32343
32761
  this.writeHeader(track, meta.decoderConfig);
@@ -32561,6 +32979,11 @@ ${cue.notes ?? ""}`;
32561
32979
  }
32562
32980
  async finalize() {
32563
32981
  const release = await this.mutex.acquire();
32982
+ if (!this.headerWritten) {
32983
+ throw new Error(
32984
+ "Cannot finalize an empty WAVE file: no packets were added and the track specified no decoderConfig in its metadata, so there's no telling what the file should look like."
32985
+ );
32986
+ }
32564
32987
  const endPos = this.writer.getPos();
32565
32988
  if (this.isRf64) {
32566
32989
  assert(this.ds64RiffSizePos !== null);
@@ -33235,10 +33658,13 @@ ${cue.notes ?? ""}`;
33235
33658
  `The dimensions ${candidateConfig.width}x${candidateConfig.height} are not supported for codec '${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to round your dimensions to the nearest even number.`
33236
33659
  );
33237
33660
  }
33238
- const support = await VideoEncoder.isConfigSupported(candidateConfig);
33239
- if (support.supported) {
33240
- selected = candidate;
33241
- break;
33661
+ try {
33662
+ const support = await VideoEncoder.isConfigSupported(candidateConfig);
33663
+ if (support.supported) {
33664
+ selected = candidate;
33665
+ break;
33666
+ }
33667
+ } catch {
33242
33668
  }
33243
33669
  }
33244
33670
  if (!selected) {
@@ -34294,8 +34720,14 @@ ${cue.notes ?? ""}`;
34294
34720
  if (typeof AudioEncoder === "undefined") {
34295
34721
  throw new Error("AudioEncoder is not supported by this browser.");
34296
34722
  }
34297
- const support = await AudioEncoder.isConfigSupported(encoderConfig);
34298
- if (!support.supported) {
34723
+ let supported;
34724
+ try {
34725
+ const support = await AudioEncoder.isConfigSupported(encoderConfig);
34726
+ supported = support.supported ?? false;
34727
+ } catch {
34728
+ supported = false;
34729
+ }
34730
+ if (!supported) {
34299
34731
  throw new Error(
34300
34732
  `This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps, ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not supported by this browser. Consider using another codec or changing your audio parameters.`
34301
34733
  );
@@ -35177,6 +35609,21 @@ ${cue.notes ?? ""}`;
35177
35609
  references: variant.linkedGroup ? this.playlistDeclarations.filter((x) => x.groupId === variant.linkedGroup.name) : []
35178
35610
  });
35179
35611
  }
35612
+ for (const track of this.output.tracks) {
35613
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
35614
+ this.getVideoTrackData(
35615
+ track,
35616
+ track.metadata.primingPacket ?? null,
35617
+ { decoderConfig: track.metadata.decoderConfig }
35618
+ );
35619
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
35620
+ this.getAudioTrackData(
35621
+ track,
35622
+ track.metadata.primingPacket ?? null,
35623
+ { decoderConfig: track.metadata.decoderConfig }
35624
+ );
35625
+ }
35626
+ }
35180
35627
  release();
35181
35628
  }
35182
35629
  async getMimeType() {
@@ -35205,12 +35652,12 @@ ${cue.notes ?? ""}`;
35205
35652
  release();
35206
35653
  }
35207
35654
  }
35208
- getVideoTrackData(track, meta) {
35655
+ getVideoTrackData(track, packet, meta) {
35209
35656
  let trackData = this.trackDatas.find((x) => x.track === track);
35210
35657
  if (trackData) {
35211
35658
  return trackData;
35212
35659
  }
35213
- validateVideoChunkMetadata(meta);
35660
+ validateVideoChunkMetadata(meta, track.source._codec);
35214
35661
  assert(meta);
35215
35662
  assert(meta?.decoderConfig);
35216
35663
  const playlists = this.playlists.filter((x) => x.tracks.includes(track));
@@ -35222,18 +35669,19 @@ ${cue.notes ?? ""}`;
35222
35669
  closed: false,
35223
35670
  info: {
35224
35671
  type: "video",
35225
- decoderConfig: meta.decoderConfig
35672
+ decoderConfig: meta.decoderConfig,
35673
+ primingPacket: packet
35226
35674
  }
35227
35675
  };
35228
35676
  this.trackDatas.push(trackData);
35229
35677
  return trackData;
35230
35678
  }
35231
- getAudioTrackData(track, meta) {
35679
+ getAudioTrackData(track, packet, meta) {
35232
35680
  let trackData = this.trackDatas.find((x) => x.track === track);
35233
35681
  if (trackData) {
35234
35682
  return trackData;
35235
35683
  }
35236
- validateAudioChunkMetadata(meta);
35684
+ validateAudioChunkMetadata(meta, track.source._codec);
35237
35685
  assert(meta);
35238
35686
  assert(meta?.decoderConfig);
35239
35687
  const playlists = this.playlists.filter((x) => x.tracks.includes(track));
@@ -35245,14 +35693,15 @@ ${cue.notes ?? ""}`;
35245
35693
  closed: false,
35246
35694
  info: {
35247
35695
  type: "audio",
35248
- decoderConfig: meta.decoderConfig
35696
+ decoderConfig: meta.decoderConfig,
35697
+ primingPacket: packet
35249
35698
  }
35250
35699
  };
35251
35700
  this.trackDatas.push(trackData);
35252
35701
  return trackData;
35253
35702
  }
35254
35703
  async addEncodedVideoPacket(track, packet, meta) {
35255
- const trackData = this.getVideoTrackData(track, meta);
35704
+ const trackData = this.getVideoTrackData(track, packet, meta);
35256
35705
  const playlist = trackData.playlist;
35257
35706
  const release = await playlist.mutex.acquire();
35258
35707
  try {
@@ -35272,7 +35721,7 @@ ${cue.notes ?? ""}`;
35272
35721
  }
35273
35722
  }
35274
35723
  async addEncodedAudioPacket(track, packet, meta) {
35275
- const trackData = this.getAudioTrackData(track, meta);
35724
+ const trackData = this.getAudioTrackData(track, packet, meta);
35276
35725
  const playlist = trackData.playlist;
35277
35726
  const release = await playlist.mutex.acquire();
35278
35727
  try {
@@ -35299,11 +35748,13 @@ ${cue.notes ?? ""}`;
35299
35748
  if (!this.allTracksAreKnown(playlist)) {
35300
35749
  return;
35301
35750
  }
35751
+ const trackDatas = this.trackDatas.filter((x) => playlist.tracks.includes(x.track));
35302
35752
  if (playlist.currentSegmentStartTimestamp === null) {
35303
- await this.onPlaylistDone(playlist);
35753
+ if (trackDatas.every((x) => x.closed)) {
35754
+ await this.onPlaylistDone(playlist);
35755
+ }
35304
35756
  return;
35305
35757
  }
35306
- const trackDatas = this.trackDatas.filter((x) => playlist.tracks.includes(x.track));
35307
35758
  const videoTrack = trackDatas.find((x) => x.info.type === "video");
35308
35759
  const audioTrack = trackDatas.find((x) => x.info.type === "audio");
35309
35760
  while (true) {
@@ -35422,12 +35873,68 @@ ${cue.notes ?? ""}`;
35422
35873
  isRoot: false,
35423
35874
  mimeType: playlist.segmentFormat.mimeType
35424
35875
  });
35425
- target._start();
35876
+ let fragmentedIsobmffOutput = null;
35877
+ if (playlist.segmentFormat._isFragmentedIsobmff()) {
35878
+ fragmentedIsobmffOutput = {
35879
+ output: new Output({
35880
+ format: playlist.segmentFormat,
35881
+ target
35882
+ }),
35883
+ videoSource: null,
35884
+ audioSource: null,
35885
+ firstMoofPosition: null,
35886
+ currentFileSize: 0
35887
+ };
35888
+ target.on("write", ({ end }) => {
35889
+ fragmentedIsobmffOutput.currentFileSize = Math.max(
35890
+ fragmentedIsobmffOutput.currentFileSize,
35891
+ end
35892
+ );
35893
+ });
35894
+ const muxer = fragmentedIsobmffOutput.output._muxer;
35895
+ muxer.minimumFragmentDuration = Infinity;
35896
+ const originalOnMoof = muxer.formatOptions.onMoof;
35897
+ muxer.formatOptions.onMoof = (data, position, timestamp) => {
35898
+ fragmentedIsobmffOutput.firstMoofPosition = position;
35899
+ originalOnMoof?.(data, position, timestamp);
35900
+ muxer.formatOptions.onMoof = originalOnMoof;
35901
+ };
35902
+ if (videoTrack) {
35903
+ fragmentedIsobmffOutput.videoSource = new EncodedVideoPacketSource(
35904
+ videoTrack.track.source._codec
35905
+ );
35906
+ fragmentedIsobmffOutput.output.addVideoTrack(
35907
+ fragmentedIsobmffOutput.videoSource,
35908
+ {
35909
+ ...videoTrack.track.metadata,
35910
+ decoderConfig: videoTrack.info.decoderConfig,
35911
+ primingPacket: videoTrack.info.primingPacket ?? void 0
35912
+ }
35913
+ );
35914
+ }
35915
+ if (audioTrack) {
35916
+ fragmentedIsobmffOutput.audioSource = new EncodedAudioPacketSource(
35917
+ audioTrack.track.source._codec
35918
+ );
35919
+ fragmentedIsobmffOutput.output.addAudioTrack(
35920
+ fragmentedIsobmffOutput.audioSource,
35921
+ {
35922
+ ...audioTrack.track.metadata,
35923
+ decoderConfig: audioTrack.info.decoderConfig,
35924
+ primingPacket: audioTrack.info.primingPacket ?? void 0
35925
+ }
35926
+ );
35927
+ }
35928
+ await fragmentedIsobmffOutput.output.start();
35929
+ } else {
35930
+ target._start();
35931
+ }
35426
35932
  playlist.singleFile = {
35427
35933
  target,
35428
35934
  path: relativeSegmentPath,
35429
35935
  nextOffset: 0,
35430
- info: segmentInfo2
35936
+ info: segmentInfo2,
35937
+ fragmentedIsobmffOutput
35431
35938
  };
35432
35939
  } else {
35433
35940
  relativeSegmentPath = playlist.singleFile.path;
@@ -35450,95 +35957,114 @@ ${cue.notes ?? ""}`;
35450
35957
  }
35451
35958
  let segmentSize = 0;
35452
35959
  let outputTarget = null;
35453
- const output = new Output({
35454
- format: playlist.segmentFormat,
35455
- target: new PathedTarget(
35456
- fullSegmentPath,
35457
- async (request) => {
35458
- const proxiedRequest = {
35459
- ...request,
35460
- isRoot: false
35461
- };
35462
- if (request.isRoot) {
35960
+ let maxEndTimestamp = -Infinity;
35961
+ let output = null;
35962
+ let videoSource = null;
35963
+ let audioSource = null;
35964
+ try {
35965
+ if (playlist.singleFile?.fragmentedIsobmffOutput) {
35966
+ output = playlist.singleFile.fragmentedIsobmffOutput.output;
35967
+ videoSource = playlist.singleFile.fragmentedIsobmffOutput.videoSource;
35968
+ audioSource = playlist.singleFile.fragmentedIsobmffOutput.audioSource;
35969
+ } else {
35970
+ output = new Output({
35971
+ format: playlist.segmentFormat,
35972
+ target: new PathedTarget(
35973
+ fullSegmentPath,
35974
+ async (request) => {
35975
+ const proxiedRequest = {
35976
+ ...request,
35977
+ isRoot: false
35978
+ };
35979
+ if (request.isRoot) {
35980
+ if (playlist.singleFile) {
35981
+ const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
35982
+ slice.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
35983
+ return slice;
35984
+ } else {
35985
+ const target = await this.output._getTarget(proxiedRequest);
35986
+ outputTarget = target;
35987
+ target.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
35988
+ return target;
35989
+ }
35990
+ }
35991
+ return this.output._getTarget(proxiedRequest);
35992
+ }
35993
+ ),
35994
+ initTarget: async () => {
35995
+ if (playlist.initSegment) {
35996
+ return new NullTarget();
35997
+ }
35463
35998
  if (playlist.singleFile) {
35999
+ playlist.initSegment = {
36000
+ path: playlist.singleFile.path,
36001
+ duration: 0,
36002
+ timestamp: 0,
36003
+ byteSize: 0,
36004
+ byteOffset: 0,
36005
+ info: null
36006
+ };
35464
36007
  const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
35465
- slice.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
36008
+ slice.on("write", ({ end }) => {
36009
+ playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
36010
+ });
36011
+ slice.on("finalized", () => {
36012
+ playlist.singleFile.nextOffset = playlist.initSegment.byteSize;
36013
+ });
35466
36014
  return slice;
35467
36015
  } else {
35468
- const target = await this.output._getTarget(proxiedRequest);
35469
- outputTarget = target;
35470
- target.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
36016
+ const playlistInfo = toPlaylistInfo(playlist);
36017
+ const initPath = await this.getInitPath(playlistInfo);
36018
+ validateInitPath(initPath);
36019
+ playlist.initSegment = {
36020
+ path: initPath,
36021
+ duration: 0,
36022
+ timestamp: 0,
36023
+ byteSize: 0,
36024
+ byteOffset: null,
36025
+ info: null
36026
+ };
36027
+ const fullInitPath = joinPaths(
36028
+ joinPaths(pathedTarget.rootPath, playlist.path),
36029
+ initPath
36030
+ );
36031
+ const target = await this.output._getTarget({
36032
+ path: fullInitPath,
36033
+ isRoot: false,
36034
+ mimeType: playlist.segmentFormat.mimeType
36035
+ });
36036
+ target.on("write", ({ end }) => {
36037
+ playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
36038
+ });
36039
+ target.on("finalized", () => {
36040
+ this.format._options.onInit?.(target, playlistInfo);
36041
+ });
35471
36042
  return target;
35472
36043
  }
35473
36044
  }
35474
- return this.output._getTarget(proxiedRequest);
35475
- }
35476
- ),
35477
- initTarget: async () => {
35478
- if (playlist.initSegment) {
35479
- return new NullTarget();
35480
- }
35481
- if (playlist.singleFile) {
35482
- playlist.initSegment = {
35483
- path: playlist.singleFile.path,
35484
- duration: 0,
35485
- timestamp: 0,
35486
- byteSize: 0,
35487
- byteOffset: 0,
35488
- info: null
35489
- };
35490
- const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
35491
- slice.on("write", ({ end }) => {
35492
- playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
35493
- });
35494
- slice.on("finalized", () => {
35495
- playlist.singleFile.nextOffset = playlist.initSegment.byteSize;
35496
- });
35497
- return slice;
35498
- } else {
35499
- const playlistInfo = toPlaylistInfo(playlist);
35500
- const initPath = await this.getInitPath(playlistInfo);
35501
- validateInitPath(initPath);
35502
- playlist.initSegment = {
35503
- path: initPath,
35504
- duration: 0,
35505
- timestamp: 0,
35506
- byteSize: 0,
35507
- byteOffset: null,
35508
- info: null
35509
- };
35510
- const fullInitPath = joinPaths(
35511
- joinPaths(pathedTarget.rootPath, playlist.path),
35512
- initPath
36045
+ });
36046
+ if (videoTrack) {
36047
+ videoSource = new EncodedVideoPacketSource(
36048
+ videoTrack.track.source._codec
35513
36049
  );
35514
- const target = await this.output._getTarget({
35515
- path: fullInitPath,
35516
- isRoot: false,
35517
- mimeType: playlist.segmentFormat.mimeType
35518
- });
35519
- target.on("write", ({ end }) => {
35520
- playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
36050
+ output.addVideoTrack(videoSource, {
36051
+ ...videoTrack.track.metadata,
36052
+ decoderConfig: videoTrack.info.decoderConfig,
36053
+ primingPacket: videoTrack.info.primingPacket ?? void 0
35521
36054
  });
35522
- target.on("finalized", () => {
35523
- this.format._options.onInit?.(target, playlistInfo);
36055
+ }
36056
+ if (audioTrack) {
36057
+ audioSource = new EncodedAudioPacketSource(
36058
+ audioTrack.track.source._codec
36059
+ );
36060
+ output.addAudioTrack(audioSource, {
36061
+ ...audioTrack.track.metadata,
36062
+ decoderConfig: audioTrack.info.decoderConfig,
36063
+ primingPacket: audioTrack.info.primingPacket ?? void 0
35524
36064
  });
35525
- return target;
35526
36065
  }
36066
+ await output.start();
35527
36067
  }
35528
- });
35529
- let maxEndTimestamp = -Infinity;
35530
- try {
35531
- let videoSource = null;
35532
- let audioSource = null;
35533
- if (videoTrack) {
35534
- videoSource = new EncodedVideoPacketSource(videoTrack.track.source._codec);
35535
- output.addVideoTrack(videoSource, videoTrack.track.metadata);
35536
- }
35537
- if (audioTrack) {
35538
- audioSource = new EncodedAudioPacketSource(audioTrack.track.source._codec);
35539
- output.addAudioTrack(audioSource, audioTrack.track.metadata);
35540
- }
35541
- await output.start();
35542
36068
  if (videoTrack) {
35543
36069
  assert(videoSource);
35544
36070
  const meta = { decoderConfig: videoTrack.info.decoderConfig };
@@ -35557,9 +36083,26 @@ ${cue.notes ?? ""}`;
35557
36083
  maxEndTimestamp = Math.max(maxEndTimestamp, packet.timestamp + packet.duration);
35558
36084
  }
35559
36085
  }
35560
- await output.finalize();
36086
+ if (playlist.singleFile?.fragmentedIsobmffOutput) {
36087
+ const muxer = playlist.singleFile.fragmentedIsobmffOutput.output._muxer;
36088
+ await muxer.forceFragmentFinalization();
36089
+ if (playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition !== null && !playlist.initSegment) {
36090
+ playlist.initSegment = {
36091
+ path: playlist.singleFile.path,
36092
+ duration: 0,
36093
+ timestamp: 0,
36094
+ byteSize: playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition,
36095
+ byteOffset: 0,
36096
+ info: null
36097
+ };
36098
+ playlist.singleFile.nextOffset = playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition;
36099
+ }
36100
+ segmentSize = playlist.singleFile.fragmentedIsobmffOutput.currentFileSize - playlist.singleFile.nextOffset;
36101
+ } else {
36102
+ await output.finalize();
36103
+ }
35561
36104
  } catch (e) {
35562
- await output.cancel();
36105
+ await output?.cancel();
35563
36106
  throw e;
35564
36107
  }
35565
36108
  if (segmentInfo) {
@@ -35617,8 +36160,12 @@ ${cue.notes ?? ""}`;
35617
36160
  assert(!playlist.done);
35618
36161
  playlist.done = true;
35619
36162
  if (playlist.singleFile) {
35620
- await playlist.singleFile.target._flush();
35621
- await playlist.singleFile.target._finalize();
36163
+ if (playlist.singleFile.fragmentedIsobmffOutput) {
36164
+ await playlist.singleFile.fragmentedIsobmffOutput.output.finalize();
36165
+ } else {
36166
+ await playlist.singleFile.target._flush();
36167
+ await playlist.singleFile.target._finalize();
36168
+ }
35622
36169
  this.format._options.onSegment?.(playlist.singleFile.target, playlist.singleFile.info);
35623
36170
  }
35624
36171
  await this.writePlaylist(playlist);
@@ -35938,6 +36485,10 @@ ${cue.notes ?? ""}`;
35938
36485
  _codecUnsupportedHint(codec) {
35939
36486
  return "";
35940
36487
  }
36488
+ /** @internal */
36489
+ _isFragmentedIsobmff() {
36490
+ return false;
36491
+ }
35941
36492
  };
35942
36493
  var IsobmffOutputFormat2 = class extends OutputFormat {
35943
36494
  /** Internal constructor. */
@@ -35979,7 +36530,7 @@ ${cue.notes ?? ""}`;
35979
36530
  video: { min: 0, max },
35980
36531
  audio: { min: 0, max },
35981
36532
  subtitle: { min: 0, max },
35982
- total: { min: 1, max }
36533
+ total: { min: 0, max }
35983
36534
  };
35984
36535
  }
35985
36536
  get supportsVideoRotationMetadata() {
@@ -35992,6 +36543,10 @@ ${cue.notes ?? ""}`;
35992
36543
  _createMuxer(output) {
35993
36544
  return new IsobmffMuxer2(output, this);
35994
36545
  }
36546
+ /** @internal */
36547
+ _isFragmentedIsobmff() {
36548
+ return this._options.fastStart === "fragmented";
36549
+ }
35995
36550
  };
35996
36551
  var Mp4OutputFormat = class extends IsobmffOutputFormat2 {
35997
36552
  /** Creates a new {@link Mp4OutputFormat} configured with the specified `options`. */
@@ -36135,7 +36690,7 @@ ${cue.notes ?? ""}`;
36135
36690
  video: { min: 0, max },
36136
36691
  audio: { min: 0, max },
36137
36692
  subtitle: { min: 0, max },
36138
- total: { min: 1, max }
36693
+ total: { min: 0, max }
36139
36694
  };
36140
36695
  }
36141
36696
  get fileExtension() {
@@ -36319,7 +36874,7 @@ ${cue.notes ?? ""}`;
36319
36874
  video: { min: 0, max: 0 },
36320
36875
  audio: { min: 0, max },
36321
36876
  subtitle: { min: 0, max: 0 },
36322
- total: { min: 1, max }
36877
+ total: { min: 0, max }
36323
36878
  };
36324
36879
  }
36325
36880
  get fileExtension() {
@@ -36456,7 +37011,7 @@ ${cue.notes ?? ""}`;
36456
37011
  video: { min: 0, max: maxVideo },
36457
37012
  audio: { min: 0, max: maxAudio },
36458
37013
  subtitle: { min: 0, max: 0 },
36459
- total: { min: 1, max: maxTotal }
37014
+ total: { min: 0, max: maxTotal }
36460
37015
  };
36461
37016
  }
36462
37017
  get fileExtension() {
@@ -36561,7 +37116,7 @@ ${cue.notes ?? ""}`;
36561
37116
  audio: { min: 0, max: supportsAudio ? Infinity : 0 },
36562
37117
  subtitle: { min: 0, max: 0 },
36563
37118
  // Currently disabled
36564
- total: { min: 1, max: Infinity }
37119
+ total: { min: 0, max: Infinity }
36565
37120
  };
36566
37121
  }
36567
37122
  get supportsVideoRotationMetadata() {
@@ -36892,6 +37447,17 @@ ${cue.notes ?? ""}`;
36892
37447
  `Invalid video frame rate: ${metadata.frameRate}. Must be a positive number.`
36893
37448
  );
36894
37449
  }
37450
+ if (metadata.decoderConfig !== void 0) {
37451
+ validateVideoChunkMetadata({ decoderConfig: metadata.decoderConfig }, source._codec);
37452
+ }
37453
+ if (metadata.primingPacket !== void 0) {
37454
+ if (!(metadata.primingPacket instanceof EncodedPacket)) {
37455
+ throw new TypeError("metadata.primingPacket, when provided, must be an EncodedPacket.");
37456
+ }
37457
+ if (metadata.decoderConfig === void 0) {
37458
+ throw new TypeError("metadata.primingPacket can only be provided alongside metadata.decoderConfig.");
37459
+ }
37460
+ }
36895
37461
  const metadataCopy = { ...metadata };
36896
37462
  metadataCopy.group ??= this.defaultTrackGroup;
36897
37463
  return this._addTrack(new OutputVideoTrack2(
@@ -36907,6 +37473,17 @@ ${cue.notes ?? ""}`;
36907
37473
  throw new TypeError("source must be an AudioSource.");
36908
37474
  }
36909
37475
  validateBaseTrackMetadata(metadata);
37476
+ if (metadata.decoderConfig !== void 0) {
37477
+ validateAudioChunkMetadata({ decoderConfig: metadata.decoderConfig }, source._codec);
37478
+ }
37479
+ if (metadata.primingPacket !== void 0) {
37480
+ if (!(metadata.primingPacket instanceof EncodedPacket)) {
37481
+ throw new TypeError("metadata.primingPacket, when provided, must be an EncodedPacket.");
37482
+ }
37483
+ if (metadata.decoderConfig === void 0) {
37484
+ throw new TypeError("metadata.primingPacket can only be provided alongside metadata.decoderConfig.");
37485
+ }
37486
+ }
36910
37487
  const metadataCopy = { ...metadata };
36911
37488
  metadataCopy.group ??= this.defaultTrackGroup;
36912
37489
  return this._addTrack(new OutputAudioTrack2(
@@ -37597,7 +38174,7 @@ ${cue.notes ?? ""}`;
37597
38174
  this.output.setMetadataTags(outputTags);
37598
38175
  }
37599
38176
  if (!this._composable) {
37600
- this.isValid = this.output.hasEnoughTracks();
38177
+ this.isValid = this.output.hasEnoughTracks() && this.output.tracks.length > 0;
37601
38178
  } else {
37602
38179
  this.isValid = true;
37603
38180
  }