mediabunny 1.50.9 → 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.
@@ -29000,7 +29000,7 @@ var Mediabunny = (() => {
29000
29000
  this.initWriter = initWriter;
29001
29001
  this.initBoxWriter = new IsobmffBoxWriter(initWriter);
29002
29002
  }
29003
- 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");
29004
29004
  {
29005
29005
  const boxWriter = this.initBoxWriter ?? this.boxWriter;
29006
29006
  assert(boxWriter);
@@ -29024,7 +29024,7 @@ var Mediabunny = (() => {
29024
29024
  }
29025
29025
  if (this.fastStart === "in-memory") {
29026
29026
  } else if (this.fastStart === "reserve") {
29027
- for (const track of this.output._tracks) {
29027
+ for (const track of this.output.tracks) {
29028
29028
  if (track.metadata.maximumPacketCount === void 0) {
29029
29029
  throw new Error(
29030
29030
  "All tracks must specify maximumPacketCount in their metadata when using fastStart: 'reserve'."
@@ -29045,7 +29045,7 @@ var Mediabunny = (() => {
29045
29045
  release();
29046
29046
  }
29047
29047
  allTracksAreKnown() {
29048
- for (const track of this.output._tracks) {
29048
+ for (const track of this.output.tracks) {
29049
29049
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
29050
29050
  return false;
29051
29051
  }
@@ -30439,7 +30439,7 @@ var Mediabunny = (() => {
30439
30439
  return this.ebmlWriter.dataOffsets.get(this.segment);
30440
30440
  }
30441
30441
  allTracksAreKnown() {
30442
- for (const track of this.output._tracks) {
30442
+ for (const track of this.output.tracks) {
30443
30443
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
30444
30444
  return false;
30445
30445
  }
@@ -31288,7 +31288,7 @@ ${cue.notes ?? ""}`;
31288
31288
  throw new Error("Subtitle tracks are not supported.");
31289
31289
  }
31290
31290
  allTracksAreKnown() {
31291
- for (const track of this.output._tracks) {
31291
+ for (const track of this.output.tracks) {
31292
31292
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
31293
31293
  return false;
31294
31294
  }
@@ -31743,7 +31743,7 @@ ${cue.notes ?? ""}`;
31743
31743
  return result;
31744
31744
  }
31745
31745
  allTracksAreKnown() {
31746
- for (const track of this.output._tracks) {
31746
+ for (const track of this.output.tracks) {
31747
31747
  if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
31748
31748
  return false;
31749
31749
  }
@@ -34567,8 +34567,8 @@ ${cue.notes ?? ""}`;
34567
34567
  }
34568
34568
  async start() {
34569
34569
  const release = await this.mutex.acquire();
34570
- const someRelative = this.output._tracks.some((t) => t.metadata.isRelativeToUnixEpoch);
34571
- 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);
34572
34572
  if (someRelative && someNotRelative) {
34573
34573
  throw new Error(
34574
34574
  "All tracks must agree on `relativeToUnixEpoch`: some tracks are relative to the Unix epoch and some are not."
@@ -34580,12 +34580,12 @@ ${cue.notes ?? ""}`;
34580
34580
  let hasVideo = false;
34581
34581
  let illegalPairingDetected = false;
34582
34582
  let keyPacketsOnlyPairingWarned = false;
34583
- for (const track of this.output._tracks) {
34583
+ for (const track of this.output.tracks) {
34584
34584
  if (track.type === "video") {
34585
34585
  hasVideo = true;
34586
34586
  }
34587
34587
  const pairableGroups = /* @__PURE__ */ new Map();
34588
- for (const otherTrack of this.output._tracks) {
34588
+ for (const otherTrack of this.output.tracks) {
34589
34589
  if (track === otherTrack) {
34590
34590
  continue;
34591
34591
  }
@@ -34639,7 +34639,7 @@ ${cue.notes ?? ""}`;
34639
34639
  const variantStreams = [];
34640
34640
  const unpairedVideoTracks = [];
34641
34641
  const unpairedAudioTracks = [];
34642
- for (const track of this.output._tracks) {
34642
+ for (const track of this.output.tracks) {
34643
34643
  const assignedGroupKeys = groupAssignment.get(track);
34644
34644
  if (assignedGroupKeys) {
34645
34645
  assert(assignedGroupKeys.length > 0);
@@ -36361,6 +36361,10 @@ ${cue.notes ?? ""}`;
36361
36361
  * {@link BaseTrackMetadata.group}.
36362
36362
  */
36363
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 = [];
36364
36368
  /** @internal */
36365
36369
  this._onFinalize = null;
36366
36370
  /** @internal */
@@ -36368,8 +36372,6 @@ ${cue.notes ?? ""}`;
36368
36372
  /** @internal */
36369
36373
  this._rootWriterPromise = null;
36370
36374
  /** @internal */
36371
- this._tracks = [];
36372
- /** @internal */
36373
36375
  this._startPromise = null;
36374
36376
  /** @internal */
36375
36377
  this._cancelPromise = null;
@@ -36549,7 +36551,7 @@ ${cue.notes ?? ""}`;
36549
36551
  const metadataCopy = { ...metadata };
36550
36552
  metadataCopy.group ??= this.defaultTrackGroup;
36551
36553
  return this._addTrack(new OutputVideoTrack2(
36552
- this._tracks.length + 1,
36554
+ this.tracks.length + 1,
36553
36555
  this,
36554
36556
  source,
36555
36557
  metadataCopy
@@ -36564,7 +36566,7 @@ ${cue.notes ?? ""}`;
36564
36566
  const metadataCopy = { ...metadata };
36565
36567
  metadataCopy.group ??= this.defaultTrackGroup;
36566
36568
  return this._addTrack(new OutputAudioTrack2(
36567
- this._tracks.length + 1,
36569
+ this.tracks.length + 1,
36568
36570
  this,
36569
36571
  source,
36570
36572
  metadataCopy
@@ -36579,7 +36581,7 @@ ${cue.notes ?? ""}`;
36579
36581
  const metadataCopy = { ...metadata };
36580
36582
  metadataCopy.group ??= this.defaultTrackGroup;
36581
36583
  return this._addTrack(new OutputSubtitleTrack2(
36582
- this._tracks.length + 1,
36584
+ this.tracks.length + 1,
36583
36585
  this,
36584
36586
  source,
36585
36587
  metadataCopy
@@ -36607,7 +36609,7 @@ ${cue.notes ?? ""}`;
36607
36609
  throw new Error("Source is already used for a track.");
36608
36610
  }
36609
36611
  const supportedTrackCounts = this.format.getSupportedTrackCounts();
36610
- const presentTracksOfThisType = this._tracks.reduce(
36612
+ const presentTracksOfThisType = this.tracks.reduce(
36611
36613
  (count, t) => count + (t.type === track.type ? 1 : 0),
36612
36614
  0
36613
36615
  );
@@ -36618,7 +36620,7 @@ ${cue.notes ?? ""}`;
36618
36620
  );
36619
36621
  }
36620
36622
  const maxTotalCount = supportedTrackCounts.total.max;
36621
- if (this._tracks.length === maxTotalCount) {
36623
+ if (this.tracks.length === maxTotalCount) {
36622
36624
  throw new Error(
36623
36625
  `${this.format._name} does not support more than ${maxTotalCount} tracks${maxTotalCount === 1 ? "" : "s"} in total.`
36624
36626
  );
@@ -36657,10 +36659,32 @@ ${cue.notes ?? ""}`;
36657
36659
  );
36658
36660
  }
36659
36661
  }
36660
- this._tracks.push(track);
36662
+ this.tracks.push(track);
36661
36663
  track.source._connectedTrack = track;
36662
36664
  return track;
36663
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
+ }
36664
36688
  /**
36665
36689
  * Starts the creation of the output file. This method should be called after all tracks have been added. Only after
36666
36690
  * the output has started can media samples be added to the tracks.
@@ -36670,7 +36694,7 @@ ${cue.notes ?? ""}`;
36670
36694
  async start() {
36671
36695
  const supportedTrackCounts = this.format.getSupportedTrackCounts();
36672
36696
  for (const trackType of ALL_TRACK_TYPES) {
36673
- const presentTracksOfThisType = this._tracks.reduce(
36697
+ const presentTracksOfThisType = this.tracks.reduce(
36674
36698
  (count, track) => count + (track.type === trackType ? 1 : 0),
36675
36699
  0
36676
36700
  );
@@ -36682,7 +36706,7 @@ ${cue.notes ?? ""}`;
36682
36706
  }
36683
36707
  }
36684
36708
  const totalMinCount = supportedTrackCounts.total.min;
36685
- if (this._tracks.length < totalMinCount) {
36709
+ if (this.tracks.length < totalMinCount) {
36686
36710
  throw new Error(
36687
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"}.`
36688
36712
  );
@@ -36699,7 +36723,7 @@ ${cue.notes ?? ""}`;
36699
36723
  const release = await this._mutex.acquire();
36700
36724
  try {
36701
36725
  await this._muxer.start();
36702
- const promises = this._tracks.map((track) => track.source._start());
36726
+ const promises = this.tracks.map((track) => track.source._start());
36703
36727
  await Promise.all(promises);
36704
36728
  } finally {
36705
36729
  release();
@@ -36734,7 +36758,7 @@ ${cue.notes ?? ""}`;
36734
36758
  this.state = "canceled";
36735
36759
  const release = await this._mutex.acquire();
36736
36760
  try {
36737
- const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(true));
36761
+ const promises = this.tracks.map((x) => x.source._flushOrWaitForOngoingClose(true));
36738
36762
  await Promise.all(promises);
36739
36763
  await Promise.all([...this._unfinalizedTargets].map((target) => target._close()));
36740
36764
  this._unfinalizedTargets.clear();
@@ -36762,7 +36786,7 @@ ${cue.notes ?? ""}`;
36762
36786
  this.state = "finalizing";
36763
36787
  const release = await this._mutex.acquire();
36764
36788
  try {
36765
- const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(false));
36789
+ const promises = this.tracks.map((x) => x.source._flushOrWaitForOngoingClose(false));
36766
36790
  await Promise.all(promises);
36767
36791
  await this._muxer.finalize();
36768
36792
  if (this._rootWriterPromise) {
@@ -36901,14 +36925,15 @@ ${cue.notes ?? ""}`;
36901
36925
  var Conversion = class _Conversion {
36902
36926
  /** Creates a new Conversion instance (duh). */
36903
36927
  constructor(options) {
36904
- /** @internal */
36905
- this._addedCounts = {
36906
- video: 0,
36907
- audio: 0,
36908
- subtitle: 0
36909
- };
36910
- /** @internal */
36911
- 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";
36912
36937
  /** @internal */
36913
36938
  this._nextOutputTrackId = 0;
36914
36939
  /** @internal */
@@ -36916,18 +36941,22 @@ ${cue.notes ?? ""}`;
36916
36941
  /** @internal */
36917
36942
  this._outputOwnTrackGroups = [];
36918
36943
  /** @internal */
36919
- this._trackPromises = [];
36944
+ this._trackPumps = [];
36945
+ /** @internal */
36946
+ this._composable = false;
36920
36947
  /** @internal */
36921
36948
  this._executed = false;
36922
36949
  /** @internal */
36923
- this._synchronizer = new TrackSynchronizer();
36950
+ this._executionUntil = Infinity;
36951
+ /** @internal */
36952
+ this._pauseRequested = false;
36953
+ /** @internal */
36954
+ this._synchronizer = new TrackSynchronizer(this);
36924
36955
  /** @internal */
36925
36956
  this._totalDuration = null;
36926
36957
  /** @internal */
36927
36958
  this._maxTimestamps = /* @__PURE__ */ new Map();
36928
36959
  // Track ID -> timestamp
36929
- /** @internal */
36930
- this._canceled = false;
36931
36960
  /**
36932
36961
  * A callback that is fired whenever the conversion progresses. Gets passed as first argument a number between
36933
36962
  * 0 and 1, indicating the completion of the conversion. Note that a progress of 1 doesn't necessarily mean the
@@ -36944,7 +36973,8 @@ ${cue.notes ?? ""}`;
36944
36973
  this._lastProgress = 0;
36945
36974
  /**
36946
36975
  * Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
36947
- * the `discardedTracks` field for reasons.
36976
+ * the `discardedTracks` field for reasons. Composable conversions are always valid, even if they utilize
36977
+ * zero tracks.
36948
36978
  *
36949
36979
  * Note: a conversion having discarded tracks does not automatically mean it is invalid; if the remaining, utilized
36950
36980
  * tracks make for a valid output file, the conversion is still allowed.
@@ -36971,8 +37001,23 @@ ${cue.notes ?? ""}`;
36971
37001
  "options.tracks, when provided, must be either 'all' or 'primary'."
36972
37002
  );
36973
37003
  }
36974
- if (options.output._tracks.length > 0 || Object.keys(options.output._metadataTags).length > 0 || options.output.state !== "pending") {
36975
- 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
+ }
36976
37021
  }
36977
37022
  if (options.video !== void 0 && typeof options.video !== "function") {
36978
37023
  if (Array.isArray(options.video)) {
@@ -37016,11 +37061,9 @@ ${cue.notes ?? ""}`;
37016
37061
  throw new TypeError("options.showWarnings, when provided, must be a boolean.");
37017
37062
  }
37018
37063
  this._options = options;
37064
+ this._composable = composable;
37019
37065
  this.input = options.input;
37020
37066
  this.output = options.output;
37021
- const { promise: started, resolve: start } = promiseWithResolvers();
37022
- this._started = started;
37023
- this._start = start;
37024
37067
  }
37025
37068
  /** Initializes a new conversion process without starting the conversion. */
37026
37069
  static async init(options) {
@@ -37132,7 +37175,7 @@ ${cue.notes ?? ""}`;
37132
37175
  const track = filteredTracks[i];
37133
37176
  const options = filteredTrackOptions[i];
37134
37177
  for (const option of options) {
37135
- if (this._totalTrackCount === outputTrackCounts.total.max) {
37178
+ if (this.output.tracks.length === outputTrackCounts.total.max) {
37136
37179
  this.discardedTracks.push({
37137
37180
  track,
37138
37181
  reason: "max_track_count_reached",
@@ -37140,7 +37183,11 @@ ${cue.notes ?? ""}`;
37140
37183
  });
37141
37184
  continue;
37142
37185
  }
37143
- 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) {
37144
37191
  this.discardedTracks.push({
37145
37192
  track,
37146
37193
  reason: "max_track_count_of_type_reached",
@@ -37171,22 +37218,28 @@ ${cue.notes ?? ""}`;
37171
37218
  }
37172
37219
  }
37173
37220
  }
37174
- const inputTags = await this.input.getMetadataTags();
37175
- let outputTags;
37176
- if (this._options.tags) {
37177
- const result = typeof this._options.tags === "function" ? await this._options.tags(inputTags) : this._options.tags;
37178
- validateMetadataTags(result);
37179
- outputTags = result;
37180
- } else {
37181
- 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);
37182
37237
  }
37183
- const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
37184
- const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
37185
- if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
37186
- delete outputTags.raw;
37238
+ if (!this._composable) {
37239
+ this.isValid = this.output.hasEnoughTracks();
37240
+ } else {
37241
+ this.isValid = true;
37187
37242
  }
37188
- this.output.setMetadataTags(outputTags);
37189
- 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;
37190
37243
  if (this._options.showWarnings ?? true) {
37191
37244
  const warnElements = [];
37192
37245
  const unintentionallyDiscardedTracks = this.discardedTracks.filter((x) => x.reason !== "discarded_by_user");
@@ -37272,59 +37325,116 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37272
37325
  return elements;
37273
37326
  }
37274
37327
  /**
37275
- * 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}.
37276
37332
  *
37277
- * Will throw if `isValid` is `false`.
37333
+ * Execution will throw if `isValid` is `false`.
37278
37334
  */
37279
- 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
+ }
37280
37345
  if (!this.isValid) {
37281
37346
  throw new Error(
37282
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("")
37283
37348
  );
37284
37349
  }
37285
- if (this._executed) {
37286
- 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.");
37287
37352
  }
37288
- this._executed = true;
37289
- for (const id of this._outputTrackIds) {
37290
- this._synchronizer.declareTrack(id);
37353
+ if (this.state === "canceled") {
37354
+ throw new ConversionCanceledError();
37291
37355
  }
37292
- if (this.onProgress) {
37293
- const uniqueUtilizedTracks = new Set(this.utilizedTracks);
37294
- const durationPromises = [...uniqueUtilizedTracks].map(async (track) => {
37295
- if (await track.isLive()) {
37296
- return Infinity;
37297
- }
37298
- return await track.getDurationFromMetadata() ?? await track.computeDuration();
37299
- });
37300
- const duration = Math.max(0, ...await Promise.all(durationPromises));
37301
- this._computeProgress = true;
37302
- this._totalDuration = Math.min(
37303
- duration - this._startTimestamp,
37304
- 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."
37305
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;
37306
37382
  for (const id of this._outputTrackIds) {
37307
- 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?.();
37308
37413
  }
37309
- this.onProgress?.(0, 0);
37310
37414
  }
37311
- await this.output.start();
37312
- this._start();
37313
37415
  try {
37314
- await Promise.all(this._trackPromises);
37416
+ await Promise.all(this._trackPumps.map((x) => x.resolvers.promise));
37315
37417
  } catch (error) {
37316
- if (!this._canceled) {
37418
+ if (this.state !== "canceled") {
37317
37419
  void this.cancel();
37318
37420
  }
37319
37421
  throw error;
37422
+ } finally {
37423
+ options.pauseSignal?.removeEventListener("abort", onPause);
37320
37424
  }
37321
- if (this._canceled) {
37425
+ if (this.state === "canceled") {
37322
37426
  throw new ConversionCanceledError();
37323
37427
  }
37324
- await this.output.finalize();
37325
- if (this._computeProgress) {
37326
- const minTimestamp = Math.min(...this._maxTimestamps.values());
37327
- 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
+ }
37328
37438
  }
37329
37439
  }
37330
37440
  /**
@@ -37332,15 +37442,21 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37332
37442
  * Does nothing if the conversion is already complete.
37333
37443
  */
37334
37444
  async cancel() {
37335
- if (this.output.state === "finalizing" || this.output.state === "finalized") {
37445
+ if (this.state === "done") {
37336
37446
  return;
37337
37447
  }
37338
- if (this._canceled) {
37448
+ if (this.state === "canceled") {
37339
37449
  Logging._warn("Conversion already canceled.");
37340
37450
  return;
37341
37451
  }
37342
- this._canceled = true;
37343
- 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
+ }
37344
37460
  }
37345
37461
  /** @internal */
37346
37462
  async _processVideoTrack(track, trackOptions, outputTrackId) {
@@ -37386,14 +37502,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37386
37502
  if (!needsTranscode) {
37387
37503
  const source = new EncodedVideoPacketSource(sourceCodec);
37388
37504
  videoSource = source;
37389
- this._trackPromises.push((async () => {
37390
- await this._started;
37505
+ this._registerTrackPump(async (pump) => {
37391
37506
  const sink = new EncodedPacketSink(track);
37392
37507
  const decoderConfig = await track.getDecoderConfig();
37393
37508
  const meta = { decoderConfig: decoderConfig ?? void 0 };
37394
37509
  for await (const packet of sink.packets(void 0, void 0, { verifyKeyPackets: true })) {
37395
- if (this._canceled) {
37396
- return;
37510
+ if (this.state === "canceled") {
37511
+ break;
37397
37512
  }
37398
37513
  if (packet.timestamp >= this._endTimestamp) {
37399
37514
  break;
@@ -37408,10 +37523,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37408
37523
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
37409
37524
  await this._synchronizer.wait(modifiedPacket.timestamp);
37410
37525
  }
37526
+ await this._checkpoint(pump, modifiedPacket.timestamp);
37411
37527
  }
37412
37528
  source.close();
37413
37529
  this._synchronizer.closeTrack(outputTrackId);
37414
- })());
37530
+ });
37415
37531
  } else {
37416
37532
  const canDecode2 = await track.canDecode();
37417
37533
  if (!canDecode2) {
@@ -37507,15 +37623,14 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37507
37623
  };
37508
37624
  const source = new VideoSampleSource(encodingConfig);
37509
37625
  videoSource = source;
37510
- this._trackPromises.push((async () => {
37511
- await this._started;
37626
+ this._registerTrackPump(async (pump) => {
37512
37627
  const sink = new VideoSampleSink(track);
37513
37628
  for await (var _sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37514
37629
  var _stack2 = [];
37515
37630
  try {
37516
37631
  const sample = __using(_stack2, _sample);
37517
- if (this._canceled) {
37518
- return;
37632
+ if (this.state === "canceled") {
37633
+ break;
37519
37634
  }
37520
37635
  const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37521
37636
  sample.setTimestamp(adjustedSampleTimestamp);
@@ -37526,6 +37641,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37526
37641
  if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37527
37642
  await this._synchronizer.wait(lastSampleTimestamp);
37528
37643
  }
37644
+ await this._checkpoint(pump, lastSampleTimestamp);
37529
37645
  }
37530
37646
  } catch (_2) {
37531
37647
  var _error2 = _2, _hasError2 = true;
@@ -37535,10 +37651,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37535
37651
  }
37536
37652
  source.close();
37537
37653
  this._synchronizer.closeTrack(outputTrackId);
37538
- })());
37654
+ });
37539
37655
  }
37540
37656
  let ownGroup = null;
37541
- if (!trackOptions.group) {
37657
+ if (!trackOptions.group && !this._composable) {
37542
37658
  ownGroup = new OutputTrackGroup();
37543
37659
  }
37544
37660
  const videoTrackLanguageCode = await track.getLanguageCode();
@@ -37551,8 +37667,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37551
37667
  rotation: outputTrackRotation,
37552
37668
  group: ownGroup ?? trackOptions.group
37553
37669
  });
37554
- this._addedCounts.video++;
37555
- this._totalTrackCount++;
37556
37670
  this.utilizedTracks.push(track);
37557
37671
  this._outputTrackIds.push(outputTrackId);
37558
37672
  this._outputOwnTrackGroups.push(ownGroup);
@@ -37580,14 +37694,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37580
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) {
37581
37695
  const source = new EncodedAudioPacketSource(sourceCodec);
37582
37696
  audioSource = source;
37583
- this._trackPromises.push((async () => {
37584
- await this._started;
37697
+ this._registerTrackPump(async (pump) => {
37585
37698
  const sink = new EncodedPacketSink(track);
37586
37699
  const decoderConfig = await track.getDecoderConfig();
37587
37700
  const meta = { decoderConfig: decoderConfig ?? void 0 };
37588
37701
  for await (const packet of sink.packets()) {
37589
- if (this._canceled) {
37590
- return;
37702
+ if (this.state === "canceled") {
37703
+ break;
37591
37704
  }
37592
37705
  if (packet.timestamp >= this._endTimestamp) {
37593
37706
  break;
@@ -37601,10 +37714,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37601
37714
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
37602
37715
  await this._synchronizer.wait(modifiedPacket.timestamp);
37603
37716
  }
37717
+ await this._checkpoint(pump, modifiedPacket.timestamp);
37604
37718
  }
37605
37719
  source.close();
37606
37720
  this._synchronizer.closeTrack(outputTrackId);
37607
- })());
37721
+ });
37608
37722
  } else {
37609
37723
  const canDecode2 = await track.canDecode();
37610
37724
  if (!canDecode2) {
@@ -37669,8 +37783,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37669
37783
  };
37670
37784
  const source = new AudioSampleSource(encodingConfig);
37671
37785
  audioSource = source;
37672
- this._trackPromises.push((async () => {
37673
- await this._started;
37786
+ this._registerTrackPump(async (pump) => {
37674
37787
  const sink = new AudioSampleSink(track);
37675
37788
  for await (var _sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37676
37789
  var _stack3 = [];
@@ -37678,8 +37791,8 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37678
37791
  const sample = __using(_stack3, _sample);
37679
37792
  var _stack2 = [];
37680
37793
  try {
37681
- if (this._canceled) {
37682
- return;
37794
+ if (this.state === "canceled") {
37795
+ break;
37683
37796
  }
37684
37797
  if (needsPadding) {
37685
37798
  var _stack = [];
@@ -37700,7 +37813,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37700
37813
  sampleRate: originalSampleRate,
37701
37814
  timestamp: 0
37702
37815
  }));
37703
- await this._registerAudioSample(silentSample, source, outputTrackId, () => lastSampleTimestamp);
37816
+ await this._registerAudioSample(
37817
+ pump,
37818
+ silentSample,
37819
+ source,
37820
+ outputTrackId,
37821
+ () => lastSampleTimestamp
37822
+ );
37704
37823
  needsPadding = false;
37705
37824
  } catch (_) {
37706
37825
  var _error = _, _hasError = true;
@@ -37730,7 +37849,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37730
37849
  }
37731
37850
  const finalSample = __using(_stack2, finalSampleLet);
37732
37851
  finalSample.setTimestamp(finalSample.timestamp - this._startTimestamp);
37733
- await this._registerAudioSample(finalSample, source, outputTrackId, () => lastSampleTimestamp);
37852
+ await this._registerAudioSample(
37853
+ pump,
37854
+ finalSample,
37855
+ source,
37856
+ outputTrackId,
37857
+ () => lastSampleTimestamp
37858
+ );
37734
37859
  } catch (_2) {
37735
37860
  var _error2 = _2, _hasError2 = true;
37736
37861
  } finally {
@@ -37744,10 +37869,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37744
37869
  }
37745
37870
  source.close();
37746
37871
  this._synchronizer.closeTrack(outputTrackId);
37747
- })());
37872
+ });
37748
37873
  }
37749
37874
  let ownGroup = null;
37750
- if (!trackOptions.group) {
37875
+ if (!trackOptions.group && !this._composable) {
37751
37876
  ownGroup = new OutputTrackGroup();
37752
37877
  }
37753
37878
  const audioTrackLanguageCode = await track.getLanguageCode();
@@ -37758,14 +37883,12 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37758
37883
  disposition: await track.getDisposition(),
37759
37884
  group: ownGroup ?? trackOptions.group
37760
37885
  });
37761
- this._addedCounts.audio++;
37762
- this._totalTrackCount++;
37763
37886
  this.utilizedTracks.push(track);
37764
37887
  this._outputTrackIds.push(outputTrackId);
37765
37888
  this._outputOwnTrackGroups.push(ownGroup);
37766
37889
  }
37767
37890
  /** @internal */
37768
- async _registerAudioSample(sample, source, outputTrackId, getLastSampleTimestamp) {
37891
+ async _registerAudioSample(pump, sample, source, outputTrackId, getLastSampleTimestamp) {
37769
37892
  this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37770
37893
  await source.add(sample);
37771
37894
  sample.close();
@@ -37774,6 +37897,33 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37774
37897
  if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37775
37898
  await this._synchronizer.wait(lastSampleTimestamp);
37776
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;
37777
37927
  }
37778
37928
  }
37779
37929
  /** @internal */
@@ -37803,10 +37953,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37803
37953
  };
37804
37954
  var MAX_TIMESTAMP_GAP = 1;
37805
37955
  var TrackSynchronizer = class {
37806
- constructor() {
37956
+ constructor(conversion) {
37807
37957
  this.maxTimestamps = /* @__PURE__ */ new Map();
37808
37958
  // Track ID -> timestamp
37809
37959
  this.resolvers = [];
37960
+ this.conversion = conversion;
37810
37961
  }
37811
37962
  declareTrack(trackId) {
37812
37963
  this.maxTimestamps.set(trackId, 0);
@@ -37816,6 +37967,9 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37816
37967
  assert(currentValue !== void 0);
37817
37968
  this.maxTimestamps.set(trackId, Math.max(timestamp, currentValue));
37818
37969
  const newMin = this.computeMinAndMaybeResolve();
37970
+ if (this.conversion.state === "canceled" || this.conversion._pauseRequested || timestamp >= this.conversion._executionUntil) {
37971
+ return false;
37972
+ }
37819
37973
  return timestamp - newMin > MAX_TIMESTAMP_GAP;
37820
37974
  }
37821
37975
  wait(timestamp) {
@@ -37830,6 +37984,12 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37830
37984
  this.maxTimestamps.delete(trackId);
37831
37985
  this.computeMinAndMaybeResolve();
37832
37986
  }
37987
+ resolveAll() {
37988
+ for (const entry of this.resolvers) {
37989
+ entry.resolve();
37990
+ }
37991
+ this.resolvers.length = 0;
37992
+ }
37833
37993
  computeMinAndMaybeResolve() {
37834
37994
  let newMin = Infinity;
37835
37995
  for (const [, timestamp] of this.maxTimestamps) {