mediabunny 1.52.3 → 1.53.0

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 (89) hide show
  1. package/dist/bundles/mediabunny.cjs +966 -396
  2. package/dist/bundles/mediabunny.min.cjs +9 -9
  3. package/dist/bundles/mediabunny.min.mjs +9 -9
  4. package/dist/bundles/mediabunny.mjs +966 -396
  5. package/dist/bundles/mediabunny.node.cjs +966 -396
  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/tsconfig.tsbuildinfo +1 -1
  62. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  63. package/dist/modules/src/wave/wave-muxer.js +15 -2
  64. package/package.json +1 -1
  65. package/src/adts/adts-muxer.ts +6 -1
  66. package/src/codec.ts +22 -2
  67. package/src/conversion.ts +2 -2
  68. package/src/encode.ts +14 -4
  69. package/src/flac/flac-muxer.ts +72 -39
  70. package/src/hls/hls-muxer.ts +261 -104
  71. package/src/input.ts +5 -2
  72. package/src/isobmff/isobmff-boxes.ts +8 -4
  73. package/src/isobmff/isobmff-demuxer.ts +70 -57
  74. package/src/isobmff/isobmff-muxer.ts +162 -76
  75. package/src/matroska/matroska-muxer.ts +29 -6
  76. package/src/media-sink.ts +3 -12
  77. package/src/media-source.ts +18 -6
  78. package/src/mp3/mp3-demuxer.ts +14 -0
  79. package/src/mp3/mp3-muxer.ts +105 -11
  80. package/src/mp3/mp3-writer.ts +3 -1
  81. package/src/mpeg-ts/mpeg-ts-demuxer.ts +5 -0
  82. package/src/mpeg-ts/mpeg-ts-muxer.ts +2 -2
  83. package/src/ogg/ogg-muxer.ts +8 -1
  84. package/src/output-format.ts +15 -5
  85. package/src/output.ts +48 -1
  86. package/src/sample.ts +160 -67
  87. package/src/segmented-input.ts +44 -8
  88. package/src/source.ts +208 -71
  89. 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
