mediabunny 1.50.8 → 1.51.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 (39) hide show
  1. package/dist/bundles/mediabunny.cjs +428 -201
  2. package/dist/bundles/mediabunny.min.cjs +12 -12
  3. package/dist/bundles/mediabunny.min.mjs +12 -12
  4. package/dist/bundles/mediabunny.mjs +428 -201
  5. package/dist/bundles/mediabunny.node.cjs +428 -201
  6. package/dist/mediabunny.d.ts +57 -4
  7. package/dist/modules/src/codec-data.d.ts.map +1 -1
  8. package/dist/modules/src/codec-data.js +15 -1
  9. package/dist/modules/src/conversion.d.ts +47 -4
  10. package/dist/modules/src/conversion.d.ts.map +1 -1
  11. package/dist/modules/src/conversion.js +420 -188
  12. package/dist/modules/src/hls/hls-muxer.js +5 -5
  13. package/dist/modules/src/index.d.ts +1 -1
  14. package/dist/modules/src/index.d.ts.map +1 -1
  15. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  16. package/dist/modules/src/isobmff/isobmff-muxer.js +4 -3
  17. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  18. package/dist/modules/src/matroska/matroska-muxer.js +4 -2
  19. package/dist/modules/src/mpeg-ts/mpeg-ts-muxer.js +1 -1
  20. package/dist/modules/src/ogg/ogg-muxer.js +1 -1
  21. package/dist/modules/src/output-format.js +1 -1
  22. package/dist/modules/src/output.d.ts +9 -0
  23. package/dist/modules/src/output.d.ts.map +1 -1
  24. package/dist/modules/src/output.js +34 -13
  25. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  26. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  27. package/dist/modules/src/wave/wave-demuxer.js +17 -1
  28. package/package.json +5 -5
  29. package/src/codec-data.ts +14 -1
  30. package/src/conversion.ts +347 -139
  31. package/src/hls/hls-muxer.ts +5 -5
  32. package/src/index.ts +1 -0
  33. package/src/isobmff/isobmff-muxer.ts +5 -3
  34. package/src/matroska/matroska-muxer.ts +5 -2
  35. package/src/mpeg-ts/mpeg-ts-muxer.ts +1 -1
  36. package/src/ogg/ogg-muxer.ts +1 -1
  37. package/src/output-format.ts +1 -1
  38. package/src/output.ts +41 -13
  39. package/src/wave/wave-demuxer.ts +25 -1
@@ -4215,8 +4215,17 @@ var Mediabunny = (() => {
4215
4215
  ];
4216
4216
  var parseOpusTocByte = (packet) => {
4217
4217
  const config = packet[0] >> 3;
4218
+ const code = packet[0] & 3;
4219
+ let frameCount;
4220
+ if (code === 0) {
4221
+ frameCount = 1;
4222
+ } else if (code === 1 || code === 2) {
4223
+ frameCount = 2;
4224
+ } else {
4225
+ frameCount = packet[1] & 63;
4226
+ }
4218
4227
  return {
4219
- durationInSamples: OPUS_FRAME_DURATION_TABLE[config]
4228
+ durationInSamples: OPUS_FRAME_DURATION_TABLE[config] * frameCount
4220
4229
  };
4221
4230
  };
4222
4231
  var parseModesFromVorbisSetupPacket = (setupHeader) => {
@@ -12379,6 +12388,21 @@ var Mediabunny = (() => {
12379
12388
  if (formatTag === 7 /* MULAW */ || formatTag === 6 /* ALAW */) {
12380
12389
  bitsPerSample = 8;
12381
12390
  }
12391
+ if (formatTag !== 1 /* PCM */ && formatTag !== 3 /* IEEE_FLOAT */ && formatTag !== 6 /* ALAW */ && formatTag !== 7 /* MULAW */) {
12392
+ throw new Error(
12393
+ `Unsupported WAVE codec (format tag ${formatTag}). Only integer/float PCM, A-law, and \u03BC-law are supported.`
12394
+ );
12395
+ }
12396
+ if (formatTag === 1 /* PCM */ && ![8, 16, 24, 32].includes(bitsPerSample)) {
12397
+ throw new Error(
12398
+ `Unsupported WAVE PCM bit depth (${bitsPerSample}). Only 8, 16, 24, and 32 bits are supported.`
12399
+ );
12400
+ }
12401
+ if (formatTag === 3 /* IEEE_FLOAT */ && ![32, 64].includes(bitsPerSample)) {
12402
+ throw new Error(
12403
+ `Unsupported WAVE float bit depth (${bitsPerSample}). Only 32 and 64 bits are supported.`
12404
+ );
12405
+ }
12382
12406
  this.audioInfo = {
12383
12407
  format: formatTag,
12384
12408
  numberOfChannels: numChannels,
@@ -12527,9 +12551,11 @@ var Mediabunny = (() => {
12527
12551
  if (this.audioInfo.format === 3 /* IEEE_FLOAT */) {
12528
12552
  if (this.audioInfo.sampleSizeInBytes === 4) {
12529
12553
  return "pcm-f32";
12554
+ } else if (this.audioInfo.sampleSizeInBytes === 8) {
12555
+ return "pcm-f64";
12530
12556
  }
12531
12557
  }
12532
- return null;
12558
+ assert(false);
12533
12559
  }
12534
12560
  async getMimeType() {
12535
12561
  return "audio/wav";
@@ -28949,6 +28975,7 @@ var Mediabunny = (() => {
28949
28975
  this.isQuickTime = format instanceof MovOutputFormat;
28950
28976
  this.isCmaf = format instanceof CmafOutputFormat;
28951
28977
  this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
28978
+ this.auxWriter.start();
28952
28979
  }
28953
28980
  async start() {
28954
28981
  const release = await this.mutex.acquire();
@@ -28973,7 +29000,7 @@ var Mediabunny = (() => {
28973
29000
  this.initWriter = initWriter;
28974
29001
  this.initBoxWriter = new IsobmffBoxWriter(initWriter);
28975
29002
  }
28976
- const holdsAvc = this.output._tracks.some((x) => x.isVideoTrack() && x.source._codec === "avc");
29003
+ const holdsAvc = this.output.tracks.some((x) => x.isVideoTrack() && x.source._codec === "avc");
28977
29004
  {
28978
29005
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
28979
29006
  assert(boxWriter);
@@ -28997,7 +29024,7 @@ var Mediabunny = (() => {
28997
29024
  }
28998
29025
  if (this.fastStart === "in-memory") {
28999
29026
  } else if (this.fastStart === "reserve") {
29000
- for (const track of this.output._tracks) {
29027
+ for (const track of this.output.tracks) {
29001
29028
  if (track.metadata.maximumPacketCount === void 0) {
29002
29029
  throw new Error(
29003
29030
  "All tracks must specify maximumPacketCount in their metadata when using fastStart: 'reserve'."
@@ -29018,7 +29045,7 @@ var Mediabunny = (() => {
29018
29045
  release();
29019
29046
  }
29020
29047
  allTracksAreKnown() {
29021
- for (const track of this.output._tracks) {
29048
+ for (const track of this.output.tracks) {
29022
29049
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
29023
29050
  return false;
29024
29051
  }
@@ -30412,7 +30439,7 @@ var Mediabunny = (() => {
30412
30439
  return this.ebmlWriter.dataOffsets.get(this.segment);
30413
30440
  }
30414
30441
  allTracksAreKnown() {
30415
- for (const track of this.output._tracks) {
30442
+ for (const track of this.output.tracks) {
30416
30443
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
30417
30444
  return false;
30418
30445
  }
@@ -30750,7 +30777,8 @@ ${cue.notes ?? ""}`;
30750
30777
  view2.setUint8(0, 128 | trackData.track.id);
30751
30778
  view2.setInt16(1, relativeTimestamp, false);
30752
30779
  const msDuration = Math.round(1e3 * chunk.duration);
30753
- if (!chunk.additions) {
30780
+ const needsBlockGroup = !!chunk.additions || trackData.type === "subtitle";
30781
+ if (!needsBlockGroup) {
30754
30782
  view2.setUint8(3, Number(chunk.type === "key") << 7);
30755
30783
  const simpleBlock = { id: 163 /* SimpleBlock */, data: [
30756
30784
  prelude,
@@ -31260,7 +31288,7 @@ ${cue.notes ?? ""}`;
31260
31288
  throw new Error("Subtitle tracks are not supported.");
31261
31289
  }
31262
31290
  allTracksAreKnown() {
31263
- for (const track of this.output._tracks) {
31291
+ for (const track of this.output.tracks) {
31264
31292
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
31265
31293
  return false;
31266
31294
  }
@@ -31715,7 +31743,7 @@ ${cue.notes ?? ""}`;
31715
31743
  return result;
31716
31744
  }
31717
31745
  allTracksAreKnown() {
31718
- for (const track of this.output._tracks) {
31746
+ for (const track of this.output.tracks) {
31719
31747
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
31720
31748
  return false;
31721
31749
  }
@@ -34539,8 +34567,8 @@ ${cue.notes ?? ""}`;
34539
34567
  }
34540
34568
  async start() {
34541
34569
  const release = await this.mutex.acquire();
34542
- const someRelative = this.output._tracks.some((t) => t.metadata.isRelativeToUnixEpoch);
34543
- const someNotRelative = this.output._tracks.some((t) => !t.metadata.isRelativeToUnixEpoch);
34570
+ const someRelative = this.output.tracks.some((t) => t.metadata.isRelativeToUnixEpoch);
34571
+ const someNotRelative = this.output.tracks.some((t) => !t.metadata.isRelativeToUnixEpoch);
34544
34572
  if (someRelative && someNotRelative) {
34545
34573
  throw new Error(
34546
34574
  "All tracks must agree on `relativeToUnixEpoch`: some tracks are relative to the Unix epoch and some are not."
@@ -34552,12 +34580,12 @@ ${cue.notes ?? ""}`;
34552
34580
  let hasVideo = false;
34553
34581
  let illegalPairingDetected = false;
34554
34582
  let keyPacketsOnlyPairingWarned = false;
34555
- for (const track of this.output._tracks) {
34583
+ for (const track of this.output.tracks) {
34556
34584
  if (track.type === "video") {
34557
34585
  hasVideo = true;
34558
34586
  }
34559
34587
  const pairableGroups = /* @__PURE__ */ new Map();
34560
- for (const otherTrack of this.output._tracks) {
34588
+ for (const otherTrack of this.output.tracks) {
34561
34589
  if (track === otherTrack) {
34562
34590
  continue;
34563
34591
  }
@@ -34611,7 +34639,7 @@ ${cue.notes ?? ""}`;
34611
34639
  const variantStreams = [];
34612
34640
  const unpairedVideoTracks = [];
34613
34641
  const unpairedAudioTracks = [];
34614
- for (const track of this.output._tracks) {
34642
+ for (const track of this.output.tracks) {
34615
34643
  const assignedGroupKeys = groupAssignment.get(track);
34616
34644
  if (assignedGroupKeys) {
34617
34645
  assert(assignedGroupKeys.length > 0);
@@ -35907,7 +35935,7 @@ ${cue.notes ?? ""}`;
35907
35935
  getSupportedCodecs() {
35908
35936
  return [
35909
35937
  ...PCM_AUDIO_CODECS.filter(
35910
- (codec) => ["pcm-s16", "pcm-s24", "pcm-s32", "pcm-f32", "pcm-u8", "ulaw", "alaw"].includes(codec)
35938
+ (codec) => ["pcm-s16", "pcm-s24", "pcm-s32", "pcm-f32", "pcm-f64", "pcm-u8", "ulaw", "alaw"].includes(codec)
35911
35939
  )
35912
35940
  ];
35913
35941
  }
@@ -36333,6 +36361,10 @@ ${cue.notes ?? ""}`;
36333
36361
  * {@link BaseTrackMetadata.group}.
36334
36362
  */
36335
36363
  this.defaultTrackGroup = new OutputTrackGroup();
36364
+ /**
36365
+ * The tracks that have been added to this output. Treat it as a readonly field; to add tracks, use the methods.
36366
+ */
36367
+ this.tracks = [];
36336
36368
  /** @internal */
36337
36369
  this._onFinalize = null;
36338
36370
  /** @internal */
@@ -36340,8 +36372,6 @@ ${cue.notes ?? ""}`;
36340
36372
  /** @internal */
36341
36373
  this._rootWriterPromise = null;
36342
36374
  /** @internal */
36343
- this._tracks = [];
36344
- /** @internal */
36345
36375
  this._startPromise = null;
36346
36376
  /** @internal */
36347
36377
  this._cancelPromise = null;
@@ -36521,7 +36551,7 @@ ${cue.notes ?? ""}`;
36521
36551
  const metadataCopy = { ...metadata };
36522
36552
  metadataCopy.group ??= this.defaultTrackGroup;
36523
36553
  return this._addTrack(new OutputVideoTrack2(
36524
- this._tracks.length + 1,
36554
+ this.tracks.length + 1,
36525
36555
  this,
36526
36556
  source,
36527
36557
  metadataCopy
@@ -36536,7 +36566,7 @@ ${cue.notes ?? ""}`;
36536
36566
  const metadataCopy = { ...metadata };
36537
36567
  metadataCopy.group ??= this.defaultTrackGroup;
36538
36568
  return this._addTrack(new OutputAudioTrack2(
36539
- this._tracks.length + 1,
36569
+ this.tracks.length + 1,
36540
36570
  this,
36541
36571
  source,
36542
36572
  metadataCopy
@@ -36551,7 +36581,7 @@ ${cue.notes ?? ""}`;
36551
36581
  const metadataCopy = { ...metadata };
36552
36582
  metadataCopy.group ??= this.defaultTrackGroup;
36553
36583
  return this._addTrack(new OutputSubtitleTrack2(
36554
- this._tracks.length + 1,
36584
+ this.tracks.length + 1,
36555
36585
  this,
36556
36586
  source,
36557
36587
  metadataCopy
@@ -36579,7 +36609,7 @@ ${cue.notes ?? ""}`;
36579
36609
  throw new Error("Source is already used for a track.");
36580
36610
  }
36581
36611
  const supportedTrackCounts = this.format.getSupportedTrackCounts();
36582
- const presentTracksOfThisType = this._tracks.reduce(
36612
+ const presentTracksOfThisType = this.tracks.reduce(
36583
36613
  (count, t) => count + (t.type === track.type ? 1 : 0),
36584
36614
  0
36585
36615
  );
@@ -36590,7 +36620,7 @@ ${cue.notes ?? ""}`;
36590
36620
  );
36591
36621
  }
36592
36622
  const maxTotalCount = supportedTrackCounts.total.max;
36593
- if (this._tracks.length === maxTotalCount) {
36623
+ if (this.tracks.length === maxTotalCount) {
36594
36624
  throw new Error(
36595
36625
  `${this.format._name} does not support more than ${maxTotalCount} tracks${maxTotalCount === 1 ? "" : "s"} in total.`
36596
36626
  );
@@ -36629,10 +36659,32 @@ ${cue.notes ?? ""}`;
36629
36659
  );
36630
36660
  }
36631
36661
  }
36632
- this._tracks.push(track);
36662
+ this.tracks.push(track);
36633
36663
  track.source._connectedTrack = track;
36634
36664
  return track;
36635
36665
  }
36666
+ /**
36667
+ * Whether the output has enough tracks (of the correct type) to be started, based on the requirements of the output
36668
+ * format.
36669
+ */
36670
+ hasEnoughTracks() {
36671
+ const supportedTrackCounts = this.format.getSupportedTrackCounts();
36672
+ for (const trackType of ALL_TRACK_TYPES) {
36673
+ const presentTracksOfThisType = this.tracks.reduce(
36674
+ (count, track) => count + (track.type === trackType ? 1 : 0),
36675
+ 0
36676
+ );
36677
+ const minCount = supportedTrackCounts[trackType].min;
36678
+ if (presentTracksOfThisType < minCount) {
36679
+ return false;
36680
+ }
36681
+ }
36682
+ const totalMinCount = supportedTrackCounts.total.min;
36683
+ if (this.tracks.length < totalMinCount) {
36684
+ return false;
36685
+ }
36686
+ return true;
36687
+ }
36636
36688
  /**
36637
36689
  * Starts the creation of the output file. This method should be called after all tracks have been added. Only after
36638
36690
  * the output has started can media samples be added to the tracks.
@@ -36642,7 +36694,7 @@ ${cue.notes ?? ""}`;
36642
36694
  async start() {
36643
36695
  const supportedTrackCounts = this.format.getSupportedTrackCounts();
36644
36696
  for (const trackType of ALL_TRACK_TYPES) {
36645
- const presentTracksOfThisType = this._tracks.reduce(
36697
+ const presentTracksOfThisType = this.tracks.reduce(
36646
36698
  (count, track) => count + (track.type === trackType ? 1 : 0),
36647
36699
  0
36648
36700
  );
@@ -36654,7 +36706,7 @@ ${cue.notes ?? ""}`;
36654
36706
  }
36655
36707
  }
36656
36708
  const totalMinCount = supportedTrackCounts.total.min;
36657
- if (this._tracks.length < totalMinCount) {
36709
+ if (this.tracks.length < totalMinCount) {
36658
36710
  throw new Error(
36659
36711
  totalMinCount === supportedTrackCounts.total.max ? `${this.format._name} requires exactly ${totalMinCount} track${totalMinCount === 1 ? "" : "s"}.` : `${this.format._name} requires at least ${totalMinCount} track${totalMinCount === 1 ? "" : "s"}.`
36660
36712
  );
@@ -36671,7 +36723,7 @@ ${cue.notes ?? ""}`;
36671
36723
  const release = await this._mutex.acquire();
36672
36724
  try {
36673
36725
  await this._muxer.start();
36674
- const promises = this._tracks.map((track) => track.source._start());
36726
+ const promises = this.tracks.map((track) => track.source._start());
36675
36727
  await Promise.all(promises);
36676
36728
  } finally {
36677
36729
  release();
@@ -36706,7 +36758,7 @@ ${cue.notes ?? ""}`;
36706
36758
  this.state = "canceled";
36707
36759
  const release = await this._mutex.acquire();
36708
36760
  try {
36709
- const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(true));
36761
+ const promises = this.tracks.map((x) => x.source._flushOrWaitForOngoingClose(true));
36710
36762
  await Promise.all(promises);
36711
36763
  await Promise.all([...this._unfinalizedTargets].map((target) => target._close()));
36712
36764
  this._unfinalizedTargets.clear();
@@ -36734,7 +36786,7 @@ ${cue.notes ?? ""}`;
36734
36786
  this.state = "finalizing";
36735
36787
  const release = await this._mutex.acquire();
36736
36788
  try {
36737
- const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(false));
36789
+ const promises = this.tracks.map((x) => x.source._flushOrWaitForOngoingClose(false));
36738
36790
  await Promise.all(promises);
36739
36791
  await this._muxer.finalize();
36740
36792
  if (this._rootWriterPromise) {
@@ -36873,14 +36925,15 @@ ${cue.notes ?? ""}`;
36873
36925
  var Conversion = class _Conversion {
36874
36926
  /** Creates a new Conversion instance (duh). */
36875
36927
  constructor(options) {
36876
- /** @internal */
36877
- this._addedCounts = {
36878
- video: 0,
36879
- audio: 0,
36880
- subtitle: 0
36881
- };
36882
- /** @internal */
36883
- this._totalTrackCount = 0;
36928
+ /**
36929
+ * The current state of the conversion.
36930
+ *
36931
+ * - `'idle'`: The conversion is not currently executing and isn't done; `execute` can be called.
36932
+ * - `'executing'`: A call to `execute` is currently running.
36933
+ * - `'canceled'`: The conversion has been canceled and can no longer be executed.
36934
+ * - `'done'`: The conversion has run to completion. Subsequent calls to `execute` do nothing.
36935
+ */
36936
+ this.state = "idle";
36884
36937
  /** @internal */
36885
36938
  this._nextOutputTrackId = 0;
36886
36939
  /** @internal */
@@ -36888,18 +36941,22 @@ ${cue.notes ?? ""}`;
36888
36941
  /** @internal */
36889
36942
  this._outputOwnTrackGroups = [];
36890
36943
  /** @internal */
36891
- this._trackPromises = [];
36944
+ this._trackPumps = [];
36945
+ /** @internal */
36946
+ this._composable = false;
36892
36947
  /** @internal */
36893
36948
  this._executed = false;
36894
36949
  /** @internal */
36895
- this._synchronizer = new TrackSynchronizer();
36950
+ this._executionUntil = Infinity;
36951
+ /** @internal */
36952
+ this._pauseRequested = false;
36953
+ /** @internal */
36954
+ this._synchronizer = new TrackSynchronizer(this);
36896
36955
  /** @internal */
36897
36956
  this._totalDuration = null;
36898
36957
  /** @internal */
36899
36958
  this._maxTimestamps = /* @__PURE__ */ new Map();
36900
36959
  // Track ID -> timestamp
36901
- /** @internal */
36902
- this._canceled = false;
36903
36960
  /**
36904
36961
  * A callback that is fired whenever the conversion progresses. Gets passed as first argument a number between
36905
36962
  * 0 and 1, indicating the completion of the conversion. Note that a progress of 1 doesn't necessarily mean the
@@ -36916,7 +36973,8 @@ ${cue.notes ?? ""}`;
36916
36973
  this._lastProgress = 0;
36917
36974
  /**
36918
36975
  * Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
36919
- * the `discardedTracks` field for reasons.
36976
+ * the `discardedTracks` field for reasons. Composable conversions are always valid, even if they utilize
36977
+ * zero tracks.
36920
36978
  *
36921
36979
  * Note: a conversion having discarded tracks does not automatically mean it is invalid; if the remaining, utilized
36922
36980
  * tracks make for a valid output file, the conversion is still allowed.
@@ -36943,8 +37001,23 @@ ${cue.notes ?? ""}`;
36943
37001
  "options.tracks, when provided, must be either 'all' or 'primary'."
36944
37002
  );
36945
37003
  }
36946
- if (options.output._tracks.length > 0 || Object.keys(options.output._metadataTags).length > 0 || options.output.state !== "pending") {
36947
- throw new TypeError("options.output must be fresh: no tracks or metadata tags added and not started.");
37004
+ if (options.composable !== void 0 && typeof options.composable !== "boolean") {
37005
+ throw new TypeError("options.composable, when provided, must be a boolean.");
37006
+ }
37007
+ const composable = options.composable ?? false;
37008
+ if (!composable) {
37009
+ if (options.output.tracks.length > 0 || Object.keys(options.output._metadataTags).length > 0 || options.output.state !== "pending") {
37010
+ throw new TypeError("options.output must be fresh: no tracks or metadata tags added and not started.");
37011
+ }
37012
+ } else {
37013
+ if (options.tags !== void 0) {
37014
+ throw new TypeError(
37015
+ "options.tags cannot be set by a composable conversion; set metadata directly on the output instead."
37016
+ );
37017
+ }
37018
+ if (options.output.state !== "pending") {
37019
+ throw new TypeError("options.output must not have been started yet.");
37020
+ }
36948
37021
  }
36949
37022
  if (options.video !== void 0 && typeof options.video !== "function") {
36950
37023
  if (Array.isArray(options.video)) {
@@ -36988,11 +37061,9 @@ ${cue.notes ?? ""}`;
36988
37061
  throw new TypeError("options.showWarnings, when provided, must be a boolean.");
36989
37062
  }
36990
37063
  this._options = options;
37064
+ this._composable = composable;
36991
37065
  this.input = options.input;
36992
37066
  this.output = options.output;
36993
- const { promise: started, resolve: start } = promiseWithResolvers();
36994
- this._started = started;
36995
- this._start = start;
36996
37067
  }
36997
37068
  /** Initializes a new conversion process without starting the conversion. */
36998
37069
  static async init(options) {
@@ -37104,7 +37175,7 @@ ${cue.notes ?? ""}`;
37104
37175
  const track = filteredTracks[i];
37105
37176
  const options = filteredTrackOptions[i];
37106
37177
  for (const option of options) {
37107
- if (this._totalTrackCount === outputTrackCounts.total.max) {
37178
+ if (this.output.tracks.length === outputTrackCounts.total.max) {
37108
37179
  this.discardedTracks.push({
37109
37180
  track,
37110
37181
  reason: "max_track_count_reached",
@@ -37112,7 +37183,11 @@ ${cue.notes ?? ""}`;
37112
37183
  });
37113
37184
  continue;
37114
37185
  }
37115
- if (this._addedCounts[track.type] === outputTrackCounts[track.type].max) {
37186
+ const addedCountOfType = this.output.tracks.reduce(
37187
+ (count, t) => count + (t.type === track.type ? 1 : 0),
37188
+ 0
37189
+ );
37190
+ if (addedCountOfType === outputTrackCounts[track.type].max) {
37116
37191
  this.discardedTracks.push({
37117
37192
  track,
37118
37193
  reason: "max_track_count_of_type_reached",
@@ -37143,22 +37218,28 @@ ${cue.notes ?? ""}`;
37143
37218
  }
37144
37219
  }
37145
37220
  }
37146
- const inputTags = await this.input.getMetadataTags();
37147
- let outputTags;
37148
- if (this._options.tags) {
37149
- const result = typeof this._options.tags === "function" ? await this._options.tags(inputTags) : this._options.tags;
37150
- validateMetadataTags(result);
37151
- outputTags = result;
37152
- } else {
37153
- outputTags = inputTags;
37221
+ if (!this._composable) {
37222
+ const inputTags = await this.input.getMetadataTags();
37223
+ let outputTags;
37224
+ if (this._options.tags) {
37225
+ const result = typeof this._options.tags === "function" ? await this._options.tags(inputTags) : this._options.tags;
37226
+ validateMetadataTags(result);
37227
+ outputTags = result;
37228
+ } else {
37229
+ outputTags = inputTags;
37230
+ }
37231
+ const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
37232
+ const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
37233
+ if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
37234
+ delete outputTags.raw;
37235
+ }
37236
+ this.output.setMetadataTags(outputTags);
37154
37237
  }
37155
- const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
37156
- const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
37157
- if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
37158
- delete outputTags.raw;
37238
+ if (!this._composable) {
37239
+ this.isValid = this.output.hasEnoughTracks();
37240
+ } else {
37241
+ this.isValid = true;
37159
37242
  }
37160
- this.output.setMetadataTags(outputTags);
37161
- this.isValid = this._totalTrackCount >= outputTrackCounts.total.min && this._addedCounts.video >= outputTrackCounts.video.min && this._addedCounts.audio >= outputTrackCounts.audio.min && this._addedCounts.subtitle >= outputTrackCounts.subtitle.min;
37162
37243
  if (this._options.showWarnings ?? true) {
37163
37244
  const warnElements = [];
37164
37245
  const unintentionallyDiscardedTracks = this.discardedTracks.filter((x) => x.reason !== "discarded_by_user");
@@ -37244,59 +37325,116 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37244
37325
  return elements;
37245
37326
  }
37246
37327
  /**
37247
- * Executes the conversion process. Resolves once conversion is complete.
37328
+ * Executes the conversion process and resolves when the conversion is complete. When
37329
+ * {@link ConversionExecuteOptions.until} is provided, the conversion will be suspended once that output timestamp
37330
+ * is reached and can be resumed with another call to `execute`. An ongoing execution may also be suspended via
37331
+ * {@link ConversionExecuteOptions.pauseSignal}.
37248
37332
  *
37249
- * Will throw if `isValid` is `false`.
37333
+ * Execution will throw if `isValid` is `false`.
37250
37334
  */
37251
- async execute() {
37335
+ async execute(options = {}) {
37336
+ if (!options || typeof options !== "object") {
37337
+ throw new TypeError("options must be an object.");
37338
+ }
37339
+ if (options.until !== void 0 && (typeof options.until !== "number" || Number.isNaN(options.until))) {
37340
+ throw new TypeError("options.until, when provided, must be a number.");
37341
+ }
37342
+ if (options.pauseSignal !== void 0 && !(options.pauseSignal instanceof AbortSignal)) {
37343
+ throw new TypeError("options.pauseSignal, when provided, must be an AbortSignal.");
37344
+ }
37252
37345
  if (!this.isValid) {
37253
37346
  throw new Error(
37254
37347
  "Cannot execute this conversion because its output configuration is invalid. Make sure to always check the isValid field before executing a conversion.\n" + this._getInvalidityExplanation().join("")
37255
37348
  );
37256
37349
  }
37257
- if (this._executed) {
37258
- throw new Error("Conversion cannot be executed twice.");
37350
+ if (this.state === "executing") {
37351
+ throw new Error("Cannot call execute() while a previous call to execute() is still running.");
37259
37352
  }
37260
- this._executed = true;
37261
- for (const id of this._outputTrackIds) {
37262
- this._synchronizer.declareTrack(id);
37353
+ if (this.state === "canceled") {
37354
+ throw new ConversionCanceledError();
37263
37355
  }
37264
- if (this.onProgress) {
37265
- const uniqueUtilizedTracks = new Set(this.utilizedTracks);
37266
- const durationPromises = [...uniqueUtilizedTracks].map(async (track) => {
37267
- if (await track.isLive()) {
37268
- return Infinity;
37269
- }
37270
- return await track.getDurationFromMetadata() ?? await track.computeDuration();
37271
- });
37272
- const duration = Math.max(0, ...await Promise.all(durationPromises));
37273
- this._computeProgress = true;
37274
- this._totalDuration = Math.min(
37275
- duration - this._startTimestamp,
37276
- this._endTimestamp - this._startTimestamp
37356
+ if (this.state === "done") {
37357
+ return;
37358
+ }
37359
+ if (this._composable && this.output.state === "pending") {
37360
+ throw new Error(
37361
+ "A composable conversion requires the output to be started. Call start() on the output before executing the conversion."
37277
37362
  );
37363
+ }
37364
+ this.state = "executing";
37365
+ this._executionUntil = options.until ?? Infinity;
37366
+ this._pauseRequested = options.pauseSignal?.aborted ?? false;
37367
+ const onPause = () => {
37368
+ if (this.state !== "executing") {
37369
+ return;
37370
+ }
37371
+ this._pauseRequested = true;
37372
+ this._synchronizer.resolveAll();
37373
+ };
37374
+ options.pauseSignal?.addEventListener("abort", onPause);
37375
+ for (const pump of this._trackPumps) {
37376
+ if (!pump.done) {
37377
+ pump.resolvers = promiseWithResolvers();
37378
+ }
37379
+ }
37380
+ if (!this._executed) {
37381
+ this._executed = true;
37278
37382
  for (const id of this._outputTrackIds) {
37279
- this._maxTimestamps.set(id, 0);
37383
+ this._synchronizer.declareTrack(id);
37384
+ }
37385
+ if (this.onProgress) {
37386
+ const uniqueUtilizedTracks = new Set(this.utilizedTracks);
37387
+ const durationPromises = [...uniqueUtilizedTracks].map(async (track) => {
37388
+ if (await track.isLive()) {
37389
+ return Infinity;
37390
+ }
37391
+ return await track.getDurationFromMetadata() ?? await track.computeDuration();
37392
+ });
37393
+ const duration = Math.max(0, ...await Promise.all(durationPromises));
37394
+ this._computeProgress = true;
37395
+ this._totalDuration = Math.min(
37396
+ duration - this._startTimestamp,
37397
+ this._endTimestamp - this._startTimestamp
37398
+ );
37399
+ for (const id of this._outputTrackIds) {
37400
+ this._maxTimestamps.set(id, 0);
37401
+ }
37402
+ this.onProgress?.(0, 0);
37403
+ }
37404
+ if (!this._composable) {
37405
+ await this.output.start();
37406
+ }
37407
+ for (const pump of this._trackPumps) {
37408
+ pump.start();
37409
+ }
37410
+ } else {
37411
+ for (const pump of this._trackPumps) {
37412
+ pump.wake?.();
37280
37413
  }
37281
- this.onProgress?.(0, 0);
37282
37414
  }
37283
- await this.output.start();
37284
- this._start();
37285
37415
  try {
37286
- await Promise.all(this._trackPromises);
37416
+ await Promise.all(this._trackPumps.map((x) => x.resolvers.promise));
37287
37417
  } catch (error) {
37288
- if (!this._canceled) {
37418
+ if (this.state !== "canceled") {
37289
37419
  void this.cancel();
37290
37420
  }
37291
37421
  throw error;
37422
+ } finally {
37423
+ options.pauseSignal?.removeEventListener("abort", onPause);
37292
37424
  }
37293
- if (this._canceled) {
37425
+ if (this.state === "canceled") {
37294
37426
  throw new ConversionCanceledError();
37295
37427
  }
37296
- await this.output.finalize();
37297
- if (this._computeProgress) {
37298
- const minTimestamp = Math.min(...this._maxTimestamps.values());
37299
- this.onProgress?.(1, minTimestamp);
37428
+ const isDone = this._trackPumps.every((x) => x.done);
37429
+ this.state = isDone ? "done" : "idle";
37430
+ if (isDone) {
37431
+ if (!this._composable) {
37432
+ await this.output.finalize();
37433
+ }
37434
+ if (this._computeProgress) {
37435
+ const minTimestamp = Math.min(...this._maxTimestamps.values());
37436
+ this.onProgress?.(1, minTimestamp);
37437
+ }
37300
37438
  }
37301
37439
  }
37302
37440
  /**
@@ -37304,15 +37442,21 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37304
37442
  * Does nothing if the conversion is already complete.
37305
37443
  */
37306
37444
  async cancel() {
37307
- if (this.output.state === "finalizing" || this.output.state === "finalized") {
37445
+ if (this.state === "done") {
37308
37446
  return;
37309
37447
  }
37310
- if (this._canceled) {
37448
+ if (this.state === "canceled") {
37311
37449
  Logging._warn("Conversion already canceled.");
37312
37450
  return;
37313
37451
  }
37314
- this._canceled = true;
37315
- await this.output.cancel();
37452
+ this.state = "canceled";
37453
+ for (const pump of this._trackPumps) {
37454
+ pump.wake?.();
37455
+ }
37456
+ this._synchronizer.resolveAll();
37457
+ if (!this._composable) {
37458
+ await this.output.cancel();
37459
+ }
37316
37460
  }
37317
37461
  /** @internal */
37318
37462
  async _processVideoTrack(track, trackOptions, outputTrackId) {
@@ -37358,14 +37502,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37358
37502
  if (!needsTranscode) {
37359
37503
  const source = new EncodedVideoPacketSource(sourceCodec);
37360
37504
  videoSource = source;
37361
- this._trackPromises.push((async () => {
37362
- await this._started;
37505
+ this._registerTrackPump(async (pump) => {
37363
37506
  const sink = new EncodedPacketSink(track);
37364
37507
  const decoderConfig = await track.getDecoderConfig();
37365
37508
  const meta = { decoderConfig: decoderConfig ?? void 0 };
37366
37509
  for await (const packet of sink.packets(void 0, void 0, { verifyKeyPackets: true })) {
37367
- if (this._canceled) {
37368
- return;
37510
+ if (this.state === "canceled") {
37511
+ break;
37369
37512
  }
37370
37513
  if (packet.timestamp >= this._endTimestamp) {
37371
37514
  break;
@@ -37380,10 +37523,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37380
37523
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
37381
37524
  await this._synchronizer.wait(modifiedPacket.timestamp);
37382
37525
  }
37526
+ await this._checkpoint(pump, modifiedPacket.timestamp);
37383
37527
  }
37384
37528
  source.close();
37385
37529
  this._synchronizer.closeTrack(outputTrackId);
37386
- })());
37530
+ });
37387
37531
  } else {
37388
37532
  const canDecode2 = await track.canDecode();
37389
37533
  if (!canDecode2) {
@@ -37423,32 +37567,39 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37423
37567
  assert(encodingConfig.transform);
37424
37568
  let needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== void 0) || !!crop || squarePixelWidth !== await track.getCodedWidth() || squarePixelHeight !== await track.getCodedHeight();
37425
37569
  if (!needsRerender) {
37426
- const tempOutput = new Output({
37427
- format: new Mp4OutputFormat(),
37428
- // Supports all video codecs
37429
- target: new NullTarget()
37430
- });
37431
- const tempSource = new VideoSampleSource(encodingConfig);
37432
- tempOutput.addVideoTrack(tempSource);
37433
- await tempOutput.start();
37434
- const sink = new VideoSampleSink(track);
37435
- const firstSample = await sink.getSample(firstTimestamp);
37436
- if (firstSample) {
37437
- try {
37438
- await tempSource.add(firstSample);
37439
- firstSample.close();
37440
- await tempOutput.finalize();
37441
- } catch (error) {
37442
- Logging._warn(
37443
- "An error occurred when probing encoder support. Falling back to rerender path.",
37444
- error
37445
- );
37446
- void tempOutput.cancel();
37447
- needsRerender = true;
37448
- encodingConfig.transform.force = true;
37570
+ var _stack = [];
37571
+ try {
37572
+ const tempOutput = new Output({
37573
+ format: new Mp4OutputFormat(),
37574
+ // Supports all video codecs
37575
+ target: new NullTarget()
37576
+ });
37577
+ const tempSource = new VideoSampleSource(encodingConfig);
37578
+ tempOutput.addVideoTrack(tempSource);
37579
+ await tempOutput.start();
37580
+ const sink = new VideoSampleSink(track);
37581
+ const firstSample = __using(_stack, await sink.getSample(firstTimestamp));
37582
+ if (firstSample) {
37583
+ try {
37584
+ await tempSource.add(firstSample);
37585
+ firstSample.close();
37586
+ await tempOutput.finalize();
37587
+ } catch (error) {
37588
+ Logging._warn(
37589
+ "An error occurred when probing encoder support. Falling back to rerender path.",
37590
+ error
37591
+ );
37592
+ void tempOutput.cancel();
37593
+ needsRerender = true;
37594
+ encodingConfig.transform.force = true;
37595
+ }
37596
+ } else {
37597
+ await tempOutput.cancel();
37449
37598
  }
37450
- } else {
37451
- await tempOutput.cancel();
37599
+ } catch (_) {
37600
+ var _error = _, _hasError = true;
37601
+ } finally {
37602
+ __callDispose(_stack, _error, _hasError);
37452
37603
  }
37453
37604
  }
37454
37605
  if (trackOptions.frameRate) {
@@ -37472,31 +37623,38 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37472
37623
  };
37473
37624
  const source = new VideoSampleSource(encodingConfig);
37474
37625
  videoSource = source;
37475
- this._trackPromises.push((async () => {
37476
- await this._started;
37626
+ this._registerTrackPump(async (pump) => {
37477
37627
  const sink = new VideoSampleSink(track);
37478
- for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37479
- if (this._canceled) {
37628
+ for await (var _sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37629
+ var _stack2 = [];
37630
+ try {
37631
+ const sample = __using(_stack2, _sample);
37632
+ if (this.state === "canceled") {
37633
+ break;
37634
+ }
37635
+ const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37636
+ sample.setTimestamp(adjustedSampleTimestamp);
37637
+ this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37638
+ await source.add(sample);
37480
37639
  sample.close();
37481
- return;
37482
- }
37483
- const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37484
- sample.setTimestamp(adjustedSampleTimestamp);
37485
- this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37486
- await source.add(sample);
37487
- if (lastSampleTimestamp !== null) {
37488
- if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37489
- await this._synchronizer.wait(lastSampleTimestamp);
37640
+ if (lastSampleTimestamp !== null) {
37641
+ if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37642
+ await this._synchronizer.wait(lastSampleTimestamp);
37643
+ }
37644
+ await this._checkpoint(pump, lastSampleTimestamp);
37490
37645
  }
37646
+ } catch (_2) {
37647
+ var _error2 = _2, _hasError2 = true;
37648
+ } finally {
37649
+ __callDispose(_stack2, _error2, _hasError2);
37491
37650
  }
37492
- sample.close();
37493
37651
  }
37494
37652
  source.close();
37495
37653
  this._synchronizer.closeTrack(outputTrackId);
37496
- })());
37654
+ });
37497
37655
  }
37498
37656
  let ownGroup = null;
37499
- if (!trackOptions.group) {
37657
+ if (!trackOptions.group && !this._composable) {
37500
37658
  ownGroup = new OutputTrackGroup();
37501
37659
  }
37502
37660
  const videoTrackLanguageCode = await track.getLanguageCode();
@@ -37509,8 +37667,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37509
37667
  rotation: outputTrackRotation,
37510
37668
  group: ownGroup ?? trackOptions.group
37511
37669
  });
37512
- this._addedCounts.video++;
37513
- this._totalTrackCount++;
37514
37670
  this.utilizedTracks.push(track);
37515
37671
  this._outputTrackIds.push(outputTrackId);
37516
37672
  this._outputOwnTrackGroups.push(ownGroup);
@@ -37538,14 +37694,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37538
37694
  if (!trackOptions.forceTranscode && !trackOptions.bitrate && numberOfChannels === originalNumberOfChannels && sampleRate === originalSampleRate && !needsTrimming && !needsPadding && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
37539
37695
  const source = new EncodedAudioPacketSource(sourceCodec);
37540
37696
  audioSource = source;
37541
- this._trackPromises.push((async () => {
37542
- await this._started;
37697
+ this._registerTrackPump(async (pump) => {
37543
37698
  const sink = new EncodedPacketSink(track);
37544
37699
  const decoderConfig = await track.getDecoderConfig();
37545
37700
  const meta = { decoderConfig: decoderConfig ?? void 0 };
37546
37701
  for await (const packet of sink.packets()) {
37547
- if (this._canceled) {
37548
- return;
37702
+ if (this.state === "canceled") {
37703
+ break;
37549
37704
  }
37550
37705
  if (packet.timestamp >= this._endTimestamp) {
37551
37706
  break;
@@ -37559,10 +37714,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37559
37714
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
37560
37715
  await this._synchronizer.wait(modifiedPacket.timestamp);
37561
37716
  }
37717
+ await this._checkpoint(pump, modifiedPacket.timestamp);
37562
37718
  }
37563
37719
  source.close();
37564
37720
  this._synchronizer.closeTrack(outputTrackId);
37565
- })());
37721
+ });
37566
37722
  } else {
37567
37723
  const canDecode2 = await track.canDecode();
37568
37724
  if (!canDecode2) {
@@ -37627,60 +37783,96 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37627
37783
  };
37628
37784
  const source = new AudioSampleSource(encodingConfig);
37629
37785
  audioSource = source;
37630
- this._trackPromises.push((async () => {
37631
- await this._started;
37786
+ this._registerTrackPump(async (pump) => {
37632
37787
  const sink = new AudioSampleSink(track);
37633
- for await (let sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37634
- if (this._canceled) {
37635
- sample.close();
37636
- return;
37637
- }
37638
- if (needsPadding) {
37639
- const paddingLength = firstTimestamp - this._startTimestamp;
37640
- const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
37641
- const bytesPerSample = getBytesPerSample(sample.format);
37642
- const data = new Uint8Array(bytesPerSample * paddingLengthSamples * originalNumberOfChannels);
37643
- if (sample.format === "u8" || sample.format === "u8-planar") {
37644
- data.fill(2 ** 7);
37645
- }
37646
- const silentSample = new AudioSample({
37647
- data,
37648
- // Use the same format the decoder is spitting out. This avoids feeding changing sample
37649
- // formats to the audio encoder.
37650
- format: sample.format,
37651
- numberOfChannels: originalNumberOfChannels,
37652
- sampleRate: originalSampleRate,
37653
- timestamp: 0
37654
- });
37655
- await this._registerAudioSample(silentSample, source, outputTrackId, () => lastSampleTimestamp);
37656
- needsPadding = false;
37657
- }
37658
- let startFrame = 0;
37659
- let endFrame = sample.numberOfFrames;
37660
- if (sample.timestamp < this._startTimestamp) {
37661
- startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
37662
- }
37663
- if (sample.timestamp + sample.duration > this._endTimestamp) {
37664
- endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
37665
- }
37666
- if (startFrame > 0 || endFrame < sample.numberOfFrames) {
37667
- const trimmedSample = sample.trim(startFrame, endFrame);
37668
- sample.close();
37669
- sample = trimmedSample;
37670
- if (sample.numberOfFrames === 0) {
37671
- sample.close();
37672
- continue;
37788
+ for await (var _sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37789
+ var _stack3 = [];
37790
+ try {
37791
+ const sample = __using(_stack3, _sample);
37792
+ var _stack2 = [];
37793
+ try {
37794
+ if (this.state === "canceled") {
37795
+ break;
37796
+ }
37797
+ if (needsPadding) {
37798
+ var _stack = [];
37799
+ try {
37800
+ const paddingLength = firstTimestamp - this._startTimestamp;
37801
+ const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
37802
+ const bytesPerSample = getBytesPerSample(sample.format);
37803
+ const data = new Uint8Array(bytesPerSample * paddingLengthSamples * originalNumberOfChannels);
37804
+ if (sample.format === "u8" || sample.format === "u8-planar") {
37805
+ data.fill(2 ** 7);
37806
+ }
37807
+ const silentSample = __using(_stack, new AudioSample({
37808
+ data,
37809
+ // Use the same format the decoder is spitting out. This avoids feeding changing sample
37810
+ // formats to the audio encoder.
37811
+ format: sample.format,
37812
+ numberOfChannels: originalNumberOfChannels,
37813
+ sampleRate: originalSampleRate,
37814
+ timestamp: 0
37815
+ }));
37816
+ await this._registerAudioSample(
37817
+ pump,
37818
+ silentSample,
37819
+ source,
37820
+ outputTrackId,
37821
+ () => lastSampleTimestamp
37822
+ );
37823
+ needsPadding = false;
37824
+ } catch (_) {
37825
+ var _error = _, _hasError = true;
37826
+ } finally {
37827
+ __callDispose(_stack, _error, _hasError);
37828
+ }
37829
+ }
37830
+ let startFrame = 0;
37831
+ let endFrame = sample.numberOfFrames;
37832
+ if (sample.timestamp < this._startTimestamp) {
37833
+ startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
37834
+ }
37835
+ if (sample.timestamp + sample.duration > this._endTimestamp) {
37836
+ endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
37837
+ }
37838
+ let finalSampleLet;
37839
+ if (startFrame > 0 || endFrame < sample.numberOfFrames) {
37840
+ const trimmedSample = sample.trim(startFrame, endFrame);
37841
+ sample.close();
37842
+ finalSampleLet = trimmedSample;
37843
+ if (trimmedSample.numberOfFrames === 0) {
37844
+ trimmedSample.close();
37845
+ continue;
37846
+ }
37847
+ } else {
37848
+ finalSampleLet = sample;
37849
+ }
37850
+ const finalSample = __using(_stack2, finalSampleLet);
37851
+ finalSample.setTimestamp(finalSample.timestamp - this._startTimestamp);
37852
+ await this._registerAudioSample(
37853
+ pump,
37854
+ finalSample,
37855
+ source,
37856
+ outputTrackId,
37857
+ () => lastSampleTimestamp
37858
+ );
37859
+ } catch (_2) {
37860
+ var _error2 = _2, _hasError2 = true;
37861
+ } finally {
37862
+ __callDispose(_stack2, _error2, _hasError2);
37673
37863
  }
37864
+ } catch (_3) {
37865
+ var _error3 = _3, _hasError3 = true;
37866
+ } finally {
37867
+ __callDispose(_stack3, _error3, _hasError3);
37674
37868
  }
37675
- sample.setTimestamp(sample.timestamp - this._startTimestamp);
37676
- await this._registerAudioSample(sample, source, outputTrackId, () => lastSampleTimestamp);
37677
37869
  }
37678
37870
  source.close();
37679
37871
  this._synchronizer.closeTrack(outputTrackId);
37680
- })());
37872
+ });
37681
37873
  }
37682
37874
  let ownGroup = null;
37683
- if (!trackOptions.group) {
37875
+ if (!trackOptions.group && !this._composable) {
37684
37876
  ownGroup = new OutputTrackGroup();
37685
37877
  }
37686
37878
  const audioTrackLanguageCode = await track.getLanguageCode();
@@ -37691,14 +37883,12 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37691
37883
  disposition: await track.getDisposition(),
37692
37884
  group: ownGroup ?? trackOptions.group
37693
37885
  });
37694
- this._addedCounts.audio++;
37695
- this._totalTrackCount++;
37696
37886
  this.utilizedTracks.push(track);
37697
37887
  this._outputTrackIds.push(outputTrackId);
37698
37888
  this._outputOwnTrackGroups.push(ownGroup);
37699
37889
  }
37700
37890
  /** @internal */
37701
- async _registerAudioSample(sample, source, outputTrackId, getLastSampleTimestamp) {
37891
+ async _registerAudioSample(pump, sample, source, outputTrackId, getLastSampleTimestamp) {
37702
37892
  this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37703
37893
  await source.add(sample);
37704
37894
  sample.close();
@@ -37707,6 +37897,33 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37707
37897
  if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37708
37898
  await this._synchronizer.wait(lastSampleTimestamp);
37709
37899
  }
37900
+ await this._checkpoint(pump, lastSampleTimestamp);
37901
+ }
37902
+ }
37903
+ /** @internal */
37904
+ _registerTrackPump(fn) {
37905
+ const pump = {
37906
+ done: false,
37907
+ resolvers: promiseWithResolvers(),
37908
+ wake: null,
37909
+ start: () => {
37910
+ void fn(pump).then(() => {
37911
+ pump.done = true;
37912
+ pump.resolvers.resolve();
37913
+ }, (error) => {
37914
+ pump.resolvers.reject(error);
37915
+ });
37916
+ }
37917
+ };
37918
+ this._trackPumps.push(pump);
37919
+ }
37920
+ /** @internal */
37921
+ async _checkpoint(pump, timestamp) {
37922
+ while (this.state !== "canceled" && (timestamp >= this._executionUntil || this._pauseRequested)) {
37923
+ pump.resolvers.resolve();
37924
+ const { promise, resolve } = promiseWithResolvers();
37925
+ pump.wake = resolve;
37926
+ await promise;
37710
37927
  }
37711
37928
  }
37712
37929
  /** @internal */
@@ -37736,10 +37953,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37736
37953
  };
37737
37954
  var MAX_TIMESTAMP_GAP = 1;
37738
37955
  var TrackSynchronizer = class {
37739
- constructor() {
37956
+ constructor(conversion) {
37740
37957
  this.maxTimestamps = /* @__PURE__ */ new Map();
37741
37958
  // Track ID -> timestamp
37742
37959
  this.resolvers = [];
37960
+ this.conversion = conversion;
37743
37961
  }
37744
37962
  declareTrack(trackId) {
37745
37963
  this.maxTimestamps.set(trackId, 0);
@@ -37749,6 +37967,9 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37749
37967
  assert(currentValue !== void 0);
37750
37968
  this.maxTimestamps.set(trackId, Math.max(timestamp, currentValue));
37751
37969
  const newMin = this.computeMinAndMaybeResolve();
37970
+ if (this.conversion.state === "canceled" || this.conversion._pauseRequested || timestamp >= this.conversion._executionUntil) {
37971
+ return false;
37972
+ }
37752
37973
  return timestamp - newMin > MAX_TIMESTAMP_GAP;
37753
37974
  }
37754
37975
  wait(timestamp) {
@@ -37763,6 +37984,12 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37763
37984
  this.maxTimestamps.delete(trackId);
37764
37985
  this.computeMinAndMaybeResolve();
37765
37986
  }
37987
+ resolveAll() {
37988
+ for (const entry of this.resolvers) {
37989
+ entry.resolve();
37990
+ }
37991
+ this.resolvers.length = 0;
37992
+ }
37766
37993
  computeMinAndMaybeResolve() {
37767
37994
  let newMin = Infinity;
37768
37995
  for (const [, timestamp] of this.maxTimestamps) {