mediabunny 1.4.3 → 1.5.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.
@@ -5660,7 +5660,11 @@ var Mediabunny = (() => {
5660
5660
  readString(length) {
5661
5661
  const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
5662
5662
  this.pos += length;
5663
- return String.fromCharCode(...new Uint8Array(view2.buffer, offset, length));
5663
+ let strLength = 0;
5664
+ while (strLength < length && view2.getUint8(offset + strLength) !== 0) {
5665
+ strLength += 1;
5666
+ }
5667
+ return String.fromCharCode(...new Uint8Array(view2.buffer, offset, strLength));
5664
5668
  }
5665
5669
  readElementId() {
5666
5670
  const size = this.readVarIntSize();
@@ -5726,6 +5730,27 @@ var Mediabunny = (() => {
5726
5730
  "pcm-f64": "A_PCM/FLOAT/IEEE",
5727
5731
  "webvtt": "S_TEXT/WEBVTT"
5728
5732
  };
5733
+ var readVarInt = (data, offset) => {
5734
+ if (offset >= data.length) {
5735
+ throw new Error("Offset out of bounds.");
5736
+ }
5737
+ const firstByte = data[offset];
5738
+ let width = 1;
5739
+ let mask = 1 << 7;
5740
+ while ((firstByte & mask) === 0 && width < 8) {
5741
+ width++;
5742
+ mask >>= 1;
5743
+ }
5744
+ if (offset + width > data.length) {
5745
+ throw new Error("VarInt extends beyond data bounds.");
5746
+ }
5747
+ let value = firstByte & mask - 1;
5748
+ for (let i = 1; i < width; i++) {
5749
+ value *= 1 << 8;
5750
+ value += data[offset + i];
5751
+ }
5752
+ return { value, width };
5753
+ };
5729
5754
  function assertDefinedSize(size) {
5730
5755
  if (size === null) {
5731
5756
  throw new Error("Undefined element size is used in a place where it is not supported.");
@@ -13571,199 +13596,195 @@ ${cue.notes ?? ""}`;
13571
13596
  return firstPacket?.timestamp ?? 0;
13572
13597
  }
13573
13598
  async getFirstPacket(options) {
13574
- if (this.internalTrack.demuxer.isFragmented) {
13575
- return this.performFragmentedLookup(
13576
- () => {
13577
- const startFragment = this.internalTrack.demuxer.fragments[0] ?? null;
13578
- if (startFragment?.isKnownToBeFirstFragment) {
13579
- let currentFragment = startFragment;
13580
- while (currentFragment) {
13581
- const trackData = currentFragment.trackData.get(this.internalTrack.id);
13582
- if (trackData) {
13583
- return {
13584
- fragmentIndex: binarySearchExact(
13585
- this.internalTrack.fragments,
13586
- currentFragment.moofOffset,
13587
- (x) => x.moofOffset
13588
- ),
13589
- sampleIndex: 0,
13590
- correctSampleFound: true
13591
- };
13592
- }
13593
- currentFragment = currentFragment.nextFragment;
13599
+ const regularPacket = await this.fetchPacketForSampleIndex(0, options);
13600
+ if (regularPacket || !this.internalTrack.demuxer.isFragmented) {
13601
+ return regularPacket;
13602
+ }
13603
+ return this.performFragmentedLookup(
13604
+ () => {
13605
+ const startFragment = this.internalTrack.demuxer.fragments[0] ?? null;
13606
+ if (startFragment?.isKnownToBeFirstFragment) {
13607
+ let currentFragment = startFragment;
13608
+ while (currentFragment) {
13609
+ const trackData = currentFragment.trackData.get(this.internalTrack.id);
13610
+ if (trackData) {
13611
+ return {
13612
+ fragmentIndex: binarySearchExact(
13613
+ this.internalTrack.fragments,
13614
+ currentFragment.moofOffset,
13615
+ (x) => x.moofOffset
13616
+ ),
13617
+ sampleIndex: 0,
13618
+ correctSampleFound: true
13619
+ };
13594
13620
  }
13621
+ currentFragment = currentFragment.nextFragment;
13595
13622
  }
13596
- return {
13597
- fragmentIndex: -1,
13598
- sampleIndex: -1,
13599
- correctSampleFound: false
13600
- };
13601
- },
13602
- -Infinity,
13603
- // Use -Infinity as a search timestamp to avoid using the lookup entries
13604
- Infinity,
13605
- options
13606
- );
13607
- }
13608
- return this.fetchPacketForSampleIndex(0, options);
13623
+ }
13624
+ return {
13625
+ fragmentIndex: -1,
13626
+ sampleIndex: -1,
13627
+ correctSampleFound: false
13628
+ };
13629
+ },
13630
+ -Infinity,
13631
+ // Use -Infinity as a search timestamp to avoid using the lookup entries
13632
+ Infinity,
13633
+ options
13634
+ );
13609
13635
  }
13610
13636
  mapTimestampIntoTimescale(timestamp) {
13611
13637
  return roundToPrecision(timestamp * this.internalTrack.timescale, 14) + this.internalTrack.editListOffset;
13612
13638
  }
13613
13639
  async getPacket(timestamp, options) {
13614
13640
  const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
13615
- if (this.internalTrack.demuxer.isFragmented) {
13616
- return this.performFragmentedLookup(
13617
- () => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
13618
- timestampInTimescale,
13619
- timestampInTimescale,
13620
- options
13621
- );
13622
- } else {
13623
- const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
13624
- const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
13625
- return this.fetchPacketForSampleIndex(sampleIndex, options);
13641
+ const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
13642
+ const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
13643
+ const regularPacket = await this.fetchPacketForSampleIndex(sampleIndex, options);
13644
+ if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
13645
+ return regularPacket;
13626
13646
  }
13647
+ return this.performFragmentedLookup(
13648
+ () => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
13649
+ timestampInTimescale,
13650
+ timestampInTimescale,
13651
+ options
13652
+ );
13627
13653
  }
13628
13654
  async getNextPacket(packet, options) {
13629
- if (this.internalTrack.demuxer.isFragmented) {
13630
- const locationInFragment = this.packetToFragmentLocation.get(packet);
13631
- if (locationInFragment === void 0) {
13632
- throw new Error("Packet was not created from this track.");
13633
- }
13634
- const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id);
13635
- const fragmentSample = trackData.samples[locationInFragment.sampleIndex];
13636
- const fragmentIndex = binarySearchExact(
13637
- this.internalTrack.fragments,
13638
- locationInFragment.fragment.moofOffset,
13639
- (x) => x.moofOffset
13640
- );
13641
- assert(fragmentIndex !== -1);
13642
- return this.performFragmentedLookup(
13643
- () => {
13644
- if (locationInFragment.sampleIndex + 1 < trackData.samples.length) {
13645
- return {
13646
- fragmentIndex,
13647
- sampleIndex: locationInFragment.sampleIndex + 1,
13648
- correctSampleFound: true
13649
- };
13650
- } else {
13651
- let currentFragment = locationInFragment.fragment;
13652
- while (currentFragment.nextFragment) {
13653
- currentFragment = currentFragment.nextFragment;
13654
- const trackData2 = currentFragment.trackData.get(this.internalTrack.id);
13655
- if (trackData2) {
13656
- const fragmentIndex2 = binarySearchExact(
13657
- this.internalTrack.fragments,
13658
- currentFragment.moofOffset,
13659
- (x) => x.moofOffset
13660
- );
13661
- assert(fragmentIndex2 !== -1);
13662
- return {
13663
- fragmentIndex: fragmentIndex2,
13664
- sampleIndex: 0,
13665
- correctSampleFound: true
13666
- };
13667
- }
13668
- }
13669
- return {
13670
- fragmentIndex,
13671
- sampleIndex: -1,
13672
- correctSampleFound: false
13673
- };
13674
- }
13675
- },
13676
- fragmentSample.presentationTimestamp,
13677
- Infinity,
13678
- options
13679
- );
13655
+ const regularSampleIndex = this.packetToSampleIndex.get(packet);
13656
+ if (regularSampleIndex !== void 0) {
13657
+ return this.fetchPacketForSampleIndex(regularSampleIndex + 1, options);
13680
13658
  }
13681
- const sampleIndex = this.packetToSampleIndex.get(packet);
13682
- if (sampleIndex === void 0) {
13659
+ const locationInFragment = this.packetToFragmentLocation.get(packet);
13660
+ if (locationInFragment === void 0) {
13683
13661
  throw new Error("Packet was not created from this track.");
13684
13662
  }
13685
- return this.fetchPacketForSampleIndex(sampleIndex + 1, options);
13663
+ const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id);
13664
+ const fragmentIndex = binarySearchExact(
13665
+ this.internalTrack.fragments,
13666
+ locationInFragment.fragment.moofOffset,
13667
+ (x) => x.moofOffset
13668
+ );
13669
+ assert(fragmentIndex !== -1);
13670
+ return this.performFragmentedLookup(
13671
+ () => {
13672
+ if (locationInFragment.sampleIndex + 1 < trackData.samples.length) {
13673
+ return {
13674
+ fragmentIndex,
13675
+ sampleIndex: locationInFragment.sampleIndex + 1,
13676
+ correctSampleFound: true
13677
+ };
13678
+ } else {
13679
+ let currentFragment = locationInFragment.fragment;
13680
+ while (currentFragment.nextFragment) {
13681
+ currentFragment = currentFragment.nextFragment;
13682
+ const trackData2 = currentFragment.trackData.get(this.internalTrack.id);
13683
+ if (trackData2) {
13684
+ const fragmentIndex2 = binarySearchExact(
13685
+ this.internalTrack.fragments,
13686
+ currentFragment.moofOffset,
13687
+ (x) => x.moofOffset
13688
+ );
13689
+ assert(fragmentIndex2 !== -1);
13690
+ return {
13691
+ fragmentIndex: fragmentIndex2,
13692
+ sampleIndex: 0,
13693
+ correctSampleFound: true
13694
+ };
13695
+ }
13696
+ }
13697
+ return {
13698
+ fragmentIndex,
13699
+ sampleIndex: -1,
13700
+ correctSampleFound: false
13701
+ };
13702
+ }
13703
+ },
13704
+ -Infinity,
13705
+ // Use -Infinity as a search timestamp to avoid using the lookup entries
13706
+ Infinity,
13707
+ options
13708
+ );
13686
13709
  }
13687
13710
  async getKeyPacket(timestamp, options) {
13688
13711
  const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
13689
- if (this.internalTrack.demuxer.isFragmented) {
13690
- return this.performFragmentedLookup(
13691
- () => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
13692
- timestampInTimescale,
13693
- timestampInTimescale,
13694
- options
13695
- );
13696
- }
13697
13712
  const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
13698
13713
  const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
13699
13714
  const keyFrameSampleIndex = sampleIndex === -1 ? -1 : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
13700
- return this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
13715
+ const regularPacket = await this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
13716
+ if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
13717
+ return regularPacket;
13718
+ }
13719
+ return this.performFragmentedLookup(
13720
+ () => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
13721
+ timestampInTimescale,
13722
+ timestampInTimescale,
13723
+ options
13724
+ );
13701
13725
  }
13702
13726
  async getNextKeyPacket(packet, options) {
13703
- if (this.internalTrack.demuxer.isFragmented) {
13704
- const locationInFragment = this.packetToFragmentLocation.get(packet);
13705
- if (locationInFragment === void 0) {
13706
- throw new Error("Packet was not created from this track.");
13707
- }
13708
- const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id);
13709
- const fragmentSample = trackData.samples[locationInFragment.sampleIndex];
13710
- const fragmentIndex = binarySearchExact(
13711
- this.internalTrack.fragments,
13712
- locationInFragment.fragment.moofOffset,
13713
- (x) => x.moofOffset
13714
- );
13715
- assert(fragmentIndex !== -1);
13716
- return this.performFragmentedLookup(
13717
- () => {
13718
- const nextKeyFrameIndex = trackData.samples.findIndex(
13719
- (x, i) => x.isKeyFrame && i > locationInFragment.sampleIndex
13720
- );
13721
- if (nextKeyFrameIndex !== -1) {
13722
- return {
13723
- fragmentIndex,
13724
- sampleIndex: nextKeyFrameIndex,
13725
- correctSampleFound: true
13726
- };
13727
- } else {
13728
- let currentFragment = locationInFragment.fragment;
13729
- while (currentFragment.nextFragment) {
13730
- currentFragment = currentFragment.nextFragment;
13731
- const trackData2 = currentFragment.trackData.get(this.internalTrack.id);
13732
- if (trackData2 && trackData2.firstKeyFrameTimestamp !== null) {
13733
- const fragmentIndex2 = binarySearchExact(
13734
- this.internalTrack.fragments,
13735
- currentFragment.moofOffset,
13736
- (x) => x.moofOffset
13737
- );
13738
- assert(fragmentIndex2 !== -1);
13739
- const keyFrameIndex = trackData2.samples.findIndex((x) => x.isKeyFrame);
13740
- assert(keyFrameIndex !== -1);
13741
- return {
13742
- fragmentIndex: fragmentIndex2,
13743
- sampleIndex: keyFrameIndex,
13744
- correctSampleFound: true
13745
- };
13746
- }
13747
- }
13748
- return {
13749
- fragmentIndex,
13750
- sampleIndex: -1,
13751
- correctSampleFound: false
13752
- };
13753
- }
13754
- },
13755
- fragmentSample.presentationTimestamp,
13756
- Infinity,
13757
- options
13758
- );
13727
+ const regularSampleIndex = this.packetToSampleIndex.get(packet);
13728
+ if (regularSampleIndex !== void 0) {
13729
+ const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
13730
+ const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
13731
+ return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
13759
13732
  }
13760
- const sampleIndex = this.packetToSampleIndex.get(packet);
13761
- if (sampleIndex === void 0) {
13733
+ const locationInFragment = this.packetToFragmentLocation.get(packet);
13734
+ if (locationInFragment === void 0) {
13762
13735
  throw new Error("Packet was not created from this track.");
13763
13736
  }
13764
- const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
13765
- const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
13766
- return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
13737
+ const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id);
13738
+ const fragmentIndex = binarySearchExact(
13739
+ this.internalTrack.fragments,
13740
+ locationInFragment.fragment.moofOffset,
13741
+ (x) => x.moofOffset
13742
+ );
13743
+ assert(fragmentIndex !== -1);
13744
+ return this.performFragmentedLookup(
13745
+ () => {
13746
+ const nextKeyFrameIndex = trackData.samples.findIndex(
13747
+ (x, i) => x.isKeyFrame && i > locationInFragment.sampleIndex
13748
+ );
13749
+ if (nextKeyFrameIndex !== -1) {
13750
+ return {
13751
+ fragmentIndex,
13752
+ sampleIndex: nextKeyFrameIndex,
13753
+ correctSampleFound: true
13754
+ };
13755
+ } else {
13756
+ let currentFragment = locationInFragment.fragment;
13757
+ while (currentFragment.nextFragment) {
13758
+ currentFragment = currentFragment.nextFragment;
13759
+ const trackData2 = currentFragment.trackData.get(this.internalTrack.id);
13760
+ if (trackData2 && trackData2.firstKeyFrameTimestamp !== null) {
13761
+ const fragmentIndex2 = binarySearchExact(
13762
+ this.internalTrack.fragments,
13763
+ currentFragment.moofOffset,
13764
+ (x) => x.moofOffset
13765
+ );
13766
+ assert(fragmentIndex2 !== -1);
13767
+ const keyFrameIndex = trackData2.samples.findIndex((x) => x.isKeyFrame);
13768
+ assert(keyFrameIndex !== -1);
13769
+ return {
13770
+ fragmentIndex: fragmentIndex2,
13771
+ sampleIndex: keyFrameIndex,
13772
+ correctSampleFound: true
13773
+ };
13774
+ }
13775
+ }
13776
+ return {
13777
+ fragmentIndex,
13778
+ sampleIndex: -1,
13779
+ correctSampleFound: false
13780
+ };
13781
+ }
13782
+ },
13783
+ -Infinity,
13784
+ // Use -Infinity as a search timestamp to avoid using the lookup entries
13785
+ Infinity,
13786
+ options
13787
+ );
13767
13788
  }
13768
13789
  async fetchPacketForSampleIndex(sampleIndex, options) {
13769
13790
  if (sampleIndex === -1) {
@@ -14159,6 +14180,9 @@ ${cue.notes ?? ""}`;
14159
14180
  const sinTheta = m21 / scaleX;
14160
14181
  return -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI);
14161
14182
  };
14183
+ var sampleTableIsEmpty = (sampleTable) => {
14184
+ return sampleTable.sampleSizes.length === 0;
14185
+ };
14162
14186
 
14163
14187
  // src/matroska/matroska-demuxer.ts
14164
14188
  var METADATA_ELEMENTS = [
@@ -14402,12 +14426,15 @@ ${cue.notes ?? ""}`;
14402
14426
  this.currentCluster = cluster;
14403
14427
  this.readContiguousElements(this.clusterReader, size);
14404
14428
  for (const [trackId, trackData] of cluster.trackData) {
14405
- let blockReferencesExist = false;
14429
+ const track = segment.tracks.find((x) => x.id === trackId) ?? null;
14406
14430
  assert(trackData.blocks.length > 0);
14431
+ let blockReferencesExist = false;
14432
+ let hasLacedBlocks = false;
14407
14433
  for (let i = 0; i < trackData.blocks.length; i++) {
14408
14434
  const block = trackData.blocks[i];
14409
14435
  block.timestamp += cluster.timestamp;
14410
14436
  blockReferencesExist ||= block.referencedTimestamps.length > 0;
14437
+ hasLacedBlocks ||= block.lacing !== 0 /* None */;
14411
14438
  }
14412
14439
  if (blockReferencesExist) {
14413
14440
  trackData.blocks = sortBlocksByReferences(trackData.blocks);
@@ -14422,13 +14449,23 @@ ${cue.notes ?? ""}`;
14422
14449
  if (i < trackData.presentationTimestamps.length - 1) {
14423
14450
  const nextEntry = trackData.presentationTimestamps[i + 1];
14424
14451
  currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp;
14452
+ } else if (currentBlock.duration === 0) {
14453
+ if (track?.defaultDuration != null) {
14454
+ if (currentBlock.lacing === 0 /* None */) {
14455
+ currentBlock.duration = track.defaultDuration;
14456
+ } else {
14457
+ }
14458
+ }
14425
14459
  }
14426
14460
  }
14461
+ if (hasLacedBlocks) {
14462
+ this.expandLacedBlocks(trackData.blocks, track);
14463
+ trackData.presentationTimestamps = trackData.blocks.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i })).sort((a, b) => a.timestamp - b.timestamp);
14464
+ }
14427
14465
  const firstBlock = trackData.blocks[trackData.presentationTimestamps[0].blockIndex];
14428
14466
  const lastBlock = trackData.blocks[last(trackData.presentationTimestamps).blockIndex];
14429
14467
  trackData.startTimestamp = firstBlock.timestamp;
14430
14468
  trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration;
14431
- const track = segment.tracks.find((x) => x.id === trackId);
14432
14469
  if (track) {
14433
14470
  const insertionIndex2 = binarySearchLessOrEqual(
14434
14471
  track.clusters,
@@ -14470,6 +14507,95 @@ ${cue.notes ?? ""}`;
14470
14507
  }
14471
14508
  return trackData;
14472
14509
  }
14510
+ expandLacedBlocks(blocks, track) {
14511
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
14512
+ const originalBlock = blocks[blockIndex];
14513
+ if (originalBlock.lacing === 0 /* None */) {
14514
+ continue;
14515
+ }
14516
+ const data = originalBlock.data;
14517
+ let pos = 0;
14518
+ const frameSizes = [];
14519
+ const frameCount = data[pos] + 1;
14520
+ pos++;
14521
+ switch (originalBlock.lacing) {
14522
+ case 1 /* Xiph */:
14523
+ {
14524
+ let totalUsedSize = 0;
14525
+ for (let i = 0; i < frameCount - 1; i++) {
14526
+ let frameSize = 0;
14527
+ while (pos < data.length) {
14528
+ const value = data[pos];
14529
+ frameSize += value;
14530
+ pos++;
14531
+ if (value < 255) {
14532
+ frameSizes.push(frameSize);
14533
+ totalUsedSize += frameSize;
14534
+ break;
14535
+ }
14536
+ }
14537
+ }
14538
+ frameSizes.push(data.length - (pos + totalUsedSize));
14539
+ }
14540
+ ;
14541
+ break;
14542
+ case 2 /* FixedSize */:
14543
+ {
14544
+ const totalDataSize = data.length - 1;
14545
+ const frameSize = Math.floor(totalDataSize / frameCount);
14546
+ for (let i = 0; i < frameCount; i++) {
14547
+ frameSizes.push(frameSize);
14548
+ }
14549
+ }
14550
+ ;
14551
+ break;
14552
+ case 3 /* Ebml */:
14553
+ {
14554
+ const firstResult = readVarInt(data, pos);
14555
+ let currentSize = firstResult.value;
14556
+ frameSizes.push(currentSize);
14557
+ pos += firstResult.width;
14558
+ let totalUsedSize = currentSize;
14559
+ for (let i = 1; i < frameCount - 1; i++) {
14560
+ const diffResult = readVarInt(data, pos);
14561
+ const unsignedDiff = diffResult.value;
14562
+ const bias = (1 << diffResult.width * 7 - 1) - 1;
14563
+ const diff = unsignedDiff - bias;
14564
+ currentSize += diff;
14565
+ frameSizes.push(currentSize);
14566
+ pos += diffResult.width;
14567
+ totalUsedSize += currentSize;
14568
+ }
14569
+ frameSizes.push(data.length - (pos + totalUsedSize));
14570
+ }
14571
+ ;
14572
+ break;
14573
+ default:
14574
+ assert(false);
14575
+ }
14576
+ assert(frameSizes.length === frameCount);
14577
+ blocks.splice(blockIndex, 1);
14578
+ let dataOffset = pos;
14579
+ for (let i = 0; i < frameCount; i++) {
14580
+ const frameSize = frameSizes[i];
14581
+ const frameData = data.subarray(dataOffset, dataOffset + frameSize);
14582
+ const blockDuration = originalBlock.duration || frameCount * (track?.defaultDuration ?? 0);
14583
+ const frameTimestamp = originalBlock.timestamp + blockDuration * i / frameCount;
14584
+ const frameDuration = blockDuration / frameCount;
14585
+ blocks.splice(blockIndex + i, 0, {
14586
+ timestamp: frameTimestamp,
14587
+ duration: frameDuration,
14588
+ isKeyFrame: originalBlock.isKeyFrame,
14589
+ referencedTimestamps: originalBlock.referencedTimestamps,
14590
+ data: frameData,
14591
+ lacing: 0 /* None */
14592
+ });
14593
+ dataOffset += frameSize;
14594
+ }
14595
+ blockIndex += frameCount;
14596
+ blockIndex--;
14597
+ }
14598
+ }
14473
14599
  readContiguousElements(reader, totalSize) {
14474
14600
  const startIndex = reader.pos;
14475
14601
  while (reader.pos - startIndex <= totalSize - MIN_HEADER_SIZE) {
@@ -14544,6 +14670,7 @@ ${cue.notes ?? ""}`;
14544
14670
  inputTrack: null,
14545
14671
  codecId: null,
14546
14672
  codecPrivate: null,
14673
+ defaultDuration: null,
14547
14674
  languageCode: UNDETERMINED_LANGUAGE,
14548
14675
  info: null
14549
14676
  };
@@ -14691,6 +14818,13 @@ ${cue.notes ?? ""}`;
14691
14818
  }
14692
14819
  ;
14693
14820
  break;
14821
+ case 2352003 /* DefaultDuration */:
14822
+ {
14823
+ if (!this.currentTrack) break;
14824
+ this.currentTrack.defaultDuration = this.currentTrack.segment.timestampFactor * reader.readUnsignedInt(size) / 1e9;
14825
+ }
14826
+ ;
14827
+ break;
14694
14828
  case 2274716 /* Language */:
14695
14829
  {
14696
14830
  if (!this.currentTrack) break;
@@ -14869,14 +15003,17 @@ ${cue.notes ?? ""}`;
14869
15003
  const relativeTimestamp = reader.readS16();
14870
15004
  const flags = reader.readU8();
14871
15005
  const isKeyFrame = !!(flags & 128);
15006
+ const lacing = flags >> 1 & 3;
14872
15007
  const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
14873
15008
  trackData.blocks.push({
14874
15009
  timestamp: relativeTimestamp,
14875
15010
  // We'll add the cluster's timestamp to this later
14876
15011
  duration: 0,
15012
+ // Will set later
14877
15013
  isKeyFrame,
14878
15014
  referencedTimestamps: [],
14879
- data: reader.readBytes(size - (reader.pos - dataStartPos))
15015
+ data: reader.readBytes(size - (reader.pos - dataStartPos)),
15016
+ lacing
14880
15017
  });
14881
15018
  }
14882
15019
  ;
@@ -14900,14 +15037,17 @@ ${cue.notes ?? ""}`;
14900
15037
  const trackNumber = reader.readVarInt();
14901
15038
  const relativeTimestamp = reader.readS16();
14902
15039
  const flags = reader.readU8();
15040
+ const lacing = flags >> 1 & 3;
14903
15041
  const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber);
14904
15042
  this.currentBlock = {
14905
15043
  timestamp: relativeTimestamp,
14906
15044
  // We'll add the cluster's timestamp to this later
14907
15045
  duration: 0,
15046
+ // Will set later
14908
15047
  isKeyFrame: true,
14909
15048
  referencedTimestamps: [],
14910
- data: reader.readBytes(size - (reader.pos - dataStartPos))
15049
+ data: reader.readBytes(size - (reader.pos - dataStartPos)),
15050
+ lacing
14911
15051
  };
14912
15052
  trackData.blocks.push(this.currentBlock);
14913
15053
  }
@@ -15010,7 +15150,6 @@ ${cue.notes ?? ""}`;
15010
15150
  throw new Error("Packet was not created from this track.");
15011
15151
  }