@@ -29213,6 +29454,7 @@ var Mediabunny = (() => {
29213
29454
  this.allTracksKnown = promiseWithResolvers();
29214
29455
  this.creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET;
29215
29456
  this.finalizedChunks = [];
29457
+ this.wroteFragmentedHeader = false;
29216
29458
  this.nextFragmentNumber = 1;
29217
29459
  // Only relevant for fragmented files, to make sure new fragments start with the highest timestamp seen so far
29218
29460
  this.maxWrittenTimestamp = -Infinity;
@@ -29220,17 +29462,18 @@ var Mediabunny = (() => {
29220
29462
  this.maxWrittenEndTimestamp = -Infinity;
29221
29463
  this.segmentHeaderSize = null;
29222
29464
  this.format = format;
29465
+ this.formatOptions = { ...format._options };
29223
29466
  this.isQuickTime = format instanceof MovOutputFormat;
29224
29467
  this.isCmaf = format instanceof CmafOutputFormat;
29225
- this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
29468
+ this.minimumFragmentDuration = this.formatOptions.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
29226
29469
  this.auxWriter.start();
29227
29470
  }
29228
29471
  async start() {
29229
29472
  const release = await this.mutex.acquire();
29230
29473
  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);
29474
+ this.writer = await this.output._getRootWriter((target) => this.formatOptions.fastStart !== void 0 ? this.formatOptions.fastStart === "fragmented" : target instanceof BufferTarget);
29232
29475
  this.boxWriter = new IsobmffBoxWriter(this.writer);
29233
- this.fastStart = this.format._options.fastStart ?? (this.writer.target instanceof BufferTarget ? "in-memory" : false);
29476
+ this.fastStart = this.formatOptions.fastStart ?? (this.writer.target instanceof BufferTarget ? "in-memory" : false);
29234
29477
  this.isFragmented = this.fastStart === "fragmented";
29235
29478
  } else {
29236
29479
  this.fastStart = "fragmented";
@@ -29252,7 +29495,7 @@ var Mediabunny = (() => {
29252
29495
  {
29253
29496
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
29254
29497
  assert(boxWriter);
29255
- if (this.format._options.onFtyp) {
29498
+ if (this.formatOptions.onFtyp) {
29256
29499
  boxWriter.writer.startTrackingWrites();
29257
29500
  }
29258
29501
  boxWriter.writeBox(ftyp({
@@ -29261,9 +29504,9 @@ var Mediabunny = (() => {
29261
29504
  fragmented: this.isFragmented,
29262
29505
  cmaf: this.isCmaf
29263
29506
  }));
29264
- if (this.format._options.onFtyp) {
29507
+ if (this.formatOptions.onFtyp) {
29265
29508
  const { data, start } = boxWriter.writer.stopTrackingWrites();
29266
- this.format._options.onFtyp(data, start);
29509
+ this.formatOptions.onFtyp(data, start);
29267
29510
  }
29268
29511
  this.ftypSize = boxWriter.writer.getPos();
29269
29512
  if (this.isCmaf) {
@@ -29283,13 +29526,28 @@ var Mediabunny = (() => {
29283
29526
  } else {
29284
29527
  assert(this.writer);
29285
29528
  assert(this.boxWriter);
29286
- if (this.format._options.onMdat) {
29529
+ if (this.formatOptions.onMdat) {
29287
29530
  this.writer.startTrackingWrites();
29288
29531
  }
29289
29532
  this.mdat = mdat(true);
29290
29533
  this.boxWriter.writeBox(this.mdat);
29291
29534
  }
29292
29535
  await this.writer?.flush();
29536
+ for (const track of this.output.tracks) {
29537
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
29538
+ this.getVideoTrackData(
29539
+ track,
29540
+ track.metadata.primingPacket ?? null,
29541
+ { decoderConfig: track.metadata.decoderConfig }
29542
+ );
29543
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
29544
+ this.getAudioTrackData(
29545
+ track,
29546
+ track.metadata.primingPacket ?? null,
29547
+ { decoderConfig: track.metadata.decoderConfig }
29548
+ );
29549
+ }
29550
+ }
29293
29551
  release();
29294
29552
  }
29295
29553
  allTracksAreKnown() {
@@ -29326,7 +29584,7 @@ var Mediabunny = (() => {
29326
29584
  if (existingTrackData) {
29327
29585
  return existingTrackData;
29328
29586
  }
29329
- validateVideoChunkMetadata(meta);
29587
+ validateVideoChunkMetadata(meta, track.source._codec);
29330
29588
  assert(meta);
29331
29589
  assert(meta.decoderConfig);
29332
29590
  const decoderConfig = { ...meta.decoderConfig };
@@ -29334,6 +29592,9 @@ var Mediabunny = (() => {
29334
29592
  assert(decoderConfig.codedHeight !== void 0);
29335
29593
  let requiresAnnexBTransformation = false;
29336
29594
  if (track.source._codec === "avc" && !decoderConfig.description) {
29595
+ if (!packet) {
29596
+ throw new Error("No AVC description provided; you must therefore provide a priming packet.");
29597
+ }
29337
29598
  const decoderConfigurationRecord = extractAvcDecoderConfigurationRecord(packet.data);
29338
29599
  if (!decoderConfigurationRecord) {
29339
29600
  throw new Error(
@@ -29343,6 +29604,9 @@ var Mediabunny = (() => {
29343
29604
  decoderConfig.description = serializeAvcDecoderConfigurationRecord(decoderConfigurationRecord);
29344
29605
  requiresAnnexBTransformation = true;
29345
29606
  } else if (track.source._codec === "hevc" && !decoderConfig.description) {
29607
+ if (!packet) {
29608
+ throw new Error("No HEVC description provided; you must therefore provide a priming packet.");
29609
+ }
29346
29610
  const decoderConfigurationRecord = extractHevcDecoderConfigurationRecord(packet.data);
29347
29611
  if (!decoderConfigurationRecord) {
29348
29612
  throw new Error(
@@ -29401,12 +29665,15 @@ var Mediabunny = (() => {
29401
29665
  if (existingTrackData) {
29402
29666
  return existingTrackData;
29403
29667
  }
29404
- validateAudioChunkMetadata(meta);
29668
+ validateAudioChunkMetadata(meta, track.source._codec);
29405
29669
  assert(meta);
29406
29670
  assert(meta.decoderConfig);
29407
29671
  const decoderConfig = { ...meta.decoderConfig };
29408
29672
  let requiresAdtsStripping = false;
29409
29673
  if (track.source._codec === "aac" && !decoderConfig.description) {
29674
+ if (!packet) {
29675
+ throw new Error("No AAC description provided; you must therefore provide a priming packet.");
29676
+ }
29410
29677
  const adtsFrame = readAdtsFrameHeader(FileSlice4.tempFromBytes(packet.data));
29411
29678
  if (!adtsFrame) {
29412
29679
  throw new Error(
@@ -29425,6 +29692,11 @@ var Mediabunny = (() => {
29425
29692
  });
29426
29693
  requiresAdtsStripping = true;
29427
29694
  }
29695
+ if (track.source._codec === "ac3" || track.source._codec === "eac3") {
29696
+ if (!packet) {
29697
+ throw new Error("AC-3/E-AC-3 require a priming packet.");
29698
+ }
29699
+ }
29428
29700
  const newTrackData = {
29429
29701
  muxer: this,
29430
29702
  track,
@@ -29436,7 +29708,7 @@ var Mediabunny = (() => {
29436
29708
  requiresPcmTransformation: !this.isFragmented && PCM_AUDIO_CODECS.includes(track.source._codec),
29437
29709
  expectedNextPcmPacketTimestamp: null,
29438
29710
  requiresAdtsStripping,
29439
- firstPacket: packet
29711
+ primingPacket: packet
29440
29712
  },
29441
29713
  timescale: decoderConfig.sampleRate,
29442
29714
  samples: [],
@@ -29865,7 +30137,8 @@ var Mediabunny = (() => {
29865
30137
  startTimestamp: sample.timestamp,
29866
30138
  samples: [],
29867
30139
  offset: null,
29868
- moofOffset: null
30140
+ moofOffset: null,
30141
+ trafIndex: null
29869
30142
  };
29870
30143
  }
29871
30144
  assert(trackData.currentChunk);
@@ -29932,19 +30205,19 @@ var Mediabunny = (() => {
29932
30205
  }
29933
30206
  async finalizeFragment(flushWriter = !this.isCmaf) {
29934
30207
  assert(this.isFragmented);
29935
- const fragmentNumber = this.nextFragmentNumber++;
29936
- if (fragmentNumber === 1) {
30208
+ if (!this.wroteFragmentedHeader) {
30209
+ this.wroteFragmentedHeader = true;
29937
30210
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
29938
30211
  assert(boxWriter);
29939
- if (this.format._options.onMoov) {
30212
+ if (this.formatOptions.onMoov) {
29940
30213
  boxWriter.writer.startTrackingWrites();
29941
30214
  }
29942
30215
  this.ensureOneEnabledTrack();
29943
30216
  const movieBox = moov(this);
29944
30217
  boxWriter.writeBox(movieBox);
29945
- if (this.format._options.onMoov) {
30218
+ if (this.formatOptions.onMoov) {
29946
30219
  const { data, start } = boxWriter.writer.stopTrackingWrites();
29947
- this.format._options.onMoov(data, start);
30220
+ this.formatOptions.onMoov(data, start);
29948
30221
  }
29949
30222
  if (this.isCmaf) {
29950
30223
  assert(this.initWriter);
@@ -29961,14 +30234,23 @@ var Mediabunny = (() => {
29961
30234
  assert(this.writer);
29962
30235
  assert(this.boxWriter);
29963
30236
  const tracksInFragment = this.trackDatas.filter((x) => x.currentChunk);
30237
+ if (tracksInFragment.length === 0) {
30238
+ if (flushWriter) {
30239
+ await this.writer.flush();
30240
+ }
30241
+ return;
30242
+ }
30243
+ const fragmentNumber = this.nextFragmentNumber++;
29964
30244
  const moofBox = moof(fragmentNumber, tracksInFragment);
29965
30245
  const moofOffset = this.writer.getPos();
29966
30246
  const mdatStartPos = moofOffset + this.boxWriter.measureBox(moofBox);
29967
30247
  let currentPos = mdatStartPos + MIN_BOX_HEADER_SIZE;
29968
30248
  let fragmentStartTimestamp = Infinity;
29969
- for (const trackData of tracksInFragment) {
30249
+ for (let i = 0; i < tracksInFragment.length; i++) {
30250
+ const trackData = tracksInFragment[i];
29970
30251
  trackData.currentChunk.offset = currentPos;
29971
30252
  trackData.currentChunk.moofOffset = moofOffset;
30253
+ trackData.currentChunk.trafIndex = i;
29972
30254
  for (const sample of trackData.currentChunk.samples) {
29973
30255
  currentPos += sample.size;
29974
30256
  }
@@ -29981,17 +30263,17 @@ var Mediabunny = (() => {
29981
30263
  trackData.currentChunk.offset += MAX_BOX_HEADER_SIZE - MIN_BOX_HEADER_SIZE;
29982
30264
  }
29983
30265
  }
29984
- if (this.format._options.onMoof) {
30266
+ if (this.formatOptions.onMoof) {
29985
30267
  this.writer.startTrackingWrites();
29986
30268
  }
29987
30269
  const newMoofBox = moof(fragmentNumber, tracksInFragment);
29988
30270
  this.boxWriter.writeBox(newMoofBox);
29989
- if (this.format._options.onMoof) {
30271
+ if (this.formatOptions.onMoof) {
29990
30272
  const { data, start } = this.writer.stopTrackingWrites();
29991
- this.format._options.onMoof(data, start, fragmentStartTimestamp);
30273
+ this.formatOptions.onMoof(data, start, fragmentStartTimestamp);
29992
30274
  }
29993
30275
  assert(this.writer.getPos() === mdatStartPos);
29994
- if (this.format._options.onMdat) {
30276
+ if (this.formatOptions.onMdat) {
29995
30277
  this.writer.startTrackingWrites();
29996
30278
  }
29997
30279
  const mdatBox = mdat(needsLargeMdatSize);
@@ -30004,9 +30286,9 @@ var Mediabunny = (() => {
30004
30286
  sample.data = null;
30005
30287
  }
30006
30288
  }
30007
- if (this.format._options.onMdat) {
30289
+ if (this.formatOptions.onMdat) {
30008
30290
  const { data, start } = this.writer.stopTrackingWrites();
30009
- this.format._options.onMdat(data, start);
30291
+ this.formatOptions.onMdat(data, start);
30010
30292
  }
30011
30293
  for (const trackData of tracksInFragment) {
30012
30294
  trackData.finalizedChunks.push(trackData.currentChunk);
@@ -30018,33 +30300,36 @@ var Mediabunny = (() => {
30018
30300
  }
30019
30301
  }
30020
30302
  async registerSampleFastStartReserve(trackData, sample) {
30021
- assert(this.writer);
30022
- assert(this.boxWriter);
30023
30303
  if (this.allTracksAreKnown()) {
30024
30304
  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
- }
30305
+ await this.createFastStartReserveMdat();
30042
30306
  }
30043
30307
  await this.addSampleToTrack(trackData, sample);
30044
30308
  } else {
30045
30309
  trackData.sampleQueue.push(sample);
30046
30310
  }
30047
30311
  }
30312
+ async createFastStartReserveMdat() {
30313
+ assert(this.writer);
30314
+ assert(this.boxWriter);
30315
+ this.ensureOneEnabledTrack();
30316
+ const moovBox = moov(this);
30317
+ const moovSize = this.boxWriter.measureBox(moovBox);
30318
+ const reservedSize = moovSize + this.computeSampleTableSizeUpperBound() + 4096;
30319
+ assert(this.ftypSize !== null);
30320
+ this.writer.seek(this.ftypSize + reservedSize);
30321
+ if (this.formatOptions.onMdat) {
30322
+ this.writer.startTrackingWrites();
30323
+ }
30324
+ this.mdat = mdat(true);
30325
+ this.boxWriter.writeBox(this.mdat);
30326
+ for (const trackData of this.trackDatas) {
30327
+ for (const sample of trackData.sampleQueue) {
30328
+ await this.addSampleToTrack(trackData, sample);
30329
+ }
30330
+ trackData.sampleQueue.length = 0;
30331
+ }
30332
+ }
30048
30333
  computeSampleTableSizeUpperBound() {
30049
30334
  assert(this.fastStart === "reserve");
30050
30335
  let upperBound = 0;
@@ -30095,11 +30380,31 @@ var Mediabunny = (() => {
30095
30380
  }
30096
30381
  }
30097
30382
  }
30383
+ /** Internal function for external callers who want to full control fragment boundaries. */
30384
+ async forceFragmentFinalization() {
30385
+ assert(this.isFragmented);
30386
+ const release = await this.mutex.acquire();
30387
+ try {
30388
+ for (const trackData of this.trackDatas) {
30389
+ if (trackData.type === "subtitle" && trackData.track.source._codec === "webvtt") {
30390
+ await this.processWebVTTCues(trackData, Infinity);
30391
+ }
30392
+ this.processTimestamps(trackData);
30393
+ }
30394
+ await this.interleaveSamples(true);
30395
+ await this.finalizeFragment();
30396
+ } finally {
30397
+ release();
30398
+ }
30399
+ }
30098
30400
  /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
30099
30401
  async finalize() {
30100
30402
  const release = await this.mutex.acquire();
30101
30403
  this.allTracksKnown.resolve();
30102
30404
  this.ensureOneEnabledTrack();
30405
+ if (!this.mdat && this.fastStart === "reserve") {
30406
+ await this.createFastStartReserveMdat();
30407
+ }
30103
30408
  for (const trackData of this.trackDatas) {
30104
30409
  trackData.closed = true;
30105
30410
  if (trackData.type === "subtitle" && trackData.track.source._codec === "webvtt") {
@@ -30113,11 +30418,12 @@ var Mediabunny = (() => {
30113
30418
  } else {
30114
30419
  for (const trackData of this.trackDatas) {
30115
30420
  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;
30421
+ if (trackData.startTimestampOffset !== null) {
30422
+ for (let i = 0; i < trackData.samples.length; i++) {
30423
+ const sample = trackData.samples[i];
30424
+ sample.timestamp -= trackData.startTimestampOffset;
30425
+ sample.decodeTimestamp -= trackData.startTimestampOffset;
30426
+ }
30121
30427
  }
30122
30428
  }
30123
30429
  }
@@ -30142,16 +30448,16 @@ var Mediabunny = (() => {
30142
30448
  if (currentChunkPos < 2 ** 32) break;
30143
30449
  if (mdatSize >= 2 ** 32) this.mdat.largeSize = true;
30144
30450
  }
30145
- if (this.format._options.onMoov) {
30451
+ if (this.formatOptions.onMoov) {
30146
30452
  this.writer.startTrackingWrites();
30147
30453
  }
30148
30454
  const movieBox = moov(this);
30149
30455
  this.boxWriter.writeBox(movieBox);
30150
- if (this.format._options.onMoov) {
30456
+ if (this.formatOptions.onMoov) {
30151
30457
  const { data, start } = this.writer.stopTrackingWrites();
30152
- this.format._options.onMoov(data, start);
30458
+ this.formatOptions.onMoov(data, start);
30153
30459
  }
30154
- if (this.format._options.onMdat) {
30460
+ if (this.formatOptions.onMdat) {
30155
30461
  this.writer.startTrackingWrites();
30156
30462
  }
30157
30463
  this.mdat.size = mdatSize;
@@ -30163,9 +30469,9 @@ var Mediabunny = (() => {
30163
30469
  sample.data = null;
30164
30470
  }
30165
30471
  }
30166
- if (this.format._options.onMdat) {
30472
+ if (this.formatOptions.onMdat) {
30167
30473
  const { data, start } = this.writer.stopTrackingWrites();
30168
- this.format._options.onMdat(data, start);
30474
+ this.formatOptions.onMdat(data, start);
30169
30475
  }
30170
30476
  } else if (this.isFragmented) {
30171
30477
  if (this.isCmaf) {
@@ -30189,29 +30495,29 @@ var Mediabunny = (() => {
30189
30495
  this.mdat.size = mdatSize;
30190
30496
  this.mdat.largeSize = mdatSize >= 2 ** 32;
30191
30497
  this.boxWriter.patchBox(this.mdat);
30192
- if (this.format._options.onMdat) {
30498
+ if (this.formatOptions.onMdat) {
30193
30499
  const { data, start } = this.writer.stopTrackingWrites();
30194
- this.format._options.onMdat(data, start);
30500
+ this.formatOptions.onMdat(data, start);
30195
30501
  }
30196
30502
  const movieBox = moov(this);
30197
30503
  if (this.fastStart === "reserve") {
30198
30504
  assert(this.ftypSize !== null);
30199
30505
  this.writer.seek(this.ftypSize);
30200
- if (this.format._options.onMoov) {
30506
+ if (this.formatOptions.onMoov) {
30201
30507
  this.writer.startTrackingWrites();
30202
30508
  }
30203
30509
  this.boxWriter.writeBox(movieBox);
30204
30510
  const remainingSpace = this.boxWriter.offsets.get(this.mdat) - this.writer.getPos();
30205
30511
  this.boxWriter.writeBox(free(remainingSpace));
30206
30512
  } else {
30207
- if (this.format._options.onMoov) {
30513
+ if (this.formatOptions.onMoov) {
30208
30514
  this.writer.startTrackingWrites();
30209
30515
  }
30210
30516
  this.boxWriter.writeBox(movieBox);
30211
30517
  }
30212
- if (this.format._options.onMoov) {
30518
+ if (this.formatOptions.onMoov) {
30213
30519
  const { data, start } = this.writer.stopTrackingWrites();
30214
- this.format._options.onMoov(data, start);
30520
+ this.formatOptions.onMoov(data, start);
30215
30521
  }
30216
30522
  }
30217
30523
  release();
@@ -30258,6 +30564,21 @@ var Mediabunny = (() => {
30258
30564
  this.createSegmentInfo();
30259
30565
  this.createCues();
30260
30566
  await this.writer.flush();
30567
+ for (const track of this.output.tracks) {
30568
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
30569
+ this.getVideoTrackData(
30570
+ track,
30571
+ track.metadata.primingPacket ?? null,
30572
+ { decoderConfig: track.metadata.decoderConfig }
30573
+ );
30574
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
30575
+ this.getAudioTrackData(
30576
+ track,
30577
+ track.metadata.primingPacket ?? null,
30578
+ { decoderConfig: track.metadata.decoderConfig }
30579
+ );
30580
+ }
30581
+ }
30261
30582
  release();
30262
30583
  }
30263
30584
  writeEBMLHeader() {
@@ -30722,7 +31043,7 @@ var Mediabunny = (() => {
30722
31043
  if (existingTrackData) {
30723
31044
  return existingTrackData;
30724
31045
  }
30725
- validateVideoChunkMetadata(meta);
31046
+ validateVideoChunkMetadata(meta, track.source._codec);
30726
31047
  assert(meta);
30727
31048
  assert(meta.decoderConfig);
30728
31049
  assert(meta.decoderConfig.codedWidth !== void 0);
@@ -30741,8 +31062,7 @@ var Mediabunny = (() => {
30741
31062
  height: meta.decoderConfig.codedHeight,
30742
31063
  aspectRatio,
30743
31064
  decoderConfig: meta.decoderConfig,
30744
- alphaMode: !!packet.sideData.alpha
30745
- // The first packet determines if this track has alpha or not
31065
+ alphaMode: packet ? !!packet.sideData.alpha : null
30746
31066
  },
30747
31067
  chunkQueue: [],
30748
31068
  lastWrittenMsTimestamp: null,
@@ -30772,12 +31092,15 @@ var Mediabunny = (() => {
30772
31092
  if (existingTrackData) {
30773
31093
  return existingTrackData;
30774
31094
  }
30775
- validateAudioChunkMetadata(meta);
31095
+ validateAudioChunkMetadata(meta, track.source._codec);
30776
31096
  assert(meta);
30777
31097
  assert(meta.decoderConfig);
30778
31098
  const decoderConfig = { ...meta.decoderConfig };
30779
31099
  let requiresAdtsStripping = false;
30780
31100
  if (track.source._codec === "aac" && !decoderConfig.description) {
31101
+ if (!packet) {
31102
+ throw new Error("No AAC description provided; you must therefore provide a priming packet.");
31103
+ }
30781
31104
  const adtsFrame = readAdtsFrameHeader(FileSlice4.tempFromBytes(packet.data));
30782
31105
  if (!adtsFrame) {
30783
31106
  throw new Error(
@@ -30847,6 +31170,7 @@ var Mediabunny = (() => {
30847
31170
  const release = await this.mutex.acquire();
30848
31171
  try {
30849
31172
  const trackData = this.getVideoTrackData(track, packet, meta);
31173
+ trackData.info.alphaMode ??= !!packet.sideData.alpha;
30850
31174
  let packetData = packet.data;
30851
31175
  if (track.source._codec === "prores") {
30852
31176
  if (packetData.byteLength < 8) {
@@ -31237,7 +31561,7 @@ ${cue.notes ?? ""}`;
31237
31561
  data.sampleRate,
31238
31562
  padding
31239
31563
  );
31240
- this.writer.seek(startPos + frameSize);
31564
+ this.writer.write(new Uint8Array(startPos + frameSize - this.writer.getPos()));
31241
31565
  }
31242
31566
  };
31243
31567
 
@@ -31322,22 +31646,95 @@ ${cue.notes ?? ""}`;
31322
31646
  throw new Error("MP3 does not support subtitles.");
31323
31647
  }
31324
31648
  async finalize() {
31325
- if (!this.xingFrameData || this.xingFramePos === null) {
31326
- return;
31327
- }
31328
31649
  const release = await this.mutex.acquire();
31650
+ if (!this.xingFrameData && this.format._options.xingHeader === false) {
31651
+ throw new Error(
31652
+ "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."
31653
+ );
31654
+ }
31655
+ if (!this.xingFrameData) {
31656
+ const track = this.output.tracks[0];
31657
+ assert(track?.isAudioTrack());
31658
+ const primingPacket = track.metadata.primingPacket;
31659
+ if (primingPacket) {
31660
+ const view2 = toDataView(primingPacket.data);
31661
+ if (view2.byteLength < 4) {
31662
+ throw new Error("Invalid MP3 header in priming packet.");
31663
+ }
31664
+ const word = view2.getUint32(0, false);
31665
+ const header = readMp3FrameHeader(word, null).header;
31666
+ if (!header) {
31667
+ throw new Error("Invalid MP3 header in priming packet.");
31668
+ }
31669
+ this.xingFrameData = {
31670
+ mpegVersionId: header.mpegVersionId,
31671
+ layer: header.layer,
31672
+ frequencyIndex: header.frequencyIndex,
31673
+ sampleRate: header.sampleRate,
31674
+ channel: header.channel,
31675
+ modeExtension: header.modeExtension,
31676
+ copyright: header.copyright,
31677
+ original: header.original,
31678
+ emphasis: header.emphasis,
31679
+ frameCount: null,
31680
+ fileSize: null,
31681
+ toc: null
31682
+ };
31683
+ } else if (track.metadata.decoderConfig) {
31684
+ const { sampleRate, numberOfChannels } = track.metadata.decoderConfig;
31685
+ const mpegVersionIds = [3, 2, 0];
31686
+ let mpegVersionId = null;
31687
+ let frequencyIndex = -1;
31688
+ for (let i = 0; i < mpegVersionIds.length; i++) {
31689
+ frequencyIndex = SAMPLING_RATES.indexOf(sampleRate << i);
31690
+ if (frequencyIndex !== -1) {
31691
+ mpegVersionId = mpegVersionIds[i];
31692
+ break;
31693
+ }
31694
+ }
31695
+ if (mpegVersionId === null) {
31696
+ throw new Error(`${sampleRate} Hz is not a valid MP3 sample rate.`);
31697
+ }
31698
+ this.xingFrameData = {
31699
+ mpegVersionId,
31700
+ layer: 1,
31701
+ // Layer III
31702
+ frequencyIndex,
31703
+ sampleRate,
31704
+ channel: numberOfChannels === 1 ? 3 : 0,
31705
+ // 3 = single channel, 0 = stereo
31706
+ modeExtension: 0,
31707
+ copyright: 0,
31708
+ original: 0,
31709
+ emphasis: 0,
31710
+ frameCount: null,
31711
+ fileSize: null,
31712
+ toc: null
31713
+ };
31714
+ } else {
31715
+ throw new Error(
31716
+ "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."
31717
+ );
31718
+ }
31719
+ this.xingFramePos = this.writer.getPos();
31720
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
31721
+ this.frameCount++;
31722
+ }
31723
+ assert(this.xingFramePos !== null);
31329
31724
  const endPos = this.writer.getPos();
31330
31725
  const audioDataEndPos = endPos - this.xingFramePos;
31331
31726
  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);
31727
+ if (this.framePositions.length > 0) {
31728
+ const toc = new Uint8Array(100);
31729
+ for (let i = 0; i < 100; i++) {
31730
+ const index = Math.floor(this.framePositions.length * (i / 100));
31731
+ const byteOffset = this.framePositions[index] - this.xingFramePos;
31732
+ toc[i] = 256 * (byteOffset / audioDataEndPos);
31733
+ }
31734
+ this.xingFrameData.toc = toc;
31337
31735
  }
31338
31736
  this.xingFrameData.frameCount = this.frameCount;
31339
31737
  this.xingFrameData.fileSize = audioDataEndPos;
31340
- this.xingFrameData.toc = toc;
31341
31738
  if (this.format._options.onXingFrame) {
31342
31739
  this.writer.startTrackingWrites();
31343
31740
  }
@@ -31365,6 +31762,12 @@ ${cue.notes ?? ""}`;
31365
31762
  async start() {
31366
31763
  const release = await this.mutex.acquire();
31367
31764
  this.writer = await this.output._getRootWriter(true);
31765
+ for (const track of this.output.tracks) {
31766
+ assert(track.isAudioTrack());
31767
+ if (track.metadata.decoderConfig) {
31768
+ this.getTrackData(track, { decoderConfig: track.metadata.decoderConfig });
31769
+ }
31770
+ }
31368
31771
  release();
31369
31772
  }
31370
31773
  async getMimeType() {
@@ -31386,7 +31789,7 @@ ${cue.notes ?? ""}`;
31386
31789
  serialNumber = Math.floor(2 ** 32 * Math.random());
31387
31790
  } while (this.trackDatas.some((td) => td.serialNumber === serialNumber));
31388
31791
  assert(track.source._codec === "vorbis" || track.source._codec === "opus");
31389
- validateAudioChunkMetadata(meta);
31792
+ validateAudioChunkMetadata(meta, track.source._codec);
31390
31793
  assert(meta);
31391
31794
  assert(meta.decoderConfig);
31392
31795
  const newTrackData = {
@@ -31738,7 +32141,7 @@ ${cue.notes ?? ""}`;
31738
32141
  if (existingTrackData) {
31739
32142
  return existingTrackData;
31740
32143
  }
31741
- validateVideoChunkMetadata(meta);
32144
+ validateVideoChunkMetadata(meta, track.source._codec);
31742
32145
  assert(meta?.decoderConfig);
31743
32146
  const codec = track.source._codec;
31744
32147
  assert(codec === "avc" || codec === "hevc");
@@ -31773,7 +32176,7 @@ ${cue.notes ?? ""}`;
31773
32176
  if (existingTrackData) {
31774
32177
  return existingTrackData;
31775
32178
  }
31776
- validateAudioChunkMetadata(meta);
32179
+ validateAudioChunkMetadata(meta, track.source._codec);
31777
32180
  assert(meta?.decoderConfig);
31778
32181
  const codec = track.source._codec;
31779
32182
  assert(codec === "aac" || codec === "mp3" || codec === "ac3" || codec === "eac3");
@@ -32325,6 +32728,14 @@ ${cue.notes ?? ""}`;
32325
32728
  const release = await this.mutex.acquire();
32326
32729
  this.writer = await this.output._getRootWriter(false);
32327
32730
  this.riffWriter = new RiffWriter(this.writer);
32731
+ const track = this.output.tracks[0];
32732
+ assert(track?.isAudioTrack());
32733
+ if (track.metadata.decoderConfig) {
32734
+ validateAudioChunkMetadata({ decoderConfig: track.metadata.decoderConfig }, track.source._codec);
32735
+ this.writeHeader(track, track.metadata.decoderConfig);
32736
+ this.sampleRate = track.metadata.decoderConfig.sampleRate;
32737
+ this.headerWritten = true;
32738
+ }
32328
32739
  release();
32329
32740
  }
32330
32741
  async getMimeType() {
@@ -32337,7 +32748,7 @@ ${cue.notes ?? ""}`;
32337
32748
  const release = await this.mutex.acquire();
32338
32749
  try {
32339
32750
  if (!this.headerWritten) {
32340
- validateAudioChunkMetadata(meta);
32751
+ validateAudioChunkMetadata(meta, track.source._codec);
32341
32752
  assert(meta);
32342
32753
  assert(meta.decoderConfig);
32343
32754
  this.writeHeader(track, meta.decoderConfig);
@@ -32561,6 +32972,11 @@ ${cue.notes ?? ""}`;
32561
32972
  }
32562
32973
  async finalize() {
32563
32974
  const release = await this.mutex.acquire();
32975
+ if (!this.headerWritten) {
32976
+ throw new Error(
32977
+ "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."
32978
+ );
32979
+ }
32564
32980
  const endPos = this.writer.getPos();
32565
32981
  if (this.isRf64) {
32566
32982
  assert(this.ds64RiffSizePos !== null);
@@ -33235,10 +33651,13 @@ ${cue.notes ?? ""}`;
33235
33651
  `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
33652
  );
33237
33653
  }
33238
- const support = await VideoEncoder.isConfigSupported(candidateConfig);
33239
- if (support.supported) {
33240
- selected = candidate;
33241
- break;
33654
+ try {
33655
+ const support = await VideoEncoder.isConfigSupported(candidateConfig);
33656
+ if (support.supported) {
33657
+ selected = candidate;
33658
+ break;
33659
+ }
33660
+ } catch {
33242
33661
  }
33243
33662
  }
33244
33663
  if (!selected) {
@@ -34294,8 +34713,14 @@ ${cue.notes ?? ""}`;
34294
34713
  if (typeof AudioEncoder === "undefined") {
34295
34714
  throw new Error("AudioEncoder is not supported by this browser.");
34296
34715
  }
34297
- const support = await AudioEncoder.isConfigSupported(encoderConfig);
34298
- if (!support.supported) {
34716
+ let supported;
34717
+ try {
34718
+ const support = await AudioEncoder.isConfigSupported(encoderConfig);
34719
+ supported = support.supported ?? false;
34720
+ } catch {
34721
+ supported = false;
34722
+ }
34723
+ if (!supported) {
34299
34724
  throw new Error(
34300
34725
  `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
34726
  );
@@ -35177,6 +35602,21 @@ ${cue.notes ?? ""}`;
35177
35602
  references: variant.linkedGroup ? this.playlistDeclarations.filter((x) => x.groupId === variant.linkedGroup.name) : []
35178
35603
  });
35179
35604
  }
35605
+ for (const track of this.output.tracks) {
35606
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
35607
+ this.getVideoTrackData(
35608
+ track,
35609
+ track.metadata.primingPacket ?? null,
35610
+ { decoderConfig: track.metadata.decoderConfig }
35611
+ );
35612
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
35613
+ this.getAudioTrackData(
35614
+ track,
35615
+ track.metadata.primingPacket ?? null,
35616
+ { decoderConfig: track.metadata.decoderConfig }
35617
+ );
35618
+ }
35619
+ }
35180
35620
  release();
35181
35621
  }
35182
35622
  async getMimeType() {
@@ -35205,12 +35645,12 @@ ${cue.notes ?? ""}`;
35205
35645
  release();
35206
35646
  }
35207
35647
  }
35208
- getVideoTrackData(track, meta) {
35648
+ getVideoTrackData(track, packet, meta) {
35209
35649
  let trackData = this.trackDatas.find((x) => x.track === track);
35210
35650
  if (trackData) {
35211
35651
  return trackData;
35212
35652
  }
35213
- validateVideoChunkMetadata(meta);
35653
+ validateVideoChunkMetadata(meta, track.source._codec);
35214
35654
  assert(meta);
35215
35655
  assert(meta?.decoderConfig);
35216
35656
  const playlists = this.playlists.filter((x) => x.tracks.includes(track));
@@ -35222,18 +35662,19 @@ ${cue.notes ?? ""}`;
35222
35662
  closed: false,
35223
35663
  info: {
35224
35664
  type: "video",
35225
- decoderConfig: meta.decoderConfig
35665
+ decoderConfig: meta.decoderConfig,
35666
+ primingPacket: packet
35226
35667
  }
35227
35668
  };
35228
35669
  this.trackDatas.push(trackData);
35229
35670
  return trackData;
35230
35671
  }
35231
- getAudioTrackData(track, meta) {
35672
+ getAudioTrackData(track, packet, meta) {
35232
35673
  let trackData = this.trackDatas.find((x) => x.track === track);
35233
35674
  if (trackData) {
35234
35675
  return trackData;
35235
35676
  }
35236
- validateAudioChunkMetadata(meta);
35677
+ validateAudioChunkMetadata(meta, track.source._codec);
35237
35678
  assert(meta);
35238
35679
  assert(meta?.decoderConfig);
35239
35680
  const playlists = this.playlists.filter((x) => x.tracks.includes(track));
@@ -35245,14 +35686,15 @@ ${cue.notes ?? ""}`;
35245
35686
  closed: false,
35246
35687
  info: {
35247
35688
  type: "audio",
35248
- decoderConfig: meta.decoderConfig
35689
+ decoderConfig: meta.decoderConfig,
35690
+ primingPacket: packet
35249
35691
  }
35250
35692
  };
35251
35693
  this.trackDatas.push(trackData);
35252
35694
  return trackData;
35253
35695
  }
35254
35696
  async addEncodedVideoPacket(track, packet, meta) {
35255
- const trackData = this.getVideoTrackData(track, meta);
35697
+ const trackData = this.getVideoTrackData(track, packet, meta);
35256
35698
  const playlist = trackData.playlist;
35257
35699
  const release = await playlist.mutex.acquire();
35258
35700
  try {
@@ -35272,7 +35714,7 @@ ${cue.notes ?? ""}`;
35272
35714
  }
35273
35715
  }
35274
35716
  async addEncodedAudioPacket(track, packet, meta) {
35275
- const trackData = this.getAudioTrackData(track, meta);
35717
+ const trackData = this.getAudioTrackData(track, packet, meta);
35276
35718
  const playlist = trackData.playlist;
35277
35719
  const release = await playlist.mutex.acquire();
35278
35720
  try {
@@ -35299,11 +35741,13 @@ ${cue.notes ?? ""}`;
35299
35741
  if (!this.allTracksAreKnown(playlist)) {
35300
35742
  return;
35301
35743
  }
35744
+ const trackDatas = this.trackDatas.filter((x) => playlist.tracks.includes(x.track));
35302
35745
  if (playlist.currentSegmentStartTimestamp === null) {
35303
- await this.onPlaylistDone(playlist);
35746
+ if (trackDatas.every((x) => x.closed)) {
35747
+ await this.onPlaylistDone(playlist);
35748
+ }
35304
35749
  return;
35305
35750
  }
35306
- const trackDatas = this.trackDatas.filter((x) => playlist.tracks.includes(x.track));
35307
35751
  const videoTrack = trackDatas.find((x) => x.info.type === "video");
35308
35752
  const audioTrack = trackDatas.find((x) => x.info.type === "audio");
35309
35753
  while (true) {
@@ -35422,12 +35866,68 @@ ${cue.notes ?? ""}`;
35422
35866
  isRoot: false,
35423
35867
  mimeType: playlist.segmentFormat.mimeType
35424
35868
  });
35425
- target._start();
35869
+ let fragmentedIsobmffOutput = null;
35870
+ if (playlist.segmentFormat._isFragmentedIsobmff()) {
35871
+ fragmentedIsobmffOutput = {
35872
+ output: new Output({
35873
+ format: playlist.segmentFormat,
35874
+ target
35875
+ }),
35876
+ videoSource: null,
35877
+ audioSource: null,
35878
+ firstMoofPosition: null,
35879
+ currentFileSize: 0
35880
+ };
35881
+ target.on("write", ({ end }) => {
35882
+ fragmentedIsobmffOutput.currentFileSize = Math.max(
35883
+ fragmentedIsobmffOutput.currentFileSize,
35884
+ end
35885
+ );
35886
+ });
35887
+ const muxer = fragmentedIsobmffOutput.output._muxer;
35888
+ muxer.minimumFragmentDuration = Infinity;
35889
+ const originalOnMoof = muxer.formatOptions.onMoof;
35890
+ muxer.formatOptions.onMoof = (data, position, timestamp) => {
35891
+ fragmentedIsobmffOutput.firstMoofPosition = position;
35892
+ originalOnMoof?.(data, position, timestamp);
35893
+ muxer.formatOptions.onMoof = originalOnMoof;
35894
+ };
35895
+ if (videoTrack) {
35896
+ fragmentedIsobmffOutput.videoSource = new EncodedVideoPacketSource(
35897
+ videoTrack.track.source._codec
35898
+ );
35899
+ fragmentedIsobmffOutput.output.addVideoTrack(
35900
+ fragmentedIsobmffOutput.videoSource,
35901
+ {
35902
+ ...videoTrack.track.metadata,
35903
+ decoderConfig: videoTrack.info.decoderConfig,
35904
+ primingPacket: videoTrack.info.primingPacket ?? void 0
35905
+ }
35906
+ );
35907
+ }
35908
+ if (audioTrack) {
35909
+ fragmentedIsobmffOutput.audioSource = new EncodedAudioPacketSource(
35910
+ audioTrack.track.source._codec
35911
+ );
35912
+ fragmentedIsobmffOutput.output.addAudioTrack(
35913
+ fragmentedIsobmffOutput.audioSource,
35914
+ {
35915
+ ...audioTrack.track.metadata,
35916
+ decoderConfig: audioTrack.info.decoderConfig,
35917
+ primingPacket: audioTrack.info.primingPacket ?? void 0
35918
+ }
35919
+ );
35920
+ }
35921
+ await fragmentedIsobmffOutput.output.start();
35922
+ } else {
35923
+ target._start();
35924
+ }
35426
35925
  playlist.singleFile = {
35427
35926
  target,
35428
35927
  path: relativeSegmentPath,
35429
35928
  nextOffset: 0,
35430
- info: segmentInfo2
35929
+ info: segmentInfo2,
35930
+ fragmentedIsobmffOutput
35431
35931
  };
35432
35932
  } else {
35433
35933
  relativeSegmentPath = playlist.singleFile.path;
@@ -35450,95 +35950,114 @@ ${cue.notes ?? ""}`;
35450
35950
  }
35451
35951
  let segmentSize = 0;
35452
35952
  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) {
35953
+ let maxEndTimestamp = -Infinity;
35954
+ let output = null;
35955
+ let videoSource = null;
35956
+ let audioSource = null;
35957
+ try {
35958
+ if (playlist.singleFile?.fragmentedIsobmffOutput) {
35959
+ output = playlist.singleFile.fragmentedIsobmffOutput.output;
35960
+ videoSource = playlist.singleFile.fragmentedIsobmffOutput.videoSource;
35961
+ audioSource = playlist.singleFile.fragmentedIsobmffOutput.audioSource;
35962
+ } else {
35963
+ output = new Output({
35964
+ format: playlist.segmentFormat,
35965
+ target: new PathedTarget(
35966
+ fullSegmentPath,
35967
+ async (request) => {
35968
+ const proxiedRequest = {
35969
+ ...request,
35970
+ isRoot: false
35971
+ };
35972
+ if (request.isRoot) {
35973
+ if (playlist.singleFile) {
35974
+ const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
35975
+ slice.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
35976
+ return slice;
35977
+ } else {
35978
+ const target = await this.output._getTarget(proxiedRequest);
35979
+ outputTarget = target;
35980
+ target.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
35981
+ return target;
35982
+ }
35983
+ }
35984
+ return this.output._getTarget(proxiedRequest);
35985
+ }
35986
+ ),
35987
+ initTarget: async () => {
35988
+ if (playlist.initSegment) {
35989
+ return new NullTarget();
35990
+ }
35463
35991
  if (playlist.singleFile) {
35992
+ playlist.initSegment = {
35993
+ path: playlist.singleFile.path,
35994
+ duration: 0,
35995
+ timestamp: 0,
35996
+ byteSize: 0,
35997
+ byteOffset: 0,
35998
+ info: null
35999
+ };
35464
36000
  const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
35465
- slice.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
36001
+ slice.on("write", ({ end }) => {
36002
+ playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
36003
+ });
36004
+ slice.on("finalized", () => {
36005
+ playlist.singleFile.nextOffset = playlist.initSegment.byteSize;
36006
+ });
35466
36007
  return slice;
35467
36008
  } else {
35468
- const target = await this.output._getTarget(proxiedRequest);
35469
- outputTarget = target;
35470
- target.on("write", ({ end }) => segmentSize = Math.max(segmentSize, end));
36009
+ const playlistInfo = toPlaylistInfo(playlist);
36010
+ const initPath = await this.getInitPath(playlistInfo);
36011
+ validateInitPath(initPath);
36012
+ playlist.initSegment = {
36013
+ path: initPath,
36014
+ duration: 0,
36015
+ timestamp: 0,
36016
+ byteSize: 0,
36017
+ byteOffset: null,
36018
+ info: null
36019
+ };
36020
+ const fullInitPath = joinPaths(
36021
+ joinPaths(pathedTarget.rootPath, playlist.path),
36022
+ initPath
36023
+ );
36024
+ const target = await this.output._getTarget({
36025
+ path: fullInitPath,
36026
+ isRoot: false,
36027
+ mimeType: playlist.segmentFormat.mimeType
36028
+ });
36029
+ target.on("write", ({ end }) => {
36030
+ playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
36031
+ });
36032
+ target.on("finalized", () => {
36033
+ this.format._options.onInit?.(target, playlistInfo);
36034
+ });
35471
36035
  return target;
35472
36036
  }
35473
36037
  }
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
36038
+ });
36039
+ if (videoTrack) {
36040
+ videoSource = new EncodedVideoPacketSource(
36041
+ videoTrack.track.source._codec
35513
36042
  );
35514
- const target = await this.output._getTarget({
35515
- path: fullInitPath,
35516
- isRoot: false,
35517
- mimeType: playlist.segmentFormat.mimeType
36043
+ output.addVideoTrack(videoSource, {
36044
+ ...videoTrack.track.metadata,
36045
+ decoderConfig: videoTrack.info.decoderConfig,
36046
+ primingPacket: videoTrack.info.primingPacket ?? void 0
35518
36047
  });
35519
- target.on("write", ({ end }) => {
35520
- playlist.initSegment.byteSize = Math.max(playlist.initSegment.byteSize, end);
35521
- });
35522
- target.on("finalized", () => {
35523
- this.format._options.onInit?.(target, playlistInfo);
36048
+ }
36049
+ if (audioTrack) {
36050
+ audioSource = new EncodedAudioPacketSource(
36051
+ audioTrack.track.source._codec
36052
+ );
36053
+ output.addAudioTrack(audioSource, {
36054
+ ...audioTrack.track.metadata,
36055
+ decoderConfig: audioTrack.info.decoderConfig,
36056
+ primingPacket: audioTrack.info.primingPacket ?? void 0
35524
36057
  });
35525
- return target;
35526
36058
  }
36059
+ await output.start();
35527
36060
  }
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
36061
  if (videoTrack) {
35543
36062
  assert(videoSource);
35544
36063
  const meta = { decoderConfig: videoTrack.info.decoderConfig };
@@ -35557,9 +36076,26 @@ ${cue.notes ?? ""}`;
35557
36076
  maxEndTimestamp = Math.max(maxEndTimestamp, packet.timestamp + packet.duration);
35558
36077
  }
35559
36078
  }
35560
- await output.finalize();
36079
+ if (playlist.singleFile?.fragmentedIsobmffOutput) {
36080
+ const muxer = playlist.singleFile.fragmentedIsobmffOutput.output._muxer;
36081
+ await muxer.forceFragmentFinalization();
36082
+ if (playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition !== null && !playlist.initSegment) {
36083
+ playlist.initSegment = {
36084
+ path: playlist.singleFile.path,
36085
+ duration: 0,
36086
+ timestamp: 0,
36087
+ byteSize: playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition,
36088
+ byteOffset: 0,
36089
+ info: null
36090
+ };
36091
+ playlist.singleFile.nextOffset = playlist.singleFile.fragmentedIsobmffOutput.firstMoofPosition;
36092
+ }
36093
+ segmentSize = playlist.singleFile.fragmentedIsobmffOutput.currentFileSize - playlist.singleFile.nextOffset;
36094
+ } else {
36095
+ await output.finalize();
36096
+ }
35561
36097
  } catch (e) {
35562
- await output.cancel();
36098
+ await output?.cancel();
35563
36099
  throw e;
35564
36100
  }
35565
36101
  if (segmentInfo) {
@@ -35617,8 +36153,12 @@ ${cue.notes ?? ""}`;
35617
36153
  assert(!playlist.done);
35618
36154
  playlist.done = true;
35619
36155
  if (playlist.singleFile) {
35620
- await playlist.singleFile.target._flush();
35621
- await playlist.singleFile.target._finalize();
36156
+ if (playlist.singleFile.fragmentedIsobmffOutput) {
36157
+ await playlist.singleFile.fragmentedIsobmffOutput.output.finalize();
36158
+ } else {
36159
+ await playlist.singleFile.target._flush();
36160
+ await playlist.singleFile.target._finalize();
36161
+ }
35622
36162
  this.format._options.onSegment?.(playlist.singleFile.target, playlist.singleFile.info);
35623
36163
  }
35624
36164
  await this.writePlaylist(playlist);
@@ -35938,6 +36478,10 @@ ${cue.notes ?? ""}`;
35938
36478
  _codecUnsupportedHint(codec) {
35939
36479
  return "";
35940
36480
  }
36481
+ /** @internal */
36482
+ _isFragmentedIsobmff() {
36483
+ return false;
36484
+ }
35941
36485
  };
35942
36486
  var IsobmffOutputFormat2 = class extends OutputFormat {
35943
36487
  /** Internal constructor. */
@@ -35979,7 +36523,7 @@ ${cue.notes ?? ""}`;
35979
36523
  video: { min: 0, max },
35980
36524
  audio: { min: 0, max },
35981
36525
  subtitle: { min: 0, max },
35982
- total: { min: 1, max }
36526
+ total: { min: 0, max }
35983
36527
  };
35984
36528
  }
35985
36529
  get supportsVideoRotationMetadata() {
@@ -35992,6 +36536,10 @@ ${cue.notes ?? ""}`;
35992
36536
  _createMuxer(output) {
35993
36537
  return new IsobmffMuxer2(output, this);
35994
36538
  }
36539
+ /** @internal */
36540
+ _isFragmentedIsobmff() {
36541
+ return this._options.fastStart === "fragmented";
36542
+ }
35995
36543
  };
35996
36544
  var Mp4OutputFormat = class extends IsobmffOutputFormat2 {
35997
36545
  /** Creates a new {@link Mp4OutputFormat} configured with the specified `options`. */
@@ -36135,7 +36683,7 @@ ${cue.notes ?? ""}`;
36135
36683
  video: { min: 0, max },
36136
36684
  audio: { min: 0, max },
36137
36685
  subtitle: { min: 0, max },
36138
- total: { min: 1, max }
36686
+ total: { min: 0, max }
36139
36687
  };
36140
36688
  }
36141
36689
  get fileExtension() {
@@ -36319,7 +36867,7 @@ ${cue.notes ?? ""}`;
36319
36867
  video: { min: 0, max: 0 },
36320
36868
  audio: { min: 0, max },
36321
36869
  subtitle: { min: 0, max: 0 },
36322
- total: { min: 1, max }
36870
+ total: { min: 0, max }
36323
36871
  };
36324
36872
  }
36325
36873
  get fileExtension() {
@@ -36456,7 +37004,7 @@ ${cue.notes ?? ""}`;
36456
37004
  video: { min: 0, max: maxVideo },
36457
37005
  audio: { min: 0, max: maxAudio },
36458
37006
  subtitle: { min: 0, max: 0 },
36459
- total: { min: 1, max: maxTotal }
37007
+ total: { min: 0, max: maxTotal }
36460
37008
  };
36461
37009
  }
36462
37010
  get fileExtension() {
@@ -36561,7 +37109,7 @@ ${cue.notes ?? ""}`;
36561
37109
  audio: { min: 0, max: supportsAudio ? Infinity : 0 },
36562
37110
  subtitle: { min: 0, max: 0 },
36563
37111
  // Currently disabled
36564
- total: { min: 1, max: Infinity }
37112
+ total: { min: 0, max: Infinity }
36565
37113
  };
36566
37114
  }
36567
37115
  get supportsVideoRotationMetadata() {
@@ -36892,6 +37440,17 @@ ${cue.notes ?? ""}`;
36892
37440
  `Invalid video frame rate: ${metadata.frameRate}. Must be a positive number.`
36893
37441
  );
36894
37442
  }
37443
+ if (metadata.decoderConfig !== void 0) {
37444
+ validateVideoChunkMetadata({ decoderConfig: metadata.decoderConfig }, source._codec);
37445
+ }
37446
+ if (metadata.primingPacket !== void 0) {
37447
+ if (!(metadata.primingPacket instanceof EncodedPacket)) {
37448
+ throw new TypeError("metadata.primingPacket, when provided, must be an EncodedPacket.");
37449
+ }
37450
+ if (metadata.decoderConfig === void 0) {
37451
+ throw new TypeError("metadata.primingPacket can only be provided alongside metadata.decoderConfig.");
37452
+ }
37453
+ }
36895
37454
  const metadataCopy = { ...metadata };
36896
37455
  metadataCopy.group ??= this.defaultTrackGroup;
36897
37456
  return this._addTrack(new OutputVideoTrack2(
@@ -36907,6 +37466,17 @@ ${cue.notes ?? ""}`;
36907
37466
  throw new TypeError("source must be an AudioSource.");
36908
37467
  }
36909
37468
  validateBaseTrackMetadata(metadata);
37469
+ if (metadata.decoderConfig !== void 0) {
37470
+ validateAudioChunkMetadata({ decoderConfig: metadata.decoderConfig }, source._codec);
37471
+ }
37472
+ if (metadata.primingPacket !== void 0) {
37473
+ if (!(metadata.primingPacket instanceof EncodedPacket)) {
37474
+ throw new TypeError("metadata.primingPacket, when provided, must be an EncodedPacket.");
37475
+ }
37476
+ if (metadata.decoderConfig === void 0) {
37477
+ throw new TypeError("metadata.primingPacket can only be provided alongside metadata.decoderConfig.");
37478
+ }
37479
+ }
36910
37480
  const metadataCopy = { ...metadata };
36911
37481
  metadataCopy.group ??= this.defaultTrackGroup;
36912
37482
  return this._addTrack(new OutputAudioTrack2(
@@ -37597,7 +38167,7 @@ ${cue.notes ?? ""}`;
37597
38167
  this.output.setMetadataTags(outputTags);
37598
38168
  }
37599
38169
  if (!this._composable) {
37600
- this.isValid = this.output.hasEnoughTracks();
38170
+ this.isValid = this.output.hasEnoughTracks() && this.output.tracks.length > 0;
37601
38171
  } else {
37602
38172
  this.isValid = true;
37603
38173
  }