mediabunny 1.45.5 → 1.47.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 (38) hide show
  1. package/dist/bundles/mediabunny.cjs +238 -206
  2. package/dist/bundles/mediabunny.min.cjs +11 -11
  3. package/dist/bundles/mediabunny.min.mjs +11 -11
  4. package/dist/bundles/mediabunny.mjs +238 -206
  5. package/dist/bundles/mediabunny.node.cjs +238 -206
  6. package/dist/mediabunny.d.ts +25 -0
  7. package/dist/modules/src/conversion.d.ts.map +1 -1
  8. package/dist/modules/src/conversion.js +91 -163
  9. package/dist/modules/src/encode.d.ts +4 -0
  10. package/dist/modules/src/encode.d.ts.map +1 -1
  11. package/dist/modules/src/encode.js +6 -0
  12. package/dist/modules/src/index.d.ts +1 -1
  13. package/dist/modules/src/index.d.ts.map +1 -1
  14. package/dist/modules/src/input.d.ts.map +1 -1
  15. package/dist/modules/src/input.js +6 -2
  16. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  17. package/dist/modules/src/isobmff/isobmff-demuxer.js +8 -7
  18. package/dist/modules/src/media-source.d.ts.map +1 -1
  19. package/dist/modules/src/media-source.js +23 -9
  20. package/dist/modules/src/misc.d.ts +8 -0
  21. package/dist/modules/src/misc.d.ts.map +1 -1
  22. package/dist/modules/src/resample.d.ts +1 -4
  23. package/dist/modules/src/resample.d.ts.map +1 -1
  24. package/dist/modules/src/resample.js +9 -9
  25. package/dist/modules/src/sample.d.ts +13 -1
  26. package/dist/modules/src/sample.d.ts.map +1 -1
  27. package/dist/modules/src/sample.js +89 -4
  28. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/conversion.ts +108 -215
  31. package/src/encode.ts +10 -0
  32. package/src/index.ts +1 -0
  33. package/src/input.ts +6 -2
  34. package/src/isobmff/isobmff-demuxer.ts +9 -7
  35. package/src/media-source.ts +30 -9
  36. package/src/misc.ts +9 -0
  37. package/src/resample.ts +10 -15
  38. package/src/sample.ts +111 -4
@@ -6890,35 +6890,39 @@ var Mediabunny = (() => {
6890
6890
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
6891
6891
  if (this.currentTrack) {
6892
6892
  const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
6893
- if (trackData) {
6894
- this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
6895
- trackData.presentationTimestamps = trackData.samples.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i })).sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
6896
- for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
6897
- const currentEntry = trackData.presentationTimestamps[i];
6898
- const currentSample = trackData.samples[currentEntry.sampleIndex];
6899
- if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
6900
- trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
6893
+ cond:
6894
+ if (trackData) {
6895
+ if (trackData.samples.length === 0) {
6896
+ this.currentFragment.trackData.delete(this.currentTrack.id);
6897
+ break cond;
6901
6898
  }
6902
- if (i < trackData.presentationTimestamps.length - 1) {
6903
- const nextEntry = trackData.presentationTimestamps[i + 1];
6904
- const duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
6905
- currentSample.duration = duration;
6899
+ trackData.presentationTimestamps = trackData.samples.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i })).sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
6900
+ for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
6901
+ const currentEntry = trackData.presentationTimestamps[i];
6902
+ const currentSample = trackData.samples[currentEntry.sampleIndex];
6903
+ if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
6904
+ trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
6905
+ }
6906
+ if (i < trackData.presentationTimestamps.length - 1) {
6907
+ const nextEntry = trackData.presentationTimestamps[i + 1];
6908
+ const duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
6909
+ currentSample.duration = duration;
6910
+ }
6911
+ }
6912
+ const firstSample = trackData.samples[trackData.presentationTimestamps[0].sampleIndex];
6913
+ const lastSample = trackData.samples[last(trackData.presentationTimestamps).sampleIndex];
6914
+ trackData.startTimestamp = firstSample.presentationTimestamp;
6915
+ trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
6916
+ const { currentFragmentState } = this.currentTrack;
6917
+ assert(currentFragmentState);
6918
+ if (currentFragmentState.startTimestamp !== null) {
6919
+ offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp);
6920
+ trackData.startTimestampIsFinal = true;
6921
+ }
6922
+ if (currentFragmentState.encryptionAuxInfo && !trackData.samples[0].encryption) {
6923
+ trackData.encryptionAuxInfo = currentFragmentState.encryptionAuxInfo;
6906
6924
  }