15012
15152
  const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id);
15013
- const block = trackData.blocks[locationInCluster.blockIndex];
15014
15153
  const clusterIndex = binarySearchExact(
15015
15154
  this.internalTrack.clusters,
15016
15155
  locationInCluster.cluster.elementStartPos,
@@ -15051,7 +15190,8 @@ ${cue.notes ?? ""}`;
15051
15190
  };
15052
15191
  }
15053
15192
  },
15054
- block.timestamp,
15193
+ -Infinity,
15194
+ // Use -Infinity as a search timestamp to avoid using the cues
15055
15195
  Infinity,
15056
15196
  options
15057
15197
  );
@@ -15071,7 +15211,6 @@ ${cue.notes ?? ""}`;
15071
15211
  throw new Error("Packet was not created from this track.");
15072
15212
  }
15073
15213
  const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id);
15074
- const block = trackData.blocks[locationInCluster.blockIndex];
15075
15214
  const clusterIndex = binarySearchExact(
15076
15215
  this.internalTrack.clusters,
15077
15216
  locationInCluster.cluster.elementStartPos,
@@ -15117,7 +15256,8 @@ ${cue.notes ?? ""}`;
15117
15256
  };
15118
15257
  }
15119
15258
  },
15120
- block.timestamp,
15259
+ -Infinity,
15260
+ // Use -Infinity as a search timestamp to avoid using the cues
15121
15261
  Infinity,
15122
15262
  options
15123
15263
  );
@@ -16774,8 +16914,11 @@ ${cue.notes ?? ""}`;
16774
16914
  if (options.video?.rotate !== void 0 && ![0, 90, 180, 270].includes(options.video.rotate)) {
16775
16915
  throw new TypeError("options.video.rotate, when provided, must be 0, 90, 180 or 270.");
16776
16916
  }
16917
+ if (options.video?.frameRate !== void 0 && (!Number.isFinite(options.video.frameRate) || options.video.frameRate <= 0)) {
16918
+ throw new TypeError("options.video.frameRate, when provided, must be a finite positive number.");
16919
+ }
16777
16920
  if (options.audio !== void 0 && (!options.audio || typeof options.audio !== "object")) {
16778
- throw new TypeError("options.video, when provided, must be an object.");
16921
+ throw new TypeError("options.audio, when provided, must be an object.");
16779
16922
  }
16780
16923
  if (options.audio?.discard !== void 0 && typeof options.audio.discard !== "boolean") {
16781
16924
  throw new TypeError("options.audio.discard, when provided, must be a boolean.");
@@ -16942,7 +17085,7 @@ ${cue.notes ?? ""}`;
16942
17085
  height = ceilToMultipleOfTwo(this._options.video.height);
16943
17086
  }
16944
17087
  const firstTimestamp = await track.getFirstTimestamp();
16945
- const needsTranscode = !!this._options.video?.forceTranscode || this._startTimestamp > 0 || firstTimestamp < 0;
17088
+ const needsTranscode = !!this._options.video?.forceTranscode || this._startTimestamp > 0 || firstTimestamp < 0 || !!this._options.video?.frameRate;
16946
17089
  const needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !outputSupportsRotation;
16947
17090
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
16948
17091
  if (!needsTranscode && !this._options.video?.bitrate && !needsRerender && videoCodecs.includes(sourceCodec) && (!this._options.video?.codec || this._options.video?.codec === sourceCodec)) {
@@ -16993,9 +17136,9 @@ ${cue.notes ?? ""}`;
16993
17136
  bitrate,
16994
17137
  onEncodedPacket: (sample) => this._reportProgress(track.id, sample.timestamp + sample.duration)
16995
17138
  };
17139
+ const source = new VideoSampleSource(encodingConfig);
17140
+ videoSource = source;
16996
17141
  if (needsRerender) {
16997
- const source = new VideoSampleSource(encodingConfig);
16998
- videoSource = source;
16999
17142
  this._trackPromises.push((async () => {
17000
17143
  await this._started;
17001
17144
  const sink = new CanvasSink(track, {
@@ -17007,6 +17150,22 @@ ${cue.notes ?? ""}`;
17007
17150
  poolSize: 1
17008
17151
  });
17009
17152
  const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
17153
+ const frameRate = this._options.video?.frameRate;
17154
+ let lastCanvas = null;
17155
+ let lastCanvasTimestamp = null;
17156
+ let lastCanvasEndTimestamp = null;
17157
+ const padFrames = async (until) => {
17158
+ assert(lastCanvas);
17159
+ assert(frameRate !== void 0);
17160
+ const frameDifference = Math.round((until - lastCanvasTimestamp) * frameRate);
17161
+ for (let i = 1; i < frameDifference; i++) {
17162
+ const sample = new VideoSample(lastCanvas, {
17163
+ timestamp: lastCanvasTimestamp + i / frameRate,
17164
+ duration: 1 / frameRate
17165
+ });
17166
+ await source.add(sample);
17167
+ }
17168
+ };
17010
17169
  for await (const { canvas, timestamp, duration } of iterator) {
17011
17170
  if (this._synchronizer.shouldWait(track.id, timestamp)) {
17012
17171
  await this._synchronizer.wait(timestamp);
@@ -17014,30 +17173,98 @@ ${cue.notes ?? ""}`;
17014
17173
  if (this._canceled) {
17015
17174
  return;
17016
17175
  }
17176
+ let adjustedSampleTimestamp = Math.max(timestamp - this._startTimestamp, 0);
17177
+ lastCanvasEndTimestamp = timestamp + duration;
17178
+ if (frameRate !== void 0) {
17179
+ const alignedTimestamp = Math.floor(adjustedSampleTimestamp * frameRate) / frameRate;
17180
+ if (lastCanvas !== null) {
17181
+ if (alignedTimestamp <= lastCanvasTimestamp) {
17182
+ lastCanvas = canvas;
17183
+ lastCanvasTimestamp = alignedTimestamp;
17184
+ continue;
17185
+ } else {
17186
+ await padFrames(alignedTimestamp);
17187
+ }
17188
+ }
17189
+ adjustedSampleTimestamp = alignedTimestamp;
17190
+ }
17017
17191
  const sample = new VideoSample(canvas, {
17018
- timestamp: Math.max(timestamp - this._startTimestamp, 0),
17019
- duration
17192
+ timestamp: adjustedSampleTimestamp,
17193
+ duration: frameRate !== void 0 ? 1 / frameRate : duration
17020
17194
  });
17021
17195
  await source.add(sample);
17022
- sample.close();
17196
+ if (frameRate !== void 0) {
17197
+ lastCanvas = canvas;
17198
+ lastCanvasTimestamp = adjustedSampleTimestamp;
17199
+ } else {
17200
+ sample.close();
17201
+ }
17202
+ }
17203
+ if (lastCanvas) {
17204
+ assert(lastCanvasEndTimestamp !== null);
17205
+ assert(frameRate !== void 0);
17206
+ await padFrames(Math.floor(lastCanvasEndTimestamp * frameRate) / frameRate);
17023
17207
  }
17208
+ source.close();
17209
+ this._synchronizer.closeTrack(track.id);
17024
17210
  })());
17025
17211
  } else {
17026
- const source = new VideoSampleSource(encodingConfig);
17027
- videoSource = source;
17028
17212
  this._trackPromises.push((async () => {
17029
17213
  await this._started;
17030
17214
  const sink = new VideoSampleSink(track);
17215
+ const frameRate = this._options.video?.frameRate;
17216
+ let lastSample = null;
17217
+ let lastSampleTimestamp = null;
17218
+ let lastSampleEndTimestamp = null;
17219
+ const padFrames = async (until) => {
17220
+ assert(lastSample);
17221
+ assert(frameRate !== void 0);
17222
+ const frameDifference = Math.round((until - lastSampleTimestamp) * frameRate);
17223
+ for (let i = 1; i < frameDifference; i++) {
17224
+ lastSample.setTimestamp(lastSampleTimestamp + i / frameRate);
17225
+ lastSample.setDuration(1 / frameRate);
17226
+ await source.add(lastSample);
17227
+ }
17228
+ lastSample.close();
17229
+ };
17031
17230
  for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
17032
17231
  if (this._synchronizer.shouldWait(track.id, sample.timestamp)) {
17033
17232
  await this._synchronizer.wait(sample.timestamp);
17034
17233
  }
17035
- sample.setTimestamp(Math.max(sample.timestamp - this._startTimestamp, 0));
17036
17234
  if (this._canceled) {
17235
+ lastSample?.close();
17037
17236
  return;
17038
17237
  }
17238
+ let adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
17239
+ lastSampleEndTimestamp = sample.timestamp + sample.duration;
17240
+ if (frameRate !== void 0) {
17241
+ const alignedTimestamp = Math.floor(adjustedSampleTimestamp * frameRate) / frameRate;
17242
+ if (lastSample !== null) {
17243
+ if (alignedTimestamp <= lastSampleTimestamp) {
17244
+ lastSample.close();
17245
+ lastSample = sample;
17246
+ lastSampleTimestamp = alignedTimestamp;
17247
+ continue;
17248
+ } else {
17249
+ await padFrames(alignedTimestamp);
17250
+ }
17251
+ }
17252
+ adjustedSampleTimestamp = alignedTimestamp;
17253
+ sample.setDuration(1 / frameRate);
17254
+ }
17255
+ sample.setTimestamp(adjustedSampleTimestamp);
17039
17256
  await source.add(sample);
17040
- sample.close();
17257
+ if (frameRate !== void 0) {
17258
+ lastSample = sample;
17259
+ lastSampleTimestamp = adjustedSampleTimestamp;
17260
+ } else {
17261
+ sample.close();
17262
+ }
17263
+ }
17264
+ if (lastSample) {
17265
+ assert(lastSampleEndTimestamp !== null);
17266
+ assert(frameRate !== void 0);
17267
+ await padFrames(Math.floor(lastSampleEndTimestamp * frameRate) / frameRate);
17041
17268
  }
17042
17269
  source.close();
17043
17270
  this._synchronizer.closeTrack(track.id);
@@ -17045,6 +17272,7 @@ ${cue.notes ?? ""}`;
17045
17272
  }
17046
17273
  }
17047
17274
  this.output.addVideoTrack(videoSource, {
17275
+ frameRate: this._options.video?.frameRate,
17048
17276
  languageCode: track.languageCode,
17049
17277
  rotation: needsRerender ? 0 : totalRotation
17050
17278
  // Rerendering will bake the rotation into the output