mediabunny 1.6.2 → 1.7.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 (41) hide show
  1. package/dist/bundles/mediabunny.cjs +278 -193
  2. package/dist/bundles/mediabunny.min.cjs +5 -5
  3. package/dist/bundles/mediabunny.min.mjs +5 -5
  4. package/dist/bundles/mediabunny.mjs +278 -193
  5. package/dist/mediabunny.d.ts +88 -55
  6. package/dist/modules/src/conversion.d.ts +77 -57
  7. package/dist/modules/src/conversion.d.ts.map +1 -1
  8. package/dist/modules/src/conversion.js +139 -100
  9. package/dist/modules/src/index.d.ts +2 -2
  10. package/dist/modules/src/index.d.ts.map +1 -1
  11. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  12. package/dist/modules/src/isobmff/isobmff-demuxer.js +4 -7
  13. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  14. package/dist/modules/src/matroska/matroska-demuxer.js +4 -7
  15. package/dist/modules/src/media-sink.d.ts.map +1 -1
  16. package/dist/modules/src/media-sink.js +42 -21
  17. package/dist/modules/src/media-source.d.ts.map +1 -1
  18. package/dist/modules/src/media-source.js +2 -0
  19. package/dist/modules/src/misc.d.ts +8 -0
  20. package/dist/modules/src/misc.d.ts.map +1 -1
  21. package/dist/modules/src/misc.js +18 -0
  22. package/dist/modules/src/mp3/mp3-demuxer.d.ts +9 -1
  23. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  24. package/dist/modules/src/mp3/mp3-demuxer.js +106 -49
  25. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  26. package/dist/modules/src/mp3/mp3-muxer.js +5 -2
  27. package/dist/modules/src/output-format.d.ts +5 -0
  28. package/dist/modules/src/output-format.d.ts.map +1 -1
  29. package/dist/modules/src/output-format.js +3 -0
  30. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/src/conversion.ts +243 -180
  33. package/src/index.ts +2 -2
  34. package/src/isobmff/isobmff-demuxer.ts +5 -15
  35. package/src/matroska/matroska-demuxer.ts +4 -19
  36. package/src/media-sink.ts +50 -28
  37. package/src/media-source.ts +4 -0
  38. package/src/misc.ts +30 -0
  39. package/src/mp3/mp3-demuxer.ts +123 -54
  40. package/src/mp3/mp3-muxer.ts +7 -2
  41. package/src/output-format.ts +9 -0
@@ -344,6 +344,10 @@ var Mediabunny = (() => {
344
344
  }
345
345
  return ans;
346
346
  };
347
+ var insertSorted = (arr, item, valueGetter) => {
348
+ const insertionIndex = binarySearchLessOrEqual(arr, valueGetter(item), valueGetter);
349
+ arr.splice(insertionIndex + 1, 0, item);
350
+ };
347
351
  var promiseWithResolvers = () => {
348
352
  let resolve;
349
353
  let reject;
@@ -550,6 +554,15 @@ var Mediabunny = (() => {
550
554
  return this.currentPromise = this.currentPromise.then(fn);
551
555
  }
552
556
  };
557
+ var isSafariCache = null;
558
+ var isSafari = () => {
559
+ if (isSafariCache !== null) {
560
+ return isSafariCache;
561
+ }
562
+ const result = !!(typeof navigator !== "undefined" && navigator.vendor?.match(/apple/i) && !navigator.userAgent?.match(/crios/i) && !navigator.userAgent?.match(/fxios/i) && !navigator.userAgent?.match(/Opera|OPT\//));
563
+ isSafariCache = result;
564
+ return result;
565
+ };
553
566
 
554
567
  // src/custom-coder.ts
555
568
  var CustomVideoDecoder = class {
@@ -6637,7 +6650,8 @@ ${cue.notes ?? ""}`;
6637
6650
  async addEncodedAudioPacket(track, packet) {
6638
6651
  const release = await this.mutex.acquire();
6639
6652
  try {
6640
- if (!this.xingFrameData) {
6653
+ const writeXingHeader = this.format._options.xingHeader !== false;
6654
+ if (!this.xingFrameData && writeXingHeader) {
6641
6655
  const view2 = toDataView(packet.data);
6642
6656
  if (view2.byteLength < 4) {
6643
6657
  throw new Error("Invalid MP3 header in sample.");
@@ -6672,10 +6686,12 @@ ${cue.notes ?? ""}`;
6672
6686
  this.frameCount++;
6673
6687
  }
6674
6688
  this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === "key");
6675
- this.framePositions.push(this.writer.getPos());
6676
6689
  this.writer.write(packet.data);
6677
6690
  this.frameCount++;
6678
6691
  await this.writer.flush();
6692
+ if (writeXingHeader) {
6693
+ this.framePositions.push(this.writer.getPos());
6694
+ }
6679
6695
  } finally {
6680
6696
  release();
6681
6697
  }
@@ -8745,6 +8761,7 @@ ${cue.notes ?? ""}`;
8745
8761
  return decodedSampleQueueSize === 0 ? 40 : 8;
8746
8762
  };
8747
8763
  var VideoDecoderWrapper = class extends DecoderWrapper {
8764
+ // Safari-specific thing, check usage.
8748
8765
  constructor(onSample, onError, codec, decoderConfig, rotation, timeResolution) {
8749
8766
  super(onSample, onError);
8750
8767
  this.rotation = rotation;
@@ -8753,21 +8770,9 @@ ${cue.notes ?? ""}`;
8753
8770
  this.customDecoder = null;
8754
8771
  this.customDecoderCallSerializer = new CallSerializer();
8755
8772
  this.customDecoderQueueSize = 0;
8773
+ this.inputTimestamps = [];
8774
+ // Timestamps input into the decoder, sorted.
8756
8775
  this.sampleQueue = [];
8757
- const sampleHandler = (sample) => {
8758
- if (this.sampleQueue.length > 0 && sample.timestamp >= last(this.sampleQueue).timestamp) {
8759
- for (const sample2 of this.sampleQueue) {
8760
- this.finalizeAndEmitSample(sample2);
8761
- }
8762
- this.sampleQueue.length = 0;
8763
- }
8764
- const insertionIndex = binarySearchLessOrEqual(
8765
- this.sampleQueue,
8766
- sample.timestamp,
8767
- (x) => x.timestamp
8768
- );
8769
- this.sampleQueue.splice(insertionIndex + 1, 0, sample);
8770
- };
8771
8776
  const MatchingCustomDecoder = customVideoDecoders.find((x) => x.supports(codec, decoderConfig));
8772
8777
  if (MatchingCustomDecoder) {
8773
8778
  this.customDecoder = new MatchingCustomDecoder();
@@ -8777,10 +8782,26 @@ ${cue.notes ?? ""}`;
8777
8782
  if (!(sample instanceof VideoSample)) {
8778
8783
  throw new TypeError("The argument passed to onSample must be a VideoSample.");
8779
8784
  }
8780
- sampleHandler(sample);
8785
+ this.finalizeAndEmitSample(sample);
8781
8786
  };
8782
8787
  void this.customDecoderCallSerializer.call(() => this.customDecoder.init());
8783
8788
  } else {
8789
+ const sampleHandler = (sample) => {
8790
+ if (isSafari()) {
8791
+ if (this.sampleQueue.length > 0 && sample.timestamp >= last(this.sampleQueue).timestamp) {
8792
+ for (const sample2 of this.sampleQueue) {
8793
+ this.finalizeAndEmitSample(sample2);
8794
+ }
8795
+ this.sampleQueue.length = 0;
8796
+ }
8797
+ insertSorted(this.sampleQueue, sample, (x) => x.timestamp);
8798
+ } else {
8799
+ const timestamp = this.inputTimestamps.shift();
8800
+ assert(timestamp !== void 0);
8801
+ sample.setTimestamp(timestamp);
8802
+ this.finalizeAndEmitSample(sample);
8803
+ }
8804
+ };
8784
8805
  this.decoder = new VideoDecoder({
8785
8806
  output: (frame) => sampleHandler(new VideoSample(frame)),
8786
8807
  error: onError
@@ -8808,6 +8829,9 @@ ${cue.notes ?? ""}`;
8808
8829
  void this.customDecoderCallSerializer.call(() => this.customDecoder.decode(packet)).then(() => this.customDecoderQueueSize--);
8809
8830
  } else {
8810
8831
  assert(this.decoder);
8832
+ if (!isSafari()) {
8833
+ insertSorted(this.inputTimestamps, packet.timestamp, (x) => x);
8834
+ }
8811
8835
  this.decoder.decode(packet.toEncodedVideoChunk());
8812
8836
  }
8813
8837
  }
@@ -8818,10 +8842,12 @@ ${cue.notes ?? ""}`;
8818
8842
  assert(this.decoder);
8819
8843
  await this.decoder.flush();
8820
8844
  }
8821
- for (const sample of this.sampleQueue) {
8822
- this.finalizeAndEmitSample(sample);
8845
+ if (isSafari()) {
8846
+ for (const sample of this.sampleQueue) {
8847
+ this.finalizeAndEmitSample(sample);
8848
+ }
8849
+ this.sampleQueue.length = 0;
8823
8850
  }
8824
- this.sampleQueue.length = 0;
8825
8851
  }
8826
8852
  close() {
8827
8853
  if (this.customDecoder) {
@@ -10420,6 +10446,9 @@ ${cue.notes ?? ""}`;
10420
10446
  if (!options || typeof options !== "object") {
10421
10447
  throw new TypeError("options must be an object.");
10422
10448
  }
10449
+ if (options.xingHeader !== void 0 && typeof options.xingHeader !== "boolean") {
10450
+ throw new TypeError("options.xingHeader, when provided, must be a boolean.");
10451
+ }
10423
10452
  if (options.onXingFrame !== void 0 && typeof options.onXingFrame !== "function") {
10424
10453
  throw new TypeError("options.onXingFrame, when provided, must be a function.");
10425
10454
  }
@@ -11567,6 +11596,7 @@ ${cue.notes ?? ""}`;
11567
11596
  this._promiseWithResolvers.reject(error);
11568
11597
  });
11569
11598
  } else {
11599
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
11570
11600
  this._audioContext = new AudioContext({ sampleRate: this._track.getSettings().sampleRate });
11571
11601
  const sourceNode = this._audioContext.createMediaStreamSource(new MediaStream([this._track]));
11572
11602
  this._scriptProcessorNode = this._audioContext.createScriptProcessor(4096);
@@ -13365,12 +13395,7 @@ ${cue.notes ?? ""}`;
13365
13395
  isKnownToBeFirstFragment: false
13366
13396
  };
13367
13397
  this.readContiguousBoxes(boxInfo.contentSize);
13368
- const insertionIndex = binarySearchLessOrEqual(
13369
- this.fragments,
13370
- this.currentFragment.moofOffset,
13371
- (x) => x.moofOffset
13372
- );
13373
- this.fragments.splice(insertionIndex + 1, 0, this.currentFragment);
13398
+ insertSorted(this.fragments, this.currentFragment, (x) => x.moofOffset);
13374
13399
  for (const [, trackData] of this.currentFragment.trackData) {
13375
13400
  const firstSample = trackData.samples[0];
13376
13401
  const lastSample = last(trackData.samples);
@@ -13394,20 +13419,14 @@ ${cue.notes ?? ""}`;
13394
13419
  if (this.currentTrack) {
13395
13420
  const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
13396
13421
  if (trackData) {
13397
- const insertionIndex = binarySearchLessOrEqual(
13398
- this.currentTrack.fragments,
13399
- this.currentFragment.moofOffset,
13400
- (x) => x.moofOffset
13401
- );
13402
- this.currentTrack.fragments.splice(insertionIndex + 1, 0, this.currentFragment);
13422
+ insertSorted(this.currentTrack.fragments, this.currentFragment, (x) => x.moofOffset);
13403
13423
  const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
13404
13424
  if (hasKeyFrame) {
13405
- const insertionIndex2 = binarySearchLessOrEqual(
13425
+ insertSorted(
13406
13426
  this.currentTrack.fragmentsWithKeyFrame,
13407
- this.currentFragment.moofOffset,
13427
+ this.currentFragment,
13408
13428
  (x) => x.moofOffset
13409
13429
  );
13410
- this.currentTrack.fragmentsWithKeyFrame.splice(insertionIndex2 + 1, 0, this.currentFragment);
13411
13430
  }
13412
13431
  const { currentFragmentState } = this.currentTrack;
13413
13432
  assert(currentFragmentState);
@@ -14496,29 +14515,14 @@ ${cue.notes ?? ""}`;
14496
14515
  trackData.startTimestamp = firstBlock.timestamp;
14497
14516
  trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration;
14498
14517
  if (track) {
14499
- const insertionIndex2 = binarySearchLessOrEqual(
14500
- track.clusters,
14501
- cluster.elementStartPos,
14502
- (x) => x.elementStartPos
14503
- );
14504
- track.clusters.splice(insertionIndex2 + 1, 0, cluster);
14518
+ insertSorted(track.clusters, cluster, (x) => x.elementStartPos);
14505
14519
  const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
14506
14520
  if (hasKeyFrame) {
14507
- const insertionIndex3 = binarySearchLessOrEqual(
14508
- track.clustersWithKeyFrame,
14509
- cluster.elementStartPos,
14510
- (x) => x.elementStartPos
14511
- );
14512
- track.clustersWithKeyFrame.splice(insertionIndex3 + 1, 0, cluster);
14521
+ insertSorted(track.clustersWithKeyFrame, cluster, (x) => x.elementStartPos);
14513
14522
  }
14514
14523
  }
14515
14524
  }
14516
- const insertionIndex = binarySearchLessOrEqual(
14517
- segment.clusters,
14518
- elementStartPos,
14519
- (x) => x.elementStartPos
14520
- );
14521
- segment.clusters.splice(insertionIndex + 1, 0, cluster);
14525
+ insertSorted(segment.clusters, cluster, (x) => x.elementStartPos);
14522
14526
  this.currentCluster = null;
14523
14527
  return cluster;
14524
14528
  }
@@ -15672,45 +15676,21 @@ ${cue.notes ?? ""}`;
15672
15676
  super(input);
15673
15677
  this.metadataPromise = null;
15674
15678
  this.firstFrameHeader = null;
15675
- this.allSamples = [];
15679
+ this.loadedSamples = [];
15680
+ // All samples from the start of the file to lastLoadedPos
15676
15681
  this.tracks = [];
15682
+ this.loadingMutex = new AsyncMutex();
15683
+ this.lastLoadedPos = 0;
15684
+ this.fileSize = 0;
15685
+ this.nextTimestampInSamples = 0;
15677
15686
  this.reader = new Mp3Reader(input._mainReader);
15678
15687
  }
15679
15688
  async readMetadata() {
15680
15689
  return this.metadataPromise ??= (async () => {
15681
- const fileSize = await this.input.source.getSize();
15682
- this.reader.fileSize = fileSize;
15683
- await this.reader.reader.loadRange(0, fileSize);
15684
- const id3Tag = this.reader.readId3();
15685
- if (id3Tag) {
15686
- this.reader.pos += id3Tag.size;
15687
- }
15688
- let nextTimestampInSamples = 0;
15689
- while (true) {
15690
- const header = this.reader.readNextFrameHeader();
15691
- if (!header) {
15692
- break;
15693
- }
15694
- const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
15695
- this.reader.pos = header.startPos + xingOffset;
15696
- const word = this.reader.readU32();
15697
- const isXing = word === XING || word === INFO;
15698
- this.reader.pos = header.startPos + header.totalSize - 1;
15699
- if (isXing) {
15700
- continue;
15701
- }
15702
- if (!this.firstFrameHeader) {
15703
- this.firstFrameHeader = header;
15704
- }
15705
- const sampleDuration = header.audioSamplesInFrame / header.sampleRate;
15706
- const sample = {
15707
- timestamp: nextTimestampInSamples / header.sampleRate,
15708
- duration: sampleDuration,
15709
- dataStart: header.startPos,
15710
- dataSize: header.totalSize
15711
- };
15712
- this.allSamples.push(sample);
15713
- nextTimestampInSamples += header.audioSamplesInFrame;
15690
+ this.fileSize = await this.input.source.getSize();
15691
+ this.reader.fileSize = this.fileSize;
15692
+ while (!this.firstFrameHeader && this.lastLoadedPos < this.fileSize) {
15693
+ await this.loadNextChunk();
15714
15694
  }
15715
15695
  if (!this.firstFrameHeader) {
15716
15696
  throw new Error("No MP3 frames found.");
@@ -15718,6 +15698,61 @@ ${cue.notes ?? ""}`;
15718
15698
  this.tracks = [new InputAudioTrack(new Mp3AudioTrackBacking(this))];
15719
15699
  })();
15720
15700
  }
15701
+ /** Loads the next 0.5 MiB of frames. */
15702
+ async loadNextChunk() {
15703
+ const release = await this.loadingMutex.acquire();
15704
+ try {
15705
+ assert(this.lastLoadedPos < this.fileSize);
15706
+ const chunkSize = 0.5 * 1024 * 1024;
15707
+ const endPos = Math.min(this.lastLoadedPos + chunkSize, this.fileSize);
15708
+ await this.reader.reader.loadRange(this.lastLoadedPos, endPos);
15709
+ this.lastLoadedPos = endPos;
15710
+ assert(this.lastLoadedPos <= this.fileSize);
15711
+ if (this.reader.pos === 0) {
15712
+ const id3Tag = this.reader.readId3();
15713
+ if (id3Tag) {
15714
+ this.reader.pos += id3Tag.size;
15715
+ }
15716
+ }
15717
+ this.parseFramesFromLoadedData();
15718
+ } finally {
15719
+ release();
15720
+ }
15721
+ }
15722
+ parseFramesFromLoadedData() {
15723
+ while (true) {
15724
+ const startPos = this.reader.pos;
15725
+ const header = this.reader.readNextFrameHeader();
15726
+ if (!header) {
15727
+ break;
15728
+ }
15729
+ if (header.startPos + header.totalSize > this.lastLoadedPos) {
15730
+ this.reader.pos = startPos;
15731
+ this.lastLoadedPos = startPos;
15732
+ break;
15733
+ }
15734
+ const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
15735
+ this.reader.pos = header.startPos + xingOffset;
15736
+ const word = this.reader.readU32();
15737
+ const isXing = word === XING || word === INFO;
15738
+ this.reader.pos = header.startPos + header.totalSize - 1;
15739
+ if (isXing) {
15740
+ continue;
15741
+ }
15742
+ if (!this.firstFrameHeader) {
15743
+ this.firstFrameHeader = header;
15744
+ }
15745
+ const sampleDuration = header.audioSamplesInFrame / header.sampleRate;
15746
+ const sample = {
15747
+ timestamp: this.nextTimestampInSamples / header.sampleRate,
15748
+ duration: sampleDuration,
15749
+ dataStart: header.startPos,
15750
+ dataSize: header.totalSize
15751
+ };
15752
+ this.loadedSamples.push(sample);
15753
+ this.nextTimestampInSamples += header.audioSamplesInFrame;
15754
+ }
15755
+ }
15721
15756
  async getMimeType() {
15722
15757
  return "audio/mpeg";
15723
15758
  }
@@ -15727,9 +15762,9 @@ ${cue.notes ?? ""}`;
15727
15762
  }
15728
15763
  async computeDuration() {
15729
15764
  await this.readMetadata();
15730
- const lastSample = last(this.allSamples);
15731
- assert(lastSample);
15732
- return lastSample.timestamp + lastSample.duration;
15765
+ const track = this.tracks[0];
15766
+ assert(track);
15767
+ return track.computeDuration();
15733
15768
  }
15734
15769
  };
15735
15770
  var Mp3AudioTrackBacking = class {
@@ -15746,8 +15781,9 @@ ${cue.notes ?? ""}`;
15746
15781
  assert(this.demuxer.firstFrameHeader);
15747
15782
  return this.demuxer.firstFrameHeader.sampleRate / this.demuxer.firstFrameHeader.audioSamplesInFrame;
15748
15783
  }
15749
- computeDuration() {
15750
- return this.demuxer.computeDuration();
15784
+ async computeDuration() {
15785
+ const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
15786
+ return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
15751
15787
  }
15752
15788
  getLanguageCode() {
15753
15789
  return UNDETERMINED_LANGUAGE;
@@ -15775,7 +15811,7 @@ ${cue.notes ?? ""}`;
15775
15811
  if (sampleIndex === -1) {
15776
15812
  return null;
15777
15813
  }
15778
- const rawSample = this.demuxer.allSamples[sampleIndex];
15814
+ const rawSample = this.demuxer.loadedSamples[sampleIndex];
15779
15815
  if (!rawSample) {
15780
15816
  return null;
15781
15817
  }
@@ -15796,26 +15832,44 @@ ${cue.notes ?? ""}`;
15796
15832
  );
15797
15833
  }
15798
15834
  async getFirstPacket(options) {
15835
+ while (this.demuxer.loadedSamples.length === 0 && this.demuxer.lastLoadedPos < this.demuxer.fileSize) {
15836
+ await this.demuxer.loadNextChunk();
15837
+ }
15799
15838
  return this.getPacketAtIndex(0, options);
15800
15839
  }
15801
15840
  async getNextPacket(packet, options) {
15802
15841
  const sampleIndex = binarySearchExact(
15803
- this.demuxer.allSamples,
15842
+ this.demuxer.loadedSamples,
15804
15843
  packet.timestamp,
15805
15844
  (x) => x.timestamp
15806
15845
  );
15807
15846
  if (sampleIndex === -1) {
15808
15847
  throw new Error("Packet was not created from this track.");
15809
15848
  }
15810
- return this.getPacketAtIndex(sampleIndex + 1, options);
15849
+ const nextIndex = sampleIndex + 1;
15850
+ while (nextIndex >= this.demuxer.loadedSamples.length && this.demuxer.lastLoadedPos < this.demuxer.fileSize) {
15851
+ await this.demuxer.loadNextChunk();
15852
+ }
15853
+ return this.getPacketAtIndex(nextIndex, options);
15811
15854
  }
15812
15855
  async getPacket(timestamp, options) {
15813
- const index = binarySearchLessOrEqual(
15814
- this.demuxer.allSamples,
15815
- timestamp,
15816
- (x) => x.timestamp
15817
- );
15818
- return this.getPacketAtIndex(index, options);
15856
+ while (true) {
15857
+ const index = binarySearchLessOrEqual(
15858
+ this.demuxer.loadedSamples,
15859
+ timestamp,
15860
+ (x) => x.timestamp
15861
+ );
15862
+ if (index === -1 && this.demuxer.loadedSamples.length > 0) {
15863
+ return null;
15864
+ }
15865
+ if (this.demuxer.lastLoadedPos === this.demuxer.fileSize) {
15866
+ return this.getPacketAtIndex(index, options);
15867
+ }
15868
+ if (index >= 0 && index + 1 < this.demuxer.loadedSamples.length) {
15869
+ return this.getPacketAtIndex(index, options);
15870
+ }
15871
+ await this.demuxer.loadNextChunk();
15872
+ }
15819
15873
  }
15820
15874
  getKeyPacket(timestamp, options) {
15821
15875
  return this.getPacket(timestamp, options);
@@ -16856,6 +16910,70 @@ ${cue.notes ?? ""}`;
16856
16910
  };
16857
16911
 
16858
16912
  // src/conversion.ts
16913
+ var validateVideoOptions = (videoOptions) => {
16914
+ if (videoOptions !== void 0 && (!videoOptions || typeof videoOptions !== "object")) {
16915
+ throw new TypeError("options.video, when provided, must be an object.");
16916
+ }
16917
+ if (videoOptions?.discard !== void 0 && typeof videoOptions.discard !== "boolean") {
16918
+ throw new TypeError("options.video.discard, when provided, must be a boolean.");
16919
+ }
16920
+ if (videoOptions?.forceTranscode !== void 0 && typeof videoOptions.forceTranscode !== "boolean") {
16921
+ throw new TypeError("options.video.forceTranscode, when provided, must be a boolean.");
16922
+ }
16923
+ if (videoOptions?.codec !== void 0 && !VIDEO_CODECS.includes(videoOptions.codec)) {
16924
+ throw new TypeError(
16925
+ `options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(", ")}.`
16926
+ );
16927
+ }
16928
+ if (videoOptions?.bitrate !== void 0 && !(videoOptions.bitrate instanceof Quality) && (!Number.isInteger(videoOptions.bitrate) || videoOptions.bitrate <= 0)) {
16929
+ throw new TypeError("options.video.bitrate, when provided, must be a positive integer or a quality.");
16930
+ }
16931
+ if (videoOptions?.width !== void 0 && (!Number.isInteger(videoOptions.width) || videoOptions.width <= 0)) {
16932
+ throw new TypeError("options.video.width, when provided, must be a positive integer.");
16933
+ }
16934
+ if (videoOptions?.height !== void 0 && (!Number.isInteger(videoOptions.height) || videoOptions.height <= 0)) {
16935
+ throw new TypeError("options.video.height, when provided, must be a positive integer.");
16936
+ }
16937
+ if (videoOptions?.fit !== void 0 && !["fill", "contain", "cover"].includes(videoOptions.fit)) {
16938
+ throw new TypeError('options.video.fit, when provided, must be one of "fill", "contain", or "cover".');
16939
+ }
16940
+ if (videoOptions?.width !== void 0 && videoOptions.height !== void 0 && videoOptions.fit === void 0) {
16941
+ throw new TypeError(
16942
+ "When both options.video.width and options.video.height are provided, options.video.fit must also be provided."
16943
+ );
16944
+ }
16945
+ if (videoOptions?.rotate !== void 0 && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
16946
+ throw new TypeError("options.video.rotate, when provided, must be 0, 90, 180 or 270.");
16947
+ }
16948
+ if (videoOptions?.frameRate !== void 0 && (!Number.isFinite(videoOptions.frameRate) || videoOptions.frameRate <= 0)) {
16949
+ throw new TypeError("options.video.frameRate, when provided, must be a finite positive number.");
16950
+ }
16951
+ };
16952
+ var validateAudioOptions = (audioOptions) => {
16953
+ if (audioOptions !== void 0 && (!audioOptions || typeof audioOptions !== "object")) {
16954
+ throw new TypeError("options.audio, when provided, must be an object.");
16955
+ }
16956
+ if (audioOptions?.discard !== void 0 && typeof audioOptions.discard !== "boolean") {
16957
+ throw new TypeError("options.audio.discard, when provided, must be a boolean.");
16958
+ }
16959
+ if (audioOptions?.forceTranscode !== void 0 && typeof audioOptions.forceTranscode !== "boolean") {
16960
+ throw new TypeError("options.audio.forceTranscode, when provided, must be a boolean.");
16961
+ }
16962
+ if (audioOptions?.codec !== void 0 && !AUDIO_CODECS.includes(audioOptions.codec)) {
16963
+ throw new TypeError(
16964
+ `options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(", ")}.`
16965
+ );
16966
+ }
16967
+ if (audioOptions?.bitrate !== void 0 && !(audioOptions.bitrate instanceof Quality) && (!Number.isInteger(audioOptions.bitrate) || audioOptions.bitrate <= 0)) {
16968
+ throw new TypeError("options.audio.bitrate, when provided, must be a positive integer or a quality.");
16969
+ }
16970
+ if (audioOptions?.numberOfChannels !== void 0 && (!Number.isInteger(audioOptions.numberOfChannels) || audioOptions.numberOfChannels <= 0)) {
16971
+ throw new TypeError("options.audio.numberOfChannels, when provided, must be a positive integer.");
16972
+ }
16973
+ if (audioOptions?.sampleRate !== void 0 && (!Number.isInteger(audioOptions.sampleRate) || audioOptions.sampleRate <= 0)) {
16974
+ throw new TypeError("options.audio.sampleRate, when provided, must be a positive integer.");
16975
+ }
16976
+ };
16859
16977
  var FALLBACK_NUMBER_OF_CHANNELS = 2;
16860
16978
  var FALLBACK_SAMPLE_RATE = 48e3;
16861
16979
  var Conversion = class _Conversion {
@@ -16909,65 +17027,13 @@ ${cue.notes ?? ""}`;
16909
17027
  if (options.output._tracks.length > 0 || options.output.state !== "pending") {
16910
17028
  throw new TypeError("options.output must be fresh: no tracks added and not started.");
16911
17029
  }
16912
- if (options.video !== void 0 && (!options.video || typeof options.video !== "object")) {
16913
- throw new TypeError("options.video, when provided, must be an object.");
16914
- }
16915
- if (options.video?.discard !== void 0 && typeof options.video.discard !== "boolean") {
16916
- throw new TypeError("options.video.discard, when provided, must be a boolean.");
16917
- }
16918
- if (options.video?.forceTranscode !== void 0 && typeof options.video.forceTranscode !== "boolean") {
16919
- throw new TypeError("options.video.forceTranscode, when provided, must be a boolean.");
16920
- }
16921
- if (options.video?.codec !== void 0 && !VIDEO_CODECS.includes(options.video.codec)) {
16922
- throw new TypeError(
16923
- `options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(", ")}.`
16924
- );
16925
- }
16926
- if (options.video?.bitrate !== void 0 && !(options.video.bitrate instanceof Quality) && (!Number.isInteger(options.video.bitrate) || options.video.bitrate <= 0)) {
16927
- throw new TypeError("options.video.bitrate, when provided, must be a positive integer or a quality.");
16928
- }
16929
- if (options.video?.width !== void 0 && (!Number.isInteger(options.video.width) || options.video.width <= 0)) {
16930
- throw new TypeError("options.video.width, when provided, must be a positive integer.");
16931
- }
16932
- if (options.video?.height !== void 0 && (!Number.isInteger(options.video.height) || options.video.height <= 0)) {
16933
- throw new TypeError("options.video.height, when provided, must be a positive integer.");
16934
- }
16935
- if (options.video?.fit !== void 0 && !["fill", "contain", "cover"].includes(options.video.fit)) {
16936
- throw new TypeError('options.video.fit, when provided, must be one of "fill", "contain", or "cover".');
16937
- }
16938
- if (options.video?.width !== void 0 && options.video.height !== void 0 && options.video.fit === void 0) {
16939
- throw new TypeError(
16940
- "When both options.video.width and options.video.height are provided, options.video.fit must also be provided."
16941
- );
16942
- }
16943
- if (options.video?.rotate !== void 0 && ![0, 90, 180, 270].includes(options.video.rotate)) {
16944
- throw new TypeError("options.video.rotate, when provided, must be 0, 90, 180 or 270.");
16945
- }
16946
- if (options.video?.frameRate !== void 0 && (!Number.isFinite(options.video.frameRate) || options.video.frameRate <= 0)) {
16947
- throw new TypeError("options.video.frameRate, when provided, must be a finite positive number.");
16948
- }
16949
- if (options.audio !== void 0 && (!options.audio || typeof options.audio !== "object")) {
16950
- throw new TypeError("options.audio, when provided, must be an object.");
16951
- }
16952
- if (options.audio?.discard !== void 0 && typeof options.audio.discard !== "boolean") {
16953
- throw new TypeError("options.audio.discard, when provided, must be a boolean.");
16954
- }
16955
- if (options.audio?.forceTranscode !== void 0 && typeof options.audio.forceTranscode !== "boolean") {
16956
- throw new TypeError("options.audio.forceTranscode, when provided, must be a boolean.");
16957
- }
16958
- if (options.audio?.codec !== void 0 && !AUDIO_CODECS.includes(options.audio.codec)) {
16959
- throw new TypeError(
16960
- `options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(", ")}.`
16961
- );
16962
- }
16963
- if (options.audio?.bitrate !== void 0 && !(options.audio.bitrate instanceof Quality) && (!Number.isInteger(options.audio.bitrate) || options.audio.bitrate <= 0)) {
16964
- throw new TypeError("options.audio.bitrate, when provided, must be a positive integer or a quality.");
16965
- }
16966
- if (options.audio?.numberOfChannels !== void 0 && (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0)) {
16967
- throw new TypeError("options.audio.numberOfChannels, when provided, must be a positive integer.");
17030
+ if (typeof options.video !== "function") {
17031
+ validateVideoOptions(options.video);
17032
+ } else {
16968
17033
  }
16969
- if (options.audio?.sampleRate !== void 0 && (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0)) {
16970
- throw new TypeError("options.audio.sampleRate, when provided, must be a positive integer.");
17034
+ if (typeof options.audio !== "function") {
17035
+ validateAudioOptions(options.audio);
17036
+ } else {
16971
17037
  }
16972
17038
  if (options.trim !== void 0 && (!options.trim || typeof options.trim !== "object")) {
16973
17039
  throw new TypeError("options.trim, when provided, must be an object.");
@@ -17000,15 +17066,34 @@ ${cue.notes ?? ""}`;
17000
17066
  async _init() {
17001
17067
  const inputTracks = await this.input.getTracks();
17002
17068
  const outputTrackCounts = this.output.format.getSupportedTrackCounts();
17069
+ let nVideo = 1;
17070
+ let nAudio = 1;
17003
17071
  for (const track of inputTracks) {
17004
- if (track.isVideoTrack() && this._options.video?.discard) {
17005
- this.discardedTracks.push({
17006
- track,
17007
- reason: "discarded_by_user"
17008
- });
17009
- continue;
17072
+ let trackOptions = void 0;
17073
+ if (track.isVideoTrack()) {
17074
+ if (this._options.video) {
17075
+ if (typeof this._options.video === "function") {
17076
+ trackOptions = await this._options.video(track, nVideo);
17077
+ validateVideoOptions(trackOptions);
17078
+ nVideo++;
17079
+ } else {
17080
+ trackOptions = this._options.video;
17081
+ }
17082
+ }
17083
+ } else if (track.isAudioTrack()) {
17084
+ if (this._options.audio) {
17085
+ if (typeof this._options.audio === "function") {
17086
+ trackOptions = await this._options.audio(track, nAudio);
17087
+ validateAudioOptions(trackOptions);
17088
+ nAudio++;
17089
+ } else {
17090
+ trackOptions = this._options.audio;
17091
+ }
17092
+ }
17093
+ } else {
17094
+ assert(false);
17010
17095
  }
17011
- if (track.isAudioTrack() && this._options.audio?.discard) {
17096
+ if (trackOptions?.discard) {
17012
17097
  this.discardedTracks.push({
17013
17098
  track,
17014
17099
  reason: "discarded_by_user"
@@ -17030,9 +17115,9 @@ ${cue.notes ?? ""}`;
17030
17115
  continue;
17031
17116
  }
17032
17117
  if (track.isVideoTrack()) {
17033
- await this._processVideoTrack(track);
17118
+ await this._processVideoTrack(track, trackOptions ?? {});
17034
17119
  } else if (track.isAudioTrack()) {
17035
- await this._processAudioTrack(track);
17120
+ await this._processAudioTrack(track, trackOptions ?? {});
17036
17121
  }
17037
17122
  }
17038
17123
  const unintentionallyDiscardedTracks = this.discardedTracks.filter((x) => x.reason !== "discarded_by_user");
@@ -17086,7 +17171,7 @@ ${cue.notes ?? ""}`;
17086
17171
  await this.output.cancel();
17087
17172
  }
17088
17173
  /** @internal */
17089
- async _processVideoTrack(track) {
17174
+ async _processVideoTrack(track, trackOptions) {
17090
17175
  const sourceCodec = track.codec;
17091
17176
  if (!sourceCodec) {
17092
17177
  this.discardedTracks.push({
@@ -17096,28 +17181,28 @@ ${cue.notes ?? ""}`;
17096
17181
  return;
17097
17182
  }
17098
17183
  let videoSource;
17099
- const totalRotation = normalizeRotation(track.rotation + (this._options.video?.rotate ?? 0));
17184
+ const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
17100
17185
  const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
17101
17186
  const [originalWidth, originalHeight] = totalRotation % 180 === 0 ? [track.codedWidth, track.codedHeight] : [track.codedHeight, track.codedWidth];
17102
17187
  let width = originalWidth;
17103
17188
  let height = originalHeight;
17104
17189
  const aspectRatio = width / height;
17105
17190
  const ceilToMultipleOfTwo = (value) => Math.ceil(value / 2) * 2;
17106
- if (this._options.video?.width !== void 0 && this._options.video.height === void 0) {
17107
- width = ceilToMultipleOfTwo(this._options.video.width);
17191
+ if (trackOptions.width !== void 0 && trackOptions.height === void 0) {
17192
+ width = ceilToMultipleOfTwo(trackOptions.width);
17108
17193
  height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
17109
- } else if (this._options.video?.width === void 0 && this._options.video?.height !== void 0) {
17110
- height = ceilToMultipleOfTwo(this._options.video.height);
17194
+ } else if (trackOptions.width === void 0 && trackOptions.height !== void 0) {
17195
+ height = ceilToMultipleOfTwo(trackOptions.height);
17111
17196
  width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
17112
- } else if (this._options.video?.width !== void 0 && this._options.video.height !== void 0) {
17113
- width = ceilToMultipleOfTwo(this._options.video.width);
17114
- height = ceilToMultipleOfTwo(this._options.video.height);
17197
+ } else if (trackOptions.width !== void 0 && trackOptions.height !== void 0) {
17198
+ width = ceilToMultipleOfTwo(trackOptions.width);
17199
+ height = ceilToMultipleOfTwo(trackOptions.height);
17115
17200
  }
17116
17201
  const firstTimestamp = await track.getFirstTimestamp();
17117
- const needsTranscode = !!this._options.video?.forceTranscode || this._startTimestamp > 0 || firstTimestamp < 0 || !!this._options.video?.frameRate;
17202
+ const needsTranscode = !!trackOptions.forceTranscode || this._startTimestamp > 0 || firstTimestamp < 0 || !!trackOptions.frameRate;
17118
17203
  const needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !outputSupportsRotation;
17119
17204
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
17120
- if (!needsTranscode && !this._options.video?.bitrate && !needsRerender && videoCodecs.includes(sourceCodec) && (!this._options.video?.codec || this._options.video?.codec === sourceCodec)) {
17205
+ if (!needsTranscode && !trackOptions.bitrate && !needsRerender && videoCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec)) {
17121
17206
  const source = new EncodedVideoPacketSource(sourceCodec);
17122
17207
  videoSource = source;
17123
17208
  this._trackPromises.push((async () => {
@@ -17148,10 +17233,10 @@ ${cue.notes ?? ""}`;
17148
17233
  });
17149
17234
  return;
17150
17235
  }
17151
- if (this._options.video?.codec) {
17152
- videoCodecs = videoCodecs.filter((codec) => codec === this._options.video?.codec);
17236
+ if (trackOptions.codec) {
17237
+ videoCodecs = videoCodecs.filter((codec) => codec === trackOptions.codec);
17153
17238
  }
17154
- const bitrate = this._options.video?.bitrate ?? QUALITY_HIGH;
17239
+ const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
17155
17240
  const encodableCodec = await getFirstEncodableVideoCodec(videoCodecs, { width, height, bitrate });
17156
17241
  if (!encodableCodec) {
17157
17242
  this.discardedTracks.push({
@@ -17173,13 +17258,13 @@ ${cue.notes ?? ""}`;
17173
17258
  const sink = new CanvasSink(track, {
17174
17259
  width,
17175
17260
  height,
17176
- fit: this._options.video?.fit ?? "fill",
17261
+ fit: trackOptions.fit ?? "fill",
17177
17262
  rotation: totalRotation,
17178
17263
  // Bake the rotation into the output
17179
17264
  poolSize: 1
17180
17265
  });
17181
17266
  const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
17182
- const frameRate = this._options.video?.frameRate;
17267
+ const frameRate = trackOptions.frameRate;
17183
17268
  let lastCanvas = null;
17184
17269
  let lastCanvasTimestamp = null;
17185
17270
  let lastCanvasEndTimestamp = null;
@@ -17241,7 +17326,7 @@ ${cue.notes ?? ""}`;
17241
17326
  this._trackPromises.push((async () => {
17242
17327
  await this._started;
17243
17328
  const sink = new VideoSampleSink(track);
17244
- const frameRate = this._options.video?.frameRate;
17329
+ const frameRate = trackOptions.frameRate;
17245
17330
  let lastSample = null;
17246
17331
  let lastSampleTimestamp = null;
17247
17332
  let lastSampleEndTimestamp = null;
@@ -17301,7 +17386,7 @@ ${cue.notes ?? ""}`;
17301
17386
  }
17302
17387
  }
17303
17388
  this.output.addVideoTrack(videoSource, {
17304
- frameRate: this._options.video?.frameRate,
17389
+ frameRate: trackOptions.frameRate,
17305
17390
  languageCode: track.languageCode,
17306
17391
  rotation: needsRerender ? 0 : totalRotation
17307
17392
  // Rerendering will bake the rotation into the output
@@ -17311,7 +17396,7 @@ ${cue.notes ?? ""}`;
17311
17396
  this.utilizedTracks.push(track);
17312
17397
  }
17313
17398
  /** @internal */
17314
- async _processAudioTrack(track) {
17399
+ async _processAudioTrack(track, trackOptions) {
17315
17400
  const sourceCodec = track.codec;
17316
17401
  if (!sourceCodec) {
17317
17402
  this.discardedTracks.push({
@@ -17324,11 +17409,11 @@ ${cue.notes ?? ""}`;
17324
17409
  const originalNumberOfChannels = track.numberOfChannels;
17325
17410
  const originalSampleRate = track.sampleRate;
17326
17411
  const firstTimestamp = await track.getFirstTimestamp();
17327
- let numberOfChannels = this._options.audio?.numberOfChannels ?? originalNumberOfChannels;
17328
- let sampleRate = this._options.audio?.sampleRate ?? originalSampleRate;
17412
+ let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
17413
+ let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
17329
17414
  let needsResample = numberOfChannels !== originalNumberOfChannels || sampleRate !== originalSampleRate || this._startTimestamp > 0 || firstTimestamp < 0;
17330
17415
  let audioCodecs = this.output.format.getSupportedAudioCodecs();
17331
- if (!this._options.audio?.forceTranscode && !this._options.audio?.bitrate && !needsResample && audioCodecs.includes(sourceCodec) && (!this._options.audio?.codec || this._options.audio.codec === sourceCodec)) {
17416
+ if (!trackOptions.forceTranscode && !trackOptions.bitrate && !needsResample && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec)) {
17332
17417
  const source = new EncodedAudioPacketSource(sourceCodec);
17333
17418
  audioSource = source;
17334
17419
  this._trackPromises.push((async () => {
@@ -17360,10 +17445,10 @@ ${cue.notes ?? ""}`;
17360
17445
  return;
17361
17446
  }
17362
17447
  let codecOfChoice = null;
17363
- if (this._options.audio?.codec) {
17364
- audioCodecs = audioCodecs.filter((codec) => codec === this._options.audio.codec);
17448
+ if (trackOptions.codec) {
17449
+ audioCodecs = audioCodecs.filter((codec) => codec === trackOptions.codec);
17365
17450
  }
17366
- const bitrate = this._options.audio?.bitrate ?? QUALITY_HIGH;
17451
+ const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
17367
17452
  const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
17368
17453
  numberOfChannels,
17369
17454
  sampleRate,