6907
6925
  }
6908
- const firstSample = trackData.samples[trackData.presentationTimestamps[0].sampleIndex];
6909
- const lastSample = trackData.samples[last(trackData.presentationTimestamps).sampleIndex];
6910
- trackData.startTimestamp = firstSample.presentationTimestamp;
6911
- trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
6912
- const { currentFragmentState } = this.currentTrack;
6913
- assert(currentFragmentState);
6914
- if (currentFragmentState.startTimestamp !== null) {
6915
- offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp);
6916
- trackData.startTimestampIsFinal = true;
6917
- }
6918
- if (currentFragmentState.encryptionAuxInfo && !trackData.samples[0].encryption) {
6919
- trackData.encryptionAuxInfo = currentFragmentState.encryptionAuxInfo;
6920
- }
6921
- }
6922
6926
  this.currentTrack.currentFragmentState = null;
6923
6927
  this.currentTrack = null;
6924
6928
  }
@@ -7051,10 +7055,6 @@ var Mediabunny = (() => {
7051
7055
  };
7052
7056
  this.currentFragment.trackData.set(track.id, trackData);
7053
7057
  }
7054
- if (sampleCount === 0) {
7055
- this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
7056
- break;
7057
- }
7058
7058
  for (let i = 0; i < sampleCount; i++) {
7059
7059
  let sampleDuration;
7060
7060
  if (sampleDurationPresent) {
@@ -7100,6 +7100,7 @@ var Mediabunny = (() => {
7100
7100
  trackData.currentOffset += sampleSize;
7101
7101
  trackData.currentTimestamp += sampleDuration;
7102
7102
  }
7103
+ this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
7103
7104
  }
7104
7105
  ;
7105
7106
  break;
@@ -19068,7 +19069,7 @@ var Mediabunny = (() => {
19068
19069
  "init.displayWidth and init.displayHeight must be either both provided or both omitted."
19069
19070
  );
19070
19071
  }
19071
- this._data = toUint8Array(data).slice();
19072
+ this._data = init._doNotCopy ? toUint8Array(data) : toUint8Array(data).slice();
19072
19073
  this._layout = init.layout ?? createDefaultPlaneLayout(init.format, init.codedWidth, init.codedHeight);
19073
19074
  this.format = init.format;
19074
19075
  this.rotation = init.rotation ?? 0;
@@ -19178,7 +19179,11 @@ var Mediabunny = (() => {
19178
19179
  // Firefox has VideoFrame glitches with opaque canvases
19179
19180
  willReadFrequently: true
19180
19181
  });
19181
- assert(context);
19182
+ if (!context) {
19183
+ throw new Error(
19184
+ "OffscreenCanvas must have support for the '2d' context in order to create a VideoSample from this data."
19185
+ );
19186
+ }
19182
19187
  context.drawImage(data, 0, 0);
19183
19188
  this._data = canvas;
19184
19189
  this._layout = null;
@@ -19243,6 +19248,7 @@ var Mediabunny = (() => {
19243
19248
  "Invalid data type: Must be a BufferSource, CanvasImageSource, or VideoSampleResource."
19244
19249
  );
19245
19250
  }
19251
+ this.encodeOptions = init?.encodeOptions ?? {};
19246
19252
  this.pixelAspectRatio = simplifyRational({
19247
19253
  num: this.squarePixelWidth * this.codedHeight,
19248
19254
  den: this.squarePixelHeight * this.codedWidth
@@ -19290,13 +19296,15 @@ var Mediabunny = (() => {
19290
19296
  return new _VideoSample(this._data, {
19291
19297
  timestamp: this.timestamp,
19292
19298
  duration: this.duration,
19293
- rotation: this.rotation
19299
+ rotation: this.rotation,
19300
+ encodeOptions: this.encodeOptions
19294
19301
  });
19295
19302
  } else if (isVideoFrame(this._data)) {
19296
19303
  return new _VideoSample(this._data.clone(), {
19297
19304
  timestamp: this.timestamp,
19298
19305
  duration: this.duration,
19299
- rotation: this.rotation
19306
+ rotation: this.rotation,
19307
+ encodeOptions: this.encodeOptions
19300
19308
  });
19301
19309
  } else if (this._data instanceof Uint8Array) {
19302
19310
  assert(this._layout);
@@ -19311,7 +19319,10 @@ var Mediabunny = (() => {
19311
19319
  rotation: this.rotation,
19312
19320
  visibleRect: this.visibleRect,
19313
19321
  displayWidth: this.displayWidth,
19314
- displayHeight: this.displayHeight
19322
+ displayHeight: this.displayHeight,
19323
+ encodeOptions: this.encodeOptions,
19324
+ // It's already been copied, if we copy it again we make the clone unnecessarily expensive
19325
+ _doNotCopy: true
19315
19326
  });
19316
19327
  } else {
19317
19328
  return new _VideoSample(this._data, {
@@ -19324,7 +19335,8 @@ var Mediabunny = (() => {
19324
19335
  rotation: this.rotation,
19325
19336
  visibleRect: this.visibleRect,
19326
19337
  displayWidth: this.displayWidth,
19327
- displayHeight: this.displayHeight
19338
+ displayHeight: this.displayHeight,
19339
+ encodeOptions: this.encodeOptions
19328
19340
  });
19329
19341
  }
19330
19342
  }
@@ -19886,7 +19898,11 @@ var Mediabunny = (() => {
19886
19898
  const context = canvas.getContext("2d", {
19887
19899
  alpha: true
19888
19900
  });
19889
- assert(context);
19901
+ if (!context) {
19902
+ throw new Error(
19903
+ "The '2d' canvas context is required to transform VideoSamples. Register a custom transformer using registerVideoSampleTransformer to work around this limitation."
19904
+ );
19905
+ }
19890
19906
  if (description.alpha === "discard") {
19891
19907
  context.fillStyle = "black";
19892
19908
  context.fillRect(0, 0, description.width, description.height);
@@ -19926,6 +19942,13 @@ var Mediabunny = (() => {
19926
19942
  }
19927
19943
  this.duration = newDuration;
19928
19944
  }
19945
+ /** Sets the encode options used when this sample is passed to an encoder. */
19946
+ setEncodeOptions(newEncodeOptions) {
19947
+ if (!newEncodeOptions || typeof newEncodeOptions !== "object") {
19948
+ throw new TypeError("newEncodeOptions must be an object.");
19949
+ }
19950
+ this.encodeOptions = newEncodeOptions;
19951
+ }
19929
19952
  /** Calls `.close()`. */
19930
19953
  [Symbol.dispose]() {
19931
19954
  this.close();
@@ -20555,6 +20578,65 @@ var Mediabunny = (() => {
20555
20578
  });
20556
20579
  }
20557
20580
  }
20581
+ /**
20582
+ * Returns a new {@link AudioSample} containing only the frames in the range [startSample, endSample). Both bounds
20583
+ * must lie within this sample's range of frames. The returned sample's timestamp is shifted to match the start of
20584
+ * the trimmed section.
20585
+ */
20586
+ trim(startSample, endSample = this.numberOfFrames) {
20587
+ if (!Number.isInteger(startSample) || startSample < 0) {
20588
+ throw new TypeError("startSample must be a non-negative integer.");
20589
+ }
20590
+ if (!Number.isInteger(endSample) || endSample < 0) {
20591
+ throw new TypeError("endSample must be a non-negative integer.");
20592
+ }
20593
+ if (startSample > this.numberOfFrames) {
20594
+ throw new RangeError("startSample out of range.");
20595
+ }
20596
+ if (endSample > this.numberOfFrames) {
20597
+ throw new RangeError("endSample out of range.");
20598
+ }
20599
+ if (endSample < startSample) {
20600
+ throw new RangeError("endSample must not be less than startSample.");
20601
+ }
20602
+ if (this._closed) {
20603
+ throw new Error("AudioSample is closed.");
20604
+ }
20605
+ const frameCount = endSample - startSample;
20606
+ const bytesPerSample = getBytesPerSample(this.format);
20607
+ let data;
20608
+ if (formatIsPlanar(this.format)) {
20609
+ const planeSize = frameCount * bytesPerSample;
20610
+ data = new Uint8Array(planeSize * this.numberOfChannels);
20611
+ if (frameCount > 0) {
20612
+ for (let i = 0; i < this.numberOfChannels; i++) {
20613
+ this.copyTo(data.subarray(i * planeSize, (i + 1) * planeSize), {
20614
+ planeIndex: i,
20615
+ format: this.format,
20616
+ frameOffset: startSample,
20617
+ frameCount
20618
+ });
20619
+ }
20620
+ }
20621
+ } else {
20622
+ data = new Uint8Array(frameCount * this.numberOfChannels * bytesPerSample);
20623
+ if (frameCount > 0) {
20624
+ this.copyTo(data, {
20625
+ planeIndex: 0,
20626
+ format: this.format,
20627
+ frameOffset: startSample,
20628
+ frameCount
20629
+ });
20630
+ }
20631
+ }
20632
+ return new _AudioSample({
20633
+ data,
20634
+ format: this.format,
20635
+ sampleRate: this.sampleRate,
20636
+ numberOfChannels: this.numberOfChannels,
20637
+ timestamp: this.timestamp + startSample / this.sampleRate
20638
+ });
20639
+ }
20558
20640
  /**
20559
20641
  * Closes this audio sample, releasing held resources. Audio samples should be closed as soon as they are not
20560
20642
  * needed anymore.
@@ -20973,6 +21055,9 @@ var Mediabunny = (() => {
20973
21055
  if (config.onEncoderConfig !== void 0 && typeof config.onEncoderConfig !== "function") {
20974
21056
  throw new TypeError("config.onEncoderConfig, when provided, must be a function.");
20975
21057
  }
21058
+ if (config.onEncodedSample !== void 0 && typeof config.onEncodedSample !== "function") {
21059
+ throw new TypeError("config.onEncodedSample, when provided, must be a function.");
21060
+ }
20976
21061
  validateVideoEncodingAdditionalOptions(config.codec, config);
20977
21062
  };
20978
21063
  var validateVideoEncodingAdditionalOptions = (codec, options) => {
@@ -21068,6 +21153,9 @@ var Mediabunny = (() => {
21068
21153
  if (config.onEncoderConfig !== void 0 && typeof config.onEncoderConfig !== "function") {
21069
21154
  throw new TypeError("config.onEncoderConfig, when provided, must be a function.");
21070
21155
  }
21156
+ if (config.onEncodedSample !== void 0 && typeof config.onEncodedSample !== "function") {
21157
+ throw new TypeError("config.onEncodedSample, when provided, must be a function.");
21158
+ }
21071
21159
  validateAudioEncodingAdditionalOptions(config.codec, config);
21072
21160
  };
21073
21161
  var validateAudioEncodingAdditionalOptions = (codec, options) => {
@@ -24515,7 +24603,10 @@ var Mediabunny = (() => {
24515
24603
  ref.free();
24516
24604
  }
24517
24605
  this._sourceRefs.length = 0;
24518
- void this._demuxerPromise?.then((demuxer) => demuxer.dispose());
24606
+ if (this._demuxerPromise) {
24607
+ void this._demuxerPromise.then((demuxer) => demuxer.dispose()).catch(() => {
24608
+ });
24609
+ }
24519
24610
  }
24520
24611
  /**
24521
24612
  * Calls `.dispose()` on the input, implementing the `Disposable` interface for use with
@@ -31968,17 +32059,17 @@ ${cue.notes ?? ""}`;
31968
32059
  constructor(options) {
31969
32060
  this.sourceSampleRate = null;
31970
32061
  this.sourceNumberOfChannels = null;
32062
+ this.startTime = null;
32063
+ /** Start frame of current buffer */
32064
+ this.bufferStartFrame = 0;
31971
32065
  /** The highest index written to in the current buffer */
31972
32066
  this.maxWrittenFrame = null;
31973
32067
  this.targetSampleRate = options.targetSampleRate;
31974
32068
  this.targetNumberOfChannels = options.targetNumberOfChannels;
31975
- this.endTime = options.endTime;
31976
32069
  this.onSample = options.onSample;
31977
32070
  this.bufferSizeInFrames = Math.floor(this.targetSampleRate * 5);
31978
32071
  this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels;
31979
32072
  this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
31980
- this.bufferStartFrame = Math.floor(options.startTime * this.targetSampleRate);
31981
- this.timestampOffset = options.startTime - this.bufferStartFrame / this.targetSampleRate;
31982
32073
  }
31983
32074
  /**
31984
32075
  * Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
@@ -32068,16 +32159,18 @@ ${cue.notes ?? ""}`;
32068
32159
  if (this.sourceSampleRate === null) {
32069
32160
  this.sourceSampleRate = audioSample.sampleRate;
32070
32161
  this.sourceNumberOfChannels = audioSample.numberOfChannels;
32162
+ this.startTime = audioSample.timestamp;
32071
32163
  this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
32072
32164
  this.doChannelMixerSetup();
32073
32165
  }
32166
+ assert(this.startTime !== null);
32074
32167
  const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
32075
32168
  this.ensureTempBufferSize(requiredSamples);
32076
32169
  const sourceDataSize = audioSample.allocationSize({ planeIndex: 0, format: "f32" });
32077
32170
  const sourceView = new Float32Array(this.tempSourceBuffer.buffer, 0, sourceDataSize / 4);
32078
32171
  audioSample.copyTo(sourceView, { planeIndex: 0, format: "f32" });
32079
- const inputStartTime = audioSample.timestamp;
32080
- const inputEndTime = Math.min(audioSample.timestamp + audioSample.duration, this.endTime);
32172
+ const inputStartTime = audioSample.timestamp - this.startTime;
32173
+ const inputEndTime = inputStartTime + audioSample.duration;
32081
32174
  const outputStartFrame = Math.floor(inputStartTime * this.targetSampleRate);
32082
32175
  const outputEndFrame = Math.ceil(inputEndTime * this.targetSampleRate);
32083
32176
  for (let outputFrame = outputStartFrame; outputFrame < outputEndFrame; outputFrame++) {
@@ -32120,15 +32213,15 @@ ${cue.notes ?? ""}`;
32120
32213
  if (this.maxWrittenFrame === null) {
32121
32214
  return;
32122
32215
  }
32216
+ assert(this.startTime !== null);
32123
32217
  const samplesWritten = (this.maxWrittenFrame + 1) * this.targetNumberOfChannels;
32124
32218
  const outputData = new Float32Array(samplesWritten);
32125
32219
  outputData.set(this.outputBuffer.subarray(0, samplesWritten));
32126
- const timestampSeconds = this.bufferStartFrame / this.targetSampleRate;
32127
32220
  const audioSample = new AudioSample({
32128
32221
  format: "f32",
32129
32222
  sampleRate: this.targetSampleRate,
32130
32223
  numberOfChannels: this.targetNumberOfChannels,
32131
- timestamp: timestampSeconds + this.timestampOffset,
32224
+ timestamp: this.startTime + this.bufferStartFrame / this.targetSampleRate,
32132
32225
  data: outputData
32133
32226
  });
32134
32227
  await this.onSample(audioSample);
@@ -32292,6 +32385,7 @@ ${cue.notes ?? ""}`;
32292
32385
  * So, we keep track of the encoder error and throw it as soon as we get the chance.
32293
32386
  */
32294
32387
  this.error = null;
32388
+ this.closed = false;
32295
32389
  this.lastMuxerPromise = Promise.resolve();
32296
32390
  }
32297
32391
  async add(videoSample, shouldClose, encodeOptions) {
@@ -32428,13 +32522,18 @@ ${cue.notes ?? ""}`;
32428
32522
  }
32429
32523
  }
32430
32524
  assert(this.encoderInitialized);
32525
+ if (this.closed) {
32526
+ break;
32527
+ }
32431
32528
  const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
32432
32529
  const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
32530
+ const mergedEncodeOptions = { ...sampleToEncode.encodeOptions, ...encodeOptions };
32433
32531
  const finalEncodeOptions = {
32434
- ...encodeOptions,
32435
- keyFrame: encodeOptions?.keyFrame || keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval
32532
+ ...mergedEncodeOptions,
32533
+ keyFrame: mergedEncodeOptions.keyFrame !== void 0 ? mergedEncodeOptions.keyFrame : keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval
32436
32534
  };
32437
32535
  this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval;
32536
+ this.encodingConfig.onEncodedSample?.(sampleToEncode);
32438
32537
  if (this.customEncoder) {
32439
32538
  this.customEncoderQueueSize++;
32440
32539
  const clonedSample = sampleToEncode.clone();
@@ -32689,6 +32788,7 @@ ${cue.notes ?? ""}`;
32689
32788
  const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp, frameRate);
32690
32789
  await this.padFrameRate(alignedEnd);
32691
32790
  }
32791
+ this.closed = true;
32692
32792
  this.frameRateLastSample?.close();
32693
32793
  this.frameRateLastSample = null;
32694
32794
  if (this.customEncoder) {
@@ -33562,6 +33662,7 @@ ${cue.notes ?? ""}`;
33562
33662
  */
33563
33663
  this.error = null;
33564
33664
  this.lastMuxerPromise = Promise.resolve();
33665
+ this.closed = false;
33565
33666
  }
33566
33667
  async add(audioSample, shouldClose) {
33567
33668
  try {
@@ -33584,8 +33685,6 @@ ${cue.notes ?? ""}`;
33584
33685
  this.resampler = new AudioResampler({
33585
33686
  targetNumberOfChannels: config.transform.numberOfChannels ?? audioSample.numberOfChannels,
33586
33687
  targetSampleRate: config.transform.sampleRate ?? audioSample.sampleRate,
33587
- startTime: audioSample.timestamp,
33588
- endTime: Infinity,
33589
33688
  onSample: async (sample) => {
33590
33689
  await this.processAndEncode(sample, true);
33591
33690
  }
@@ -33654,6 +33753,9 @@ ${cue.notes ?? ""}`;
33654
33753
  }
33655
33754
  }
33656
33755
  assert(this.encoderInitialized);
33756
+ if (this.closed) {
33757
+ return;
33758
+ }
33657
33759
  {
33658
33760
  const startSampleIndex = Math.round(
33659
33761
  audioSample.timestamp * audioSample.sampleRate
@@ -33679,6 +33781,7 @@ ${cue.notes ?? ""}`;
33679
33781
  this.lastEndSampleIndex += audioSample.numberOfFrames;
33680
33782
  }
33681
33783
  }
33784
+ this.encodingConfig.onEncodedSample?.(audioSample);
33682
33785
  if (this.customEncoder) {
33683
33786
  this.customEncoderQueueSize++;
33684
33787
  const clonedSample = audioSample.clone();
@@ -33951,6 +34054,7 @@ ${cue.notes ?? ""}`;
33951
34054
  await this.resampler.finalize();
33952
34055
  }
33953
34056
  this.resampler = null;
34057
+ this.closed = true;
33954
34058
  if (this.customEncoder) {
33955
34059
  if (!forceClose) {
33956
34060
  void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
@@ -37313,6 +37417,9 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37313
37417
  if (trackOptions.frameRate) {
37314
37418
  encodingConfig.transform.frameRate = trackOptions.frameRate;
37315
37419
  }
37420
+ if (trackOptions.process) {
37421
+ encodingConfig.transform.process = trackOptions.process;
37422
+ }
37316
37423
  if (needsRerender) {
37317
37424
  outputTrackRotation = 0;
37318
37425
  encodingConfig.transform.width = width;
@@ -37322,6 +37429,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37322
37429
  encodingConfig.transform.crop = crop;
37323
37430
  encodingConfig.transform.alpha = alpha;
37324
37431
  }
37432
+ let lastSampleTimestamp = null;
37433
+ encodingConfig.onEncodedSample = (sample) => {
37434
+ lastSampleTimestamp = sample.timestamp;
37435
+ };
37325
37436
  const source = new VideoSampleSource(encodingConfig);
37326
37437
  videoSource = source;
37327
37438
  this._trackPromises.push((async () => {
@@ -37334,7 +37445,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37334
37445
  }
37335
37446
  const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37336
37447
  sample.setTimestamp(adjustedSampleTimestamp);
37337
- await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
37448
+ this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37449
+ await source.add(sample);
37450
+ if (lastSampleTimestamp !== null) {
37451
+ if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37452
+ await this._synchronizer.wait(lastSampleTimestamp);
37453
+ }
37454
+ }
37338
37455
  sample.close();
37339
37456
  }
37340
37457
  source.close();
@@ -37362,52 +37479,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37362
37479
  this._outputOwnTrackGroups.push(ownGroup);
37363
37480
  }
37364
37481
  /** @internal */
37365
- async _registerVideoSample(trackOptions, outputTrackId, source, sample) {
37366
- if (this._canceled) {
37367
- return;
37368
- }
37369
- this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37370
- let finalSamples;
37371
- if (!trackOptions.process) {
37372
- finalSamples = [sample];
37373
- } else {
37374
- let processed = trackOptions.process(sample);
37375
- if (processed instanceof Promise) processed = await processed;
37376
- if (!Array.isArray(processed)) {
37377
- processed = processed === null ? [] : [processed];
37378
- }
37379
- finalSamples = processed.map((x) => {
37380
- if (x instanceof VideoSample) {
37381
- return x;
37382
- }
37383
- if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
37384
- return new VideoSample(x);
37385
- }
37386
- return new VideoSample(x, {
37387
- timestamp: sample.timestamp,
37388
- duration: sample.duration
37389
- });
37390
- });
37391
- }
37392
- try {
37393
- for (const finalSample of finalSamples) {
37394
- if (this._canceled) {
37395
- break;
37396
- }
37397
- await source.add(finalSample);
37398
- if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
37399
- await this._synchronizer.wait(finalSample.timestamp);
37400
- }
37401
- }
37402
- } finally {
37403
- for (const finalSample of finalSamples) {
37404
- if (finalSample !== sample) {
37405
- finalSample.close();
37406
- }
37407
- }
37408
- }
37409
- }
37410
- /** @internal */
37411
37482
  async _processAudioTrack(track, trackOptions, outputTrackId) {
37412
37483
  const sourceCodec = await track.getCodec();
37413
37484
  if (!sourceCodec) {
@@ -37424,9 +37495,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37424
37495
  const firstTimestamp = await track.getFirstTimestamp();
37425
37496
  let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
37426
37497
  let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
37427
- let needsResample = numberOfChannels !== originalNumberOfChannels || sampleRate !== originalSampleRate || firstTimestamp < this._startTimestamp || firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
37498
+ const needsTrimming = firstTimestamp < this._startTimestamp;
37499
+ const needsPadding = firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
37428
37500
  let audioCodecs = this.output.format.getSupportedAudioCodecs();
37429
- if (!trackOptions.forceTranscode && !trackOptions.bitrate && !needsResample && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
37501
+ 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) {
37430
37502
  const source = new EncodedAudioPacketSource(sourceCodec);
37431
37503
  audioSource = source;
37432
37504
  this._trackPromises.push((async () => {
@@ -37482,7 +37554,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37482
37554
  });
37483
37555
  const nonPcmCodec = encodableCodecsWithDefaultParams.find((codec) => NON_PCM_AUDIO_CODECS.includes(codec));
37484
37556
  if (nonPcmCodec) {
37485
- needsResample = true;
37486
37557
  codecOfChoice = nonPcmCodec;
37487
37558
  numberOfChannels = FALLBACK_NUMBER_OF_CHANNELS;
37488
37559
  sampleRate = FALLBACK_SAMPLE_RATE;
@@ -37498,38 +37569,70 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37498
37569
  });
37499
37570
  return;
37500
37571
  }
37501
- if (needsResample) {
37502
- audioSource = this._resampleAudio(
37503
- track,
37504
- trackOptions,
37505
- outputTrackId,
37506
- codecOfChoice,
37507
- numberOfChannels,
37508
- sampleRate,
37509
- bitrate
37510
- );
37511
- } else {
37512
- const source = new AudioSampleSource({
37513
- codec: codecOfChoice,
37514
- bitrate
37515
- });
37516
- audioSource = source;
37517
- this._trackPromises.push((async () => {
37518
- await this._started;
37519
- const sink = new AudioSampleSink(track);
37520
- for await (const sample of sink.samples(void 0, this._endTimestamp)) {
37521
- if (this._canceled) {
37572
+ const encodingConfig = {
37573
+ codec: codecOfChoice,
37574
+ bitrate,
37575
+ transform: {
37576
+ sampleFormat: trackOptions.sampleFormat,
37577
+ process: trackOptions.process
37578
+ }
37579
+ };
37580
+ assert(encodingConfig.transform);
37581
+ if (numberOfChannels !== originalNumberOfChannels) {
37582
+ encodingConfig.transform.numberOfChannels = numberOfChannels;
37583
+ }
37584
+ if (sampleRate !== originalSampleRate) {
37585
+ encodingConfig.transform.sampleRate = sampleRate;
37586
+ }
37587
+ let lastSampleTimestamp = null;
37588
+ encodingConfig.onEncodedSample = (sample) => {
37589
+ lastSampleTimestamp = sample.timestamp;
37590
+ };
37591
+ const source = new AudioSampleSource(encodingConfig);
37592
+ audioSource = source;
37593
+ this._trackPromises.push((async () => {
37594
+ await this._started;
37595
+ if (needsPadding) {
37596
+ const paddingLength = firstTimestamp - this._startTimestamp;
37597
+ const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
37598
+ const silentSample = new AudioSample({
37599
+ data: new Float32Array(paddingLengthSamples * originalNumberOfChannels),
37600
+ format: "f32-planar",
37601
+ numberOfChannels: originalNumberOfChannels,
37602
+ sampleRate: originalSampleRate,
37603
+ timestamp: 0
37604
+ });
37605
+ await this._registerAudioSample(silentSample, source, outputTrackId, () => lastSampleTimestamp);
37606
+ }
37607
+ const sink = new AudioSampleSink(track);
37608
+ for await (let sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37609
+ if (this._canceled) {
37610
+ sample.close();
37611
+ return;
37612
+ }
37613
+ let startFrame = 0;
37614
+ let endFrame = sample.numberOfFrames;
37615
+ if (sample.timestamp < this._startTimestamp) {
37616
+ startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
37617
+ }
37618
+ if (sample.timestamp + sample.duration > this._endTimestamp) {
37619
+ endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
37620
+ }
37621
+ if (startFrame > 0 || endFrame < sample.numberOfFrames) {
37622
+ const trimmedSample = sample.trim(startFrame, endFrame);
37623
+ sample.close();
37624
+ sample = trimmedSample;
37625
+ if (sample.numberOfFrames === 0) {
37522
37626
  sample.close();
37523
- return;
37627
+ continue;
37524
37628
  }
37525
- sample.setTimestamp(sample.timestamp - this._startTimestamp);
37526
- await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
37527
- sample.close();
37528
37629
  }
37529
- source.close();
37530
- this._synchronizer.closeTrack(outputTrackId);
37531
- })());
37532
- }
37630
+ sample.setTimestamp(sample.timestamp - this._startTimestamp);
37631
+ await this._registerAudioSample(sample, source, outputTrackId, () => lastSampleTimestamp);
37632
+ }
37633
+ source.close();
37634
+ this._synchronizer.closeTrack(outputTrackId);
37635
+ })());
37533
37636
  }
37534
37637
  let ownGroup = null;
37535
37638
  if (!trackOptions.group) {
@@ -37550,89 +37653,18 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37550
37653
  this._outputOwnTrackGroups.push(ownGroup);
37551
37654
  }
37552
37655
  /** @internal */
37553
- async _registerAudioSample(trackOptions, outputTrackId, source, inputSample) {
37554
- if (this._canceled) {
37555
- return;
37556
- }
37557
- let sample = inputSample;
37558
- if (trackOptions.sampleFormat !== void 0 && toInterleavedAudioFormat(sample.format) !== trackOptions.sampleFormat) {
37559
- sample = audioSampleToInterleavedFormat(sample, trackOptions.sampleFormat);
37560
- }
37656
+ async _registerAudioSample(sample, source, outputTrackId, getLastSampleTimestamp) {
37561
37657
  this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37562
- let finalSamples;
37563
- if (!trackOptions.process) {
37564
- finalSamples = [sample];
37565
- } else {
37566
- let processed = trackOptions.process(sample);
37567
- if (processed instanceof Promise) processed = await processed;
37568
- if (!Array.isArray(processed)) {
37569
- processed = processed === null ? [] : [processed];
37570
- }
37571
- if (!processed.every((x) => x instanceof AudioSample)) {
37572
- throw new TypeError(
37573
- "The audio process function must return an AudioSample, null, or an array of AudioSamples."
37574
- );
37575
- }
37576
- finalSamples = processed;
37577
- }
37578
- try {
37579
- for (const finalSample of finalSamples) {
37580
- if (this._canceled) {
37581
- break;
37582
- }
37583
- await source.add(finalSample);
37584
- if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
37585
- await this._synchronizer.wait(finalSample.timestamp);
37586
- }
37587
- }
37588
- } finally {
37589
- if (sample !== inputSample) {
37590
- sample.close();
37591
- }
37592
- for (const finalSample of finalSamples) {
37593
- if (finalSample !== inputSample) {
37594
- finalSample.close();
37595
- }
37658
+ await source.add(sample);
37659
+ sample.close();
37660
+ const lastSampleTimestamp = getLastSampleTimestamp();
37661
+ if (lastSampleTimestamp !== null) {
37662
+ if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37663
+ await this._synchronizer.wait(lastSampleTimestamp);
37596
37664
  }
37597
37665
  }
37598
37666
  }
37599
37667
  /** @internal */
37600
- _resampleAudio(track, trackOptions, outputTrackId, codec, targetNumberOfChannels, targetSampleRate, bitrate) {
37601
- const source = new AudioSampleSource({
37602
- codec,
37603
- bitrate
37604
- });
37605
- this._trackPromises.push((async () => {
37606
- await this._started;
37607
- const resampler = new AudioResampler({
37608
- targetNumberOfChannels,
37609
- targetSampleRate,
37610
- startTime: this._startTimestamp,
37611
- endTime: this._endTimestamp,
37612
- onSample: async (sample) => {
37613
- assert(sample.timestamp >= this._startTimestamp);
37614
- sample.setTimestamp(sample.timestamp - this._startTimestamp);
37615
- await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
37616
- sample.close();
37617
- }
37618
- });
37619
- const sink = new AudioSampleSink(track);
37620
- const iterator = sink.samples(this._startTimestamp, this._endTimestamp);
37621
- for await (const sample of iterator) {
37622
- if (this._canceled) {
37623
- sample.close();
37624
- return;
37625
- }
37626
- await resampler.add(sample);
37627
- sample.close();
37628
- }
37629
- await resampler.finalize();
37630
- source.close();
37631
- this._synchronizer.closeTrack(outputTrackId);
37632
- })());
37633
- return source;
37634
- }
37635
- /** @internal */
37636
37668
  _reportProgress(trackId, endTimestamp) {
37637
37669
  if (!this._computeProgress) {
37638
37670
  return;