mediabunny 1.44.2 → 1.45.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 (49) hide show
  1. package/README.md +8 -8
  2. package/dist/bundles/mediabunny.cjs +1297 -447
  3. package/dist/bundles/mediabunny.min.cjs +17 -17
  4. package/dist/bundles/mediabunny.min.mjs +17 -17
  5. package/dist/bundles/mediabunny.mjs +1297 -447
  6. package/dist/bundles/mediabunny.node.cjs +1297 -447
  7. package/dist/mediabunny.d.ts +219 -15
  8. package/dist/modules/src/conversion.d.ts +5 -5
  9. package/dist/modules/src/conversion.d.ts.map +1 -1
  10. package/dist/modules/src/conversion.js +58 -162
  11. package/dist/modules/src/encode.d.ts +9 -4
  12. package/dist/modules/src/encode.d.ts.map +1 -1
  13. package/dist/modules/src/encode.js +5 -3
  14. package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
  15. package/dist/modules/src/flac/flac-demuxer.js +1 -1
  16. package/dist/modules/src/index.d.ts +1 -1
  17. package/dist/modules/src/index.d.ts.map +1 -1
  18. package/dist/modules/src/index.js +1 -1
  19. package/dist/modules/src/input-format.d.ts.map +1 -1
  20. package/dist/modules/src/input-format.js +2 -1
  21. package/dist/modules/src/isobmff/isobmff-muxer.js +1 -1
  22. package/dist/modules/src/media-sink.d.ts +31 -0
  23. package/dist/modules/src/media-sink.d.ts.map +1 -1
  24. package/dist/modules/src/media-sink.js +353 -79
  25. package/dist/modules/src/media-source.d.ts +30 -0
  26. package/dist/modules/src/media-source.d.ts.map +1 -1
  27. package/dist/modules/src/media-source.js +287 -148
  28. package/dist/modules/src/misc.d.ts +1 -4
  29. package/dist/modules/src/misc.d.ts.map +1 -1
  30. package/dist/modules/src/misc.js +4 -4
  31. package/dist/modules/src/sample.d.ts +200 -8
  32. package/dist/modules/src/sample.d.ts.map +1 -1
  33. package/dist/modules/src/sample.js +820 -99
  34. package/dist/modules/src/source.d.ts +1 -1
  35. package/dist/modules/src/source.d.ts.map +1 -1
  36. package/dist/modules/src/source.js +8 -5
  37. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +7 -6
  39. package/src/conversion.ts +70 -192
  40. package/src/encode.ts +14 -6
  41. package/src/flac/flac-demuxer.ts +2 -1
  42. package/src/index.ts +6 -0
  43. package/src/input-format.ts +2 -1
  44. package/src/isobmff/isobmff-muxer.ts +1 -1
  45. package/src/media-sink.ts +450 -93
  46. package/src/media-source.ts +356 -168
  47. package/src/misc.ts +5 -5
  48. package/src/sample.ts +1129 -112
  49. package/src/source.ts +11 -7
@@ -103,6 +103,7 @@ var Mediabunny = (() => {
103
103
  AudioBufferSink: () => AudioBufferSink,
104
104
  AudioBufferSource: () => AudioBufferSource,
105
105
  AudioSample: () => AudioSample,
106
+ AudioSampleResource: () => AudioSampleResource,
106
107
  AudioSampleSink: () => AudioSampleSink,
107
108
  AudioSampleSource: () => AudioSampleSource,
108
109
  AudioSource: () => AudioSource,
@@ -200,6 +201,7 @@ var Mediabunny = (() => {
200
201
  VIDEO_SAMPLE_PIXEL_FORMATS: () => VIDEO_SAMPLE_PIXEL_FORMATS,
201
202
  VideoSample: () => VideoSample,
202
203
  VideoSampleColorSpace: () => VideoSampleColorSpace,
204
+ VideoSampleResource: () => VideoSampleResource,
203
205
  VideoSampleSink: () => VideoSampleSink,
204
206
  VideoSampleSource: () => VideoSampleSource,
205
207
  VideoSource: () => VideoSource,
@@ -230,7 +232,8 @@ var Mediabunny = (() => {
230
232
  getFirstEncodableVideoCodec: () => getFirstEncodableVideoCodec,
231
233
  prefer: () => prefer,
232
234
  registerDecoder: () => registerDecoder,
233
- registerEncoder: () => registerEncoder
235
+ registerEncoder: () => registerEncoder,
236
+ registerVideoSampleTransformer: () => registerVideoSampleTransformer
234
237
  });
235
238
 
236
239
  // src/misc.ts
@@ -661,8 +664,8 @@ var Mediabunny = (() => {
661
664
  const nextDenominator = integer * currDenominator + prevDenominator;
662
665
  if (nextDenominator > maxDenominator) {
663
666
  return {
664
- numerator: sign * currNumerator,
665
- denominator: currDenominator
667
+ num: sign * currNumerator,
668
+ den: currDenominator
666
669
  };
667
670
  }
668
671
  prevNumerator = currNumerator;
@@ -675,8 +678,8 @@ var Mediabunny = (() => {
675
678
  }
676
679
  }
677
680
  return {
678
- numerator: sign * currNumerator,
679
- denominator: currDenominator
681
+ num: sign * currNumerator,
682
+ den: currDenominator
680
683
  };
681
684
  };
682
685
  var CallSerializer = class {
@@ -13356,7 +13359,7 @@ var Mediabunny = (() => {
13356
13359
  async getPacket(timestamp, options) {
13357
13360
  assert(this.demuxer.audioInfo);
13358
13361
  if (timestamp < 0) {
13359
- throw new Error("Timestamp cannot be negative");
13362
+ return null;
13360
13363
  }
13361
13364
  const release = await this.demuxer.readingMutex.acquire();
13362
13365
  try {
@@ -15895,7 +15898,7 @@ var Mediabunny = (() => {
15895
15898
  };
15896
15899
  var URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20;
15897
15900
  var DEFAULT_RETRY_DELAY = (previousAttempts, error, src) => {
15898
- const couldBeCorsError = error instanceof Error && (error.message.includes("Failed to fetch") || error.message.includes("Load failed") || error.message.includes("NetworkError when attempting to fetch resource"));
15901
+ const couldBeCorsError = error instanceof Error && (error.message.includes("Failed to fetch") || error.message.includes("Load failed") || error.message.includes("NetworkError when attempting to fetch resource")) && typeof window !== "undefined";
15899
15902
  if (couldBeCorsError) {
15900
15903
  let originOfSrc = null;
15901
15904
  try {
@@ -16280,7 +16283,7 @@ var Mediabunny = (() => {
16280
16283
  /** @internal */
16281
16284
  this._pulling = false;
16282
16285
  this._stream = stream;
16283
- this._maxCacheSize = options.maxCacheSize ?? 16 * 2 ** 20;
16286
+ this._maxCacheSize = options.maxCacheSize ?? 32 * 2 ** 20;
16284
16287
  }
16285
16288
  /** @internal */
16286
16289
  _getFileSize() {
@@ -16377,6 +16380,7 @@ var Mediabunny = (() => {
16377
16380
  }
16378
16381
  const startIndex = this._currentIndex;
16379
16382
  const endIndex = this._currentIndex + value.byteLength;
16383
+ this._dispatchRead(startIndex, endIndex);
16380
16384
  for (let i = 0; i < this._pendingSlices.length; i++) {
16381
16385
  const pendingSlice = this._pendingSlices[i];
16382
16386
  const cappedStart = Math.max(startIndex, pendingSlice.start);
@@ -18293,7 +18297,8 @@ var Mediabunny = (() => {
18293
18297
  let slice = input._reader.requestSlice(4, 4);
18294
18298
  if (slice instanceof Promise) slice = await slice;
18295
18299
  if (!slice) return false;
18296
- return readAscii(slice, 4) === "moof";
18300
+ const fourCc = readAscii(slice, 4);
18301
+ return fourCc === "moof" || fourCc === "sidx";
18297
18302
  }
18298
18303
  get name() {
18299
18304
  return "MP4";
@@ -18841,6 +18846,14 @@ var Mediabunny = (() => {
18841
18846
  }
18842
18847
  });
18843
18848
  }
18849
+ var VideoSampleResource = class {
18850
+ constructor() {
18851
+ /** @internal */
18852
+ this._referenceCount = 0;
18853
+ /** @internal */
18854
+ this._lastAllocationBuffer = null;
18855
+ }
18856
+ };
18844
18857
  var VIDEO_SAMPLE_PIXEL_FORMATS = [
18845
18858
  // 4:2:0 Y, U, V
18846
18859
  "I420",
@@ -18940,7 +18953,25 @@ var Mediabunny = (() => {
18940
18953
  this.rotation = init.rotation ?? 0;
18941
18954
  this.timestamp = init.timestamp;
18942
18955
  this.duration = init.duration ?? 0;
18943
- this.colorSpace = new VideoSampleColorSpace(init.colorSpace);
18956
+ let colorSpaceInit = init.colorSpace ?? null;
18957
+ if (colorSpaceInit === null) {
18958
+ if (this.format === "RGBA" || this.format === "RGBX" || this.format === "BGRA" || this.format === "BGRX") {
18959
+ colorSpaceInit = {
18960
+ primaries: "bt709",
18961
+ transfer: "iec61966-2-1",
18962
+ matrix: "rgb",
18963
+ fullRange: true
18964
+ };
18965
+ } else {
18966
+ colorSpaceInit = {
18967
+ primaries: "bt709",
18968
+ transfer: "bt709",
18969
+ matrix: "bt709",
18970
+ fullRange: false
18971
+ };
18972
+ }
18973
+ }
18974
+ this.colorSpace = new VideoSampleColorSpace(colorSpaceInit);
18944
18975
  this.visibleRect = {
18945
18976
  left: init.visibleRect?.left ?? 0,
18946
18977
  top: init.visibleRect?.top ?? 0,
@@ -18951,8 +18982,8 @@ var Mediabunny = (() => {
18951
18982
  this.squarePixelWidth = this.rotation % 180 === 0 ? init.displayWidth : init.displayHeight;
18952
18983
  this.squarePixelHeight = this.rotation % 180 === 0 ? init.displayHeight : init.displayWidth;
18953
18984
  } else {
18954
- this.squarePixelWidth = this.codedWidth;
18955
- this.squarePixelHeight = this.codedHeight;
18985
+ this.squarePixelWidth = this.visibleRect.width;
18986
+ this.squarePixelHeight = this.visibleRect.height;
18956
18987
  }
18957
18988
  } else if (typeof VideoFrame !== "undefined" && data instanceof VideoFrame) {
18958
18989
  if (init?.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
@@ -19043,8 +19074,53 @@ var Mediabunny = (() => {
19043
19074
  transfer: "iec61966-2-1",
19044
19075
  fullRange: true
19045
19076
  });
19077
+ } else if (data instanceof VideoSampleResource) {
19078
+ if (!init || typeof init !== "object") {
19079
+ throw new TypeError("init must be an object.");
19080
+ }
19081
+ if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
19082
+ throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
19083
+ }
19084
+ if (!Number.isFinite(init.timestamp)) {
19085
+ throw new TypeError("init.timestamp must be a number.");
19086
+ }
19087
+ if (init.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
19088
+ throw new TypeError("init.duration, when provided, must be a non-negative number.");
19089
+ }
19090
+ this._data = data;
19091
+ data._referenceCount++;
19092
+ this.format = data.getFormat();
19093
+ if (this.format !== null && !VIDEO_SAMPLE_PIXEL_FORMATS.includes(this.format)) {
19094
+ throw new TypeError("getFormat() must return a VideoSamplePixelFormat or null.");
19095
+ }
19096
+ this.visibleRect = {
19097
+ left: 0,
19098
+ top: 0,
19099
+ width: data.getCodedWidth(),
19100
+ height: data.getCodedHeight()
19101
+ };
19102
+ if (!Number.isInteger(this.visibleRect.width) || this.visibleRect.width <= 0) {
19103
+ throw new TypeError("getCodedWidth() must return a positive integer.");
19104
+ }
19105
+ if (!Number.isInteger(this.visibleRect.height) || this.visibleRect.height <= 0) {
19106
+ throw new TypeError("getCodedHeight() must return a positive integer.");
19107
+ }
19108
+ this.squarePixelWidth = data.getSquarePixelWidth();
19109
+ if (!Number.isInteger(this.squarePixelWidth) || this.squarePixelWidth <= 0) {
19110
+ throw new TypeError("getSquarePixelWidth() must return a positive integer.");
19111
+ }
19112
+ this.squarePixelHeight = data.getSquarePixelHeight();
19113
+ if (!Number.isInteger(this.squarePixelHeight) || this.squarePixelHeight <= 0) {
19114
+ throw new TypeError("getSquarePixelHeight() must return a positive integer.");
19115
+ }
19116
+ this.rotation = init.rotation ?? 0;
19117
+ this.timestamp = init.timestamp;
19118
+ this.duration = init.duration ?? 0;
19119
+ this.colorSpace = data.getColorSpace();
19046
19120
  } else {
19047
- throw new TypeError("Invalid data type: Must be a BufferSource or CanvasImageSource.");
19121
+ throw new TypeError(
19122
+ "Invalid data type: Must be a BufferSource, CanvasImageSource, or VideoSampleResource."
19123
+ );
19048
19124
  }
19049
19125
  this.pixelAspectRatio = simplifyRational({
19050
19126
  num: this.squarePixelWidth * this.codedHeight,
@@ -19089,7 +19165,13 @@ var Mediabunny = (() => {
19089
19165
  throw new Error("VideoSample is closed.");
19090
19166
  }
19091
19167
  assert(this._data !== null);
19092
- if (isVideoFrame(this._data)) {
19168
+ if (this._data instanceof VideoSampleResource) {
19169
+ return new _VideoSample(this._data, {
19170
+ timestamp: this.timestamp,
19171
+ duration: this.duration,
19172
+ rotation: this.rotation
19173
+ });
19174
+ } else if (isVideoFrame(this._data)) {
19093
19175
  return new _VideoSample(this._data.clone(), {
19094
19176
  timestamp: this.timestamp,
19095
19177
  duration: this.duration,
@@ -19134,7 +19216,12 @@ var Mediabunny = (() => {
19134
19216
  return;
19135
19217
  }
19136
19218
  finalizationRegistry?.unregister(this);
19137
- if (isVideoFrame(this._data)) {
19219
+ if (this._data instanceof VideoSampleResource) {
19220
+ this._data._referenceCount--;
19221
+ if (this._data._referenceCount === 0) {
19222
+ this._data.close();
19223
+ }
19224
+ } else if (isVideoFrame(this._data)) {
19138
19225
  this._data.close();
19139
19226
  } else {
19140
19227
  this._data = null;
@@ -19142,35 +19229,24 @@ var Mediabunny = (() => {
19142
19229
  this._closed = true;
19143
19230
  }
19144
19231
  /**
19145
- * Returns the number of bytes required to hold this video sample's pixel data. Throws if `format` is `null`.
19232
+ * Returns the number of bytes required to hold this video sample's pixel data.
19146
19233
  */
19147
19234
  allocationSize(options = {}) {
19148
19235
  validateVideoFrameCopyToOptions(options);
19149
19236
  if (this._closed) {
19150
19237
  throw new Error("VideoSample is closed.");
19151
19238
  }
19152
- if (this.format === null) {
19153
- throw new Error("Cannot get allocation size when format is null. Sorry!");
19154
- }
19155
- assert(this._data !== null);
19156
- if (!isVideoFrame(this._data)) {
19157
- if (options.colorSpace || options.format && options.format !== this.format || options.layout || options.rect) {
19158
- const videoFrame = this.toVideoFrame();
19159
- const size = videoFrame.allocationSize(options);
19160
- videoFrame.close();
19161
- return size;
19162
- }
19239
+ if ((options.format ?? this.format) == null) {
19240
+ throw new Error("Cannot get allocation size when format is null.");
19163
19241
  }
19164
19242
  if (isVideoFrame(this._data)) {
19165
19243
  return this._data.allocationSize(options);
19166
- } else if (this._data instanceof Uint8Array) {
19167
- return this._data.byteLength;
19168
- } else {
19169
- return this.codedWidth * this.codedHeight * 4;
19170
19244
  }
19245
+ const combinedLayout = ParseVideoFrameCopyToOptions(this, options);
19246
+ return combinedLayout.allocationSize;
19171
19247
  }
19172
19248
  /**
19173
- * Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView. Throws if `format` is `null`.
19249
+ * Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView.
19174
19250
  * @returns The byte layout of the planes of the copied data.
19175
19251
  */
19176
19252
  async copyTo(destination, options = {}) {
@@ -19181,37 +19257,139 @@ var Mediabunny = (() => {
19181
19257
  if (this._closed) {
19182
19258
  throw new Error("VideoSample is closed.");
19183
19259
  }
19184
- if (this.format === null) {
19185
- throw new Error("Cannot copy video sample data when format is null. Sorry!");
19260
+ if ((options.format ?? this.format) == null) {
19261
+ throw new Error("Cannot copy video sample data when format is null.");
19186
19262
  }
19187
19263
  assert(this._data !== null);
19188
- if (!isVideoFrame(this._data)) {
19189
- if (options.colorSpace || options.format && options.format !== this.format || options.layout || options.rect) {
19190
- const videoFrame = this.toVideoFrame();
19191
- const layout = await videoFrame.copyTo(destination, options);
19192
- videoFrame.close();
19193
- return layout;
19194
- }
19195
- }
19196
19264
  if (isVideoFrame(this._data)) {
19197
19265
  return this._data.copyTo(destination, options);
19266
+ }
19267
+ if (options.format && !["RGBA", "RGBX", "BGRA", "BGRX"].includes(this.format) && ["RGBA", "RGBX", "BGRA", "BGRX"].includes(options.format)) {
19268
+ if (this._data instanceof VideoSampleResource) {
19269
+ var _stack = [];
19270
+ try {
19271
+ const rgbSample = __using(_stack, await this._data.toRgbSample(
19272
+ {
19273
+ timestamp: this.timestamp,
19274
+ duration: this.duration,
19275
+ rotation: this.rotation
19276
+ },
19277
+ options.colorSpace ?? "srgb"
19278
+ ));
19279
+ if (!(rgbSample instanceof _VideoSample)) {
19280
+ throw new TypeError("toRgbSample() must return a VideoSample.");
19281
+ }
19282
+ if (!["RGBA", "RGBX", "BGRA", "BGRX"].includes(rgbSample.format)) {
19283
+ throw new Error(
19284
+ `Sample returned by toRgbSample was expected to have an RGB format, got '${rgbSample.format}' instead.`
19285
+ );
19286
+ }
19287
+ return await rgbSample.copyTo(destination, options);
19288
+ } catch (_) {
19289
+ var _error = _, _hasError = true;
19290
+ } finally {
19291
+ __callDispose(_stack, _error, _hasError);
19292
+ }
19293
+ } else {
19294
+ if (typeof VideoFrame === "undefined") {
19295
+ throw new Error(
19296
+ "For this sample, converting from a non-RGB to an RGB format requires VideoFrame to be defined."
19297
+ );
19298
+ }
19299
+ const tempFrame = this.toVideoFrame();
19300
+ const result = await tempFrame.copyTo(destination, options);
19301
+ tempFrame.close();
19302
+ return result;
19303
+ }
19304
+ }
19305
+ const combinedLayout = ParseVideoFrameCopyToOptions(this, options);
19306
+ assert(this.format);
19307
+ const destBytes = toUint8Array(destination);
19308
+ if (destBytes.byteLength < combinedLayout.allocationSize) {
19309
+ throw new TypeError(
19310
+ `Destination buffer too small. Required: ${combinedLayout.allocationSize}, Available: ${destBytes.byteLength}`
19311
+ );
19312
+ }
19313
+ const planeConfigs = getPlaneConfigs(this.format);
19314
+ let dataPlanes;
19315
+ if (this._data instanceof VideoSampleResource) {
19316
+ let result = this._data.getDataPlanes();
19317
+ if (result instanceof Promise) result = await result;
19318
+ if (!Array.isArray(result) || result.some((x) => !(x.data instanceof Uint8Array) || !Number.isInteger(x.stride) || x.stride < 0)) {
19319
+ throw new TypeError(
19320
+ 'getDataPlanes() must return an array of objects with a Uint8Array "data" property and a non-negative integer "stride" property.'
19321
+ );
19322
+ }
19323
+ dataPlanes = result;
19198
19324
  } else if (this._data instanceof Uint8Array) {
19199
19325
  assert(this._layout);
19200
- const dest = toUint8Array(destination);
19201
- dest.set(this._data);
19202
- return this._layout;
19326
+ assert(this._layout.length === planeConfigs.length);
19327
+ dataPlanes = this._layout.map((planeLayout, i) => {
19328
+ const height = Math.ceil(this.codedHeight / planeConfigs[i].heightDivisor);
19329
+ return {
19330
+ data: this._data.subarray(
19331
+ planeLayout.offset,
19332
+ planeLayout.offset + planeLayout.stride * height
19333
+ ),
19334
+ stride: planeLayout.stride
19335
+ };
19336
+ });
19203
19337
  } else {
19204
19338
  const canvas = this._data;
19205
19339
  const context = canvas.getContext("2d");
19206
19340
  assert(context);
19207
19341
  const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
19208
- const dest = toUint8Array(destination);
19209
- dest.set(imageData.data);
19210
- return [{
19211
- offset: 0,
19342
+ dataPlanes = [{
19343
+ data: toUint8Array(imageData.data),
19212
19344
  stride: 4 * this.codedWidth
19213
19345
  }];
19214
19346
  }
19347
+ const planeLayouts = [];
19348
+ const numPlanes = planeConfigs.length;
19349
+ for (let planeIndex = 0; planeIndex < numPlanes; planeIndex++) {
19350
+ const computedLayout = combinedLayout.computedLayouts[planeIndex];
19351
+ const sourceStride = dataPlanes[planeIndex].stride;
19352
+ const sourceData = dataPlanes[planeIndex].data;
19353
+ let sourceOffset = computedLayout.sourceTop * sourceStride;
19354
+ sourceOffset += computedLayout.sourceLeftBytes;
19355
+ let destinationOffset = computedLayout.destinationOffset;
19356
+ const rowBytes = computedLayout.sourceWidthBytes;
19357
+ const layout = {
19358
+ offset: destinationOffset,
19359
+ stride: computedLayout.destinationStride
19360
+ };
19361
+ for (let row = 0; row < computedLayout.sourceHeight; row++) {
19362
+ if (sourceOffset + rowBytes > sourceData.byteLength) {
19363
+ throw new Error(`Source buffer OOB read.`);
19364
+ }
19365
+ if (destinationOffset + rowBytes > destBytes.byteLength) {
19366
+ throw new Error(`Destination buffer OOB write.`);
19367
+ }
19368
+ const srcSub = sourceData.subarray(sourceOffset, sourceOffset + rowBytes);
19369
+ destBytes.set(srcSub, destinationOffset);
19370
+ sourceOffset += sourceStride;
19371
+ destinationOffset += computedLayout.destinationStride;
19372
+ }
19373
+ planeLayouts.push(layout);
19374
+ }
19375
+ if (options.format !== void 0) {
19376
+ const needsRgbConversion = this.format.startsWith("RGB") !== options.format.startsWith("RGB");
19377
+ const needsAlphaConversion = this.format.includes("X") && options.format.includes("A");
19378
+ if (needsRgbConversion || needsAlphaConversion) {
19379
+ for (let i = 0; i < combinedLayout.allocationSize; i += 4) {
19380
+ if (needsRgbConversion) {
19381
+ const r = destBytes[i];
19382
+ const b = destBytes[i + 2];
19383
+ destBytes[i] = b;
19384
+ destBytes[i + 2] = r;
19385
+ }
19386
+ if (needsAlphaConversion) {
19387
+ destBytes[i + 3] = 255;
19388
+ }
19389
+ }
19390
+ }
19391
+ }
19392
+ return planeLayouts;
19215
19393
  }
19216
19394
  /**
19217
19395
  * Converts this video sample to a VideoFrame for use with the WebCodecs API. The VideoFrame returned by this
@@ -19222,7 +19400,43 @@ var Mediabunny = (() => {
19222
19400
  throw new Error("VideoSample is closed.");
19223
19401
  }
19224
19402
  assert(this._data !== null);
19225
- if (isVideoFrame(this._data)) {
19403
+ if (this._data instanceof VideoSampleResource) {
19404
+ if (this.format === null) {
19405
+ throw new Error(
19406
+ "Cannot convert a VideoSampleResource-backed VideoSample to VideoFrame if format is null."
19407
+ );
19408
+ }
19409
+ const planes = this._data.getDataPlanes();
19410
+ if (planes instanceof Promise) {
19411
+ throw new Error(
19412
+ "Cannot convert a VideoSampleResource-backed VideoSample to VideoFrame if getDataPlanes() returns a promise."
19413
+ );
19414
+ }
19415
+ const size = planes.reduce((a, b) => a + b.data.byteLength, 0);
19416
+ const buffer = new Uint8Array(size);
19417
+ let offset = 0;
19418
+ const offsets = [];
19419
+ for (const plane of planes) {
19420
+ buffer.set(plane.data, offset);
19421
+ offsets.push(offset);
19422
+ offset += plane.data.byteLength;
19423
+ }
19424
+ return new VideoFrame(buffer, {
19425
+ format: this.format,
19426
+ layout: planes.map((x, i) => ({
19427
+ offset: offsets[i],
19428
+ stride: x.stride
19429
+ })),
19430
+ codedWidth: this.codedWidth,
19431
+ codedHeight: this.codedHeight,
19432
+ timestamp: this.microsecondTimestamp,
19433
+ duration: this.microsecondDuration,
19434
+ colorSpace: this.colorSpace,
19435
+ displayWidth: this.squarePixelWidth,
19436
+ // Not display* since we're not passing rotation
19437
+ displayHeight: this.squarePixelHeight
19438
+ });
19439
+ } else if (isVideoFrame(this._data)) {
19226
19440
  return new VideoFrame(this._data, {
19227
19441
  timestamp: this.microsecondTimestamp,
19228
19442
  duration: this.microsecondDuration || void 0
@@ -19235,7 +19449,10 @@ var Mediabunny = (() => {
19235
19449
  codedHeight: this.codedHeight,
19236
19450
  timestamp: this.microsecondTimestamp,
19237
19451
  duration: this.microsecondDuration || void 0,
19238
- colorSpace: this.colorSpace
19452
+ colorSpace: this.colorSpace,
19453
+ displayWidth: this.squarePixelWidth,
19454
+ // Not display* since we're not passing rotation
19455
+ displayHeight: this.squarePixelHeight
19239
19456
  });
19240
19457
  } else {
19241
19458
  return new VideoFrame(this._data, {
@@ -19350,8 +19567,9 @@ var Mediabunny = (() => {
19350
19567
  const canvasHeight = context.canvas.height;
19351
19568
  const rotation = options.rotation ?? this.rotation;
19352
19569
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
19353
- if (options.crop) {
19354
- clampCropRectangle(options.crop, rotatedWidth, rotatedHeight);
19570
+ let finalCrop = options.crop;
19571
+ if (finalCrop) {
19572
+ finalCrop = clampCropRectangle(finalCrop, rotatedWidth, rotatedHeight);
19355
19573
  }
19356
19574
  let dx;
19357
19575
  let dy;
@@ -19414,7 +19632,7 @@ var Mediabunny = (() => {
19414
19632
  * Converts this video sample to a
19415
19633
  * [`CanvasImageSource`](https://udn.realityripple.com/docs/Web/API/CanvasImageSource) for drawing to a canvas.
19416
19634
  *
19417
- * You must use the value returned by this method immediately, as any VideoFrame created internally will
19635
+ * You must use the value returned by this method immediately, as any VideoFrame created internally may
19418
19636
  * automatically be closed in the next microtask.
19419
19637
  */
19420
19638
  toCanvasImageSource() {
@@ -19422,7 +19640,7 @@ var Mediabunny = (() => {
19422
19640
  throw new Error("VideoSample is closed.");
19423
19641
  }
19424
19642
  assert(this._data !== null);
19425
- if (this._data instanceof Uint8Array) {
19643
+ if (this._data instanceof VideoSampleResource || this._data instanceof Uint8Array) {
19426
19644
  const videoFrame = this.toVideoFrame();
19427
19645
  queueMicrotask(() => videoFrame.close());
19428
19646
  return videoFrame;
@@ -19430,6 +19648,142 @@ var Mediabunny = (() => {
19430
19648
  return this._data;
19431
19649
  }
19432
19650
  }
19651
+ /**
19652
+ * Transform this video sample to a new video sample given the options. Can be used to resize, rotate, and crop
19653
+ * the sample.
19654
+ *
19655
+ * In non-browser environments, this method will not work by default. To make it work, register a custom
19656
+ * transformer function via {@link registerVideoSampleTransformer}.
19657
+ */
19658
+ async transform(options) {
19659
+ if (!options || typeof options !== "object") {
19660
+ throw new TypeError("options must be an object.");
19661
+ }
19662
+ if (options.width !== void 0 && (!Number.isInteger(options.width) || options.width <= 0)) {
19663
+ throw new TypeError("options.width, when provided, must be a positive integer.");
19664
+ }
19665
+ if (options.height !== void 0 && (!Number.isInteger(options.height) || options.height <= 0)) {
19666
+ throw new TypeError("options.height, when provided, must be a positive integer.");
19667
+ }
19668
+ if (options.roundDimensionsTo !== void 0 && (!Number.isInteger(options.roundDimensionsTo) || options.roundDimensionsTo <= 0)) {
19669
+ throw new TypeError("options.roundDimensionsTo, when provided, must be a positive integer.");
19670
+ }
19671
+ if (options.fit !== void 0 && !["fill", "contain", "cover"].includes(options.fit)) {
19672
+ throw new TypeError('options.fit, when provided, must be one of "fill", "contain", or "cover".');
19673
+ }
19674
+ if (options.width !== void 0 && options.height !== void 0 && options.fit === void 0) {
19675
+ throw new TypeError(
19676
+ "When both options.width and options.height are provided, options.fit must also be provided."
19677
+ );
19678
+ }
19679
+ if (options.rotate !== void 0 && ![0, 90, 180, 270].includes(options.rotate)) {
19680
+ throw new TypeError("options.rotate, when provided, must be 0, 90, 180 or 270.");
19681
+ }
19682
+ if (options.crop !== void 0) {
19683
+ validateCropRectangle(options.crop, "options.");
19684
+ }
19685
+ if (options.alpha !== void 0 && !["keep", "discard"].includes(options.alpha)) {
19686
+ throw new TypeError("options.alpha, when provided, must be 'keep' or 'discard'.");
19687
+ }
19688
+ const rotation = normalizeRotation(this.rotation + (options.rotate ?? 0));
19689
+ const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
19690
+ let finalCrop = options.crop;
19691
+ if (finalCrop) {
19692
+ finalCrop = clampCropRectangle(finalCrop, rotatedWidth, rotatedHeight);
19693
+ }
19694
+ const cropWidth = finalCrop ? finalCrop.width : rotatedWidth;
19695
+ const cropHeight = finalCrop ? finalCrop.height : rotatedHeight;
19696
+ const originalAspectRatio = cropWidth / cropHeight;
19697
+ let targetWidth;
19698
+ let targetHeight;
19699
+ if (options.width !== void 0 && options.height === void 0) {
19700
+ targetWidth = options.width;
19701
+ targetHeight = targetWidth / originalAspectRatio;
19702
+ } else if (options.width === void 0 && options.height !== void 0) {
19703
+ targetHeight = options.height;
19704
+ targetWidth = targetHeight * originalAspectRatio;
19705
+ } else if (options.width !== void 0 && options.height !== void 0) {
19706
+ targetWidth = options.width;
19707
+ targetHeight = options.height;
19708
+ } else {
19709
+ targetWidth = cropWidth;
19710
+ targetHeight = cropHeight;
19711
+ }
19712
+ targetWidth = roundToMultiple(targetWidth, options.roundDimensionsTo ?? 1);
19713
+ targetHeight = roundToMultiple(targetHeight, options.roundDimensionsTo ?? 1);
19714
+ const description = {
19715
+ width: targetWidth,
19716
+ height: targetHeight,
19717
+ fit: options.fit ?? "fill",
19718
+ rotation,
19719
+ crop: finalCrop ?? {
19720
+ left: 0,
19721
+ top: 0,
19722
+ width: rotatedWidth,
19723
+ height: rotatedHeight
19724
+ },
19725
+ alpha: options.alpha ?? "keep"
19726
+ };
19727
+ for (const transformer of registeredVideoSampleTransformers) {
19728
+ let result = transformer(this, description);
19729
+ if (result instanceof Promise) result = await result;
19730
+ if (result !== null) {
19731
+ return result;
19732
+ }
19733
+ }
19734
+ let canvas = null;
19735
+ let canvasIsNew = false;
19736
+ for (const entry of transformationCanvasCache) {
19737
+ if (entry.canvas.width === description.width && entry.canvas.height === description.height) {
19738
+ canvas = entry.canvas;
19739
+ entry.age = transformationCanvasCacheNextAge++;
19740
+ break;
19741
+ }
19742
+ }
19743
+ if (canvas === null) {
19744
+ if (typeof OffscreenCanvas !== "undefined") {
19745
+ canvas = new OffscreenCanvas(description.width, description.height);
19746
+ } else {
19747
+ if (typeof window === "undefined" || typeof document === "undefined") {
19748
+ throw new Error(
19749
+ "Cannot transform VideoSamples in this environment. Either run in an environment with OffscreenCanvas or HTMLCanvasElement, or supply a custom VideoSample transformer using registerVideoSampleTransformer()."
19750
+ );
19751
+ }
19752
+ canvas = document.createElement("canvas");
19753
+ canvas.width = description.width;
19754
+ canvas.height = description.height;
19755
+ }
19756
+ canvasIsNew = true;
19757
+ if (transformationCanvasCache.length >= TRANSFORMATION_CANVAS_CACHE_MAX_SIZE) {
19758
+ transformationCanvasCache.splice(arrayArgmin(transformationCanvasCache, (x) => x.age), 1);
19759
+ }
19760
+ transformationCanvasCache.push({
19761
+ canvas,
19762
+ age: transformationCanvasCacheNextAge++
19763
+ });
19764
+ }
19765
+ const context = canvas.getContext("2d", {
19766
+ alpha: true
19767
+ });
19768
+ assert(context);
19769
+ if (description.alpha === "discard") {
19770
+ context.fillStyle = "black";
19771
+ context.fillRect(0, 0, description.width, description.height);
19772
+ } else if (!canvasIsNew) {
19773
+ context.clearRect(0, 0, description.width, description.height);
19774
+ }
19775
+ this.drawWithFit(context, {
19776
+ fit: description.fit,
19777
+ rotation: description.rotation,
19778
+ crop: description.crop
19779
+ });
19780
+ return new _VideoSample(canvas, {
19781
+ timestamp: this.timestamp,
19782
+ duration: this.duration,
19783
+ rotation: 0
19784
+ // Any previous rotation is now baked in
19785
+ });
19786
+ }
19433
19787
  /** Sets the rotation metadata of this video sample. */
19434
19788
  setRotation(newRotation) {
19435
19789
  if (![0, 90, 180, 270].includes(newRotation)) {
@@ -19456,6 +19810,16 @@ var Mediabunny = (() => {
19456
19810
  this.close();
19457
19811
  }
19458
19812
  };
19813
+ var registeredVideoSampleTransformers = [];
19814
+ var registerVideoSampleTransformer = (transformer) => {
19815
+ if (registeredVideoSampleTransformers.includes(transformer)) {
19816
+ return;
19817
+ }
19818
+ registeredVideoSampleTransformers.push(transformer);
19819
+ };
19820
+ var TRANSFORMATION_CANVAS_CACHE_MAX_SIZE = 3;
19821
+ var transformationCanvasCache = [];
19822
+ var transformationCanvasCacheNextAge = 0;
19459
19823
  var VideoSampleColorSpace = class {
19460
19824
  /** Creates a new VideoSampleColorSpace. */
19461
19825
  constructor(init) {
@@ -19651,15 +20015,135 @@ var Mediabunny = (() => {
19651
20015
  assert(false);
19652
20016
  }
19653
20017
  };
20018
+ var ParseVideoFrameCopyToOptions = (sample, options) => {
20019
+ const defaultRect = {
20020
+ left: 0,
20021
+ top: 0,
20022
+ width: sample.codedWidth,
20023
+ height: sample.codedHeight
20024
+ };
20025
+ const overrideRect = options.rect;
20026
+ const parsedRect = ParseVisibleRect(
20027
+ defaultRect,
20028
+ overrideRect,
20029
+ sample.codedWidth,
20030
+ sample.codedHeight,
20031
+ sample.format
20032
+ );
20033
+ const optLayout = options.layout;
20034
+ let format;
20035
+ if (!options.format || options.format === sample.format) {
20036
+ format = sample.format;
20037
+ } else if (["RGBA", "RGBX", "BGRA", "BGRX"].includes(options.format)) {
20038
+ format = options.format;
20039
+ } else {
20040
+ throw new Error("NotSupportedError: Invalid destination format.");
20041
+ }
20042
+ return ComputeLayoutAndAllocationSize(parsedRect, format, optLayout);
20043
+ };
20044
+ var ParseVisibleRect = (defaultRect, overrideRect, codedWidth, codedHeight, format) => {
20045
+ const sourceRect = { ...defaultRect };
20046
+ if (overrideRect !== void 0) {
20047
+ if (overrideRect.width === 0 || overrideRect.height === 0) {
20048
+ throw new TypeError("visibleRect dimensions cannot be zero.");
20049
+ }
20050
+ if ((overrideRect.x || 0) + (overrideRect.width || 0) > codedWidth) {
20051
+ throw new TypeError("visibleRect exceeds codedWidth.");
20052
+ }
20053
+ if ((overrideRect.y || 0) + (overrideRect.height || 0) > codedHeight) {
20054
+ throw new TypeError("visibleRect exceeds codedHeight.");
20055
+ }
20056
+ sourceRect.x = overrideRect.x || 0;
20057
+ sourceRect.y = overrideRect.y || 0;
20058
+ sourceRect.width = overrideRect.width || 0;
20059
+ sourceRect.height = overrideRect.height || 0;
20060
+ }
20061
+ const validAlignment = VerifyRectOffsetAlignment(format, sourceRect);
20062
+ if (!validAlignment) {
20063
+ throw new TypeError("visibleRect alignment is invalid for the format.");
20064
+ }
20065
+ return sourceRect;
20066
+ };
20067
+ var VerifyRectOffsetAlignment = (format, rect) => {
20068
+ if (format === null) return true;
20069
+ const planes = getPlaneConfigs(format);
20070
+ for (let planeIndex = 0; planeIndex < planes.length; planeIndex++) {
20071
+ const plane = planes[planeIndex];
20072
+ const sampleWidth = plane.widthDivisor;
20073
+ const sampleHeight = plane.heightDivisor;
20074
+ if ((rect.x || 0) % sampleWidth !== 0) return false;
20075
+ if ((rect.y || 0) % sampleHeight !== 0) return false;
20076
+ }
20077
+ return true;
20078
+ };
20079
+ var ComputeLayoutAndAllocationSize = (parsedRect, format, layout) => {
20080
+ const planes = getPlaneConfigs(format);
20081
+ const numPlanes = planes.length;
20082
+ if (layout !== void 0 && layout.length !== numPlanes) {
20083
+ throw new TypeError(`Layout must have ${numPlanes} planes.`);
20084
+ }
20085
+ let minAllocationSize = 0;
20086
+ const computedLayouts = [];
20087
+ const endOffsets = [];
20088
+ for (let planeIndex = 0; planeIndex < numPlanes; planeIndex++) {
20089
+ const plane = planes[planeIndex];
20090
+ const sampleBytes = plane.sampleBytes;
20091
+ const sampleWidth = plane.widthDivisor;
20092
+ const sampleHeight = plane.heightDivisor;
20093
+ const computedLayout = {
20094
+ destinationOffset: 0,
20095
+ destinationStride: 0,
20096
+ sourceTop: 0,
20097
+ sourceHeight: 0,
20098
+ sourceLeftBytes: 0,
20099
+ sourceWidthBytes: 0
20100
+ };
20101
+ computedLayout.sourceTop = Math.ceil(Math.trunc(parsedRect.y || 0) / sampleHeight);
20102
+ computedLayout.sourceHeight = Math.ceil(Math.trunc(parsedRect.height || 0) / sampleHeight);
20103
+ computedLayout.sourceLeftBytes = Math.floor(Math.trunc(parsedRect.x || 0) / sampleWidth) * sampleBytes;
20104
+ computedLayout.sourceWidthBytes = Math.floor(Math.trunc(parsedRect.width || 0) / sampleWidth) * sampleBytes;
20105
+ if (layout !== void 0) {
20106
+ const planeLayout = layout[planeIndex];
20107
+ if (planeLayout.stride < computedLayout.sourceWidthBytes) {
20108
+ throw new TypeError(`Stride for plane ${planeIndex} is too small.`);
20109
+ }
20110
+ computedLayout.destinationOffset = planeLayout.offset;
20111
+ computedLayout.destinationStride = planeLayout.stride;
20112
+ } else {
20113
+ computedLayout.destinationOffset = minAllocationSize;
20114
+ computedLayout.destinationStride = computedLayout.sourceWidthBytes;
20115
+ }
20116
+ const planeSize = computedLayout.destinationStride * computedLayout.sourceHeight;
20117
+ const planeEnd = planeSize + computedLayout.destinationOffset;
20118
+ if (planeEnd > 4294967295) {
20119
+ throw new TypeError("Allocation size exceeds limit.");
20120
+ }
20121
+ endOffsets.push(planeEnd);
20122
+ minAllocationSize = Math.max(minAllocationSize, planeEnd);
20123
+ for (let earlierPlaneIndex = 0; earlierPlaneIndex < planeIndex; earlierPlaneIndex++) {
20124
+ const earlierLayout = computedLayouts[earlierPlaneIndex];
20125
+ if (endOffsets[planeIndex] <= earlierLayout.destinationOffset || endOffsets[earlierPlaneIndex] <= computedLayout.destinationOffset) {
20126
+ continue;
20127
+ }
20128
+ throw new TypeError("Planes overlap.");
20129
+ }
20130
+ computedLayouts.push(computedLayout);
20131
+ }
20132
+ return {
20133
+ allocationSize: minAllocationSize,
20134
+ computedLayouts
20135
+ };
20136
+ };
19654
20137
  var AUDIO_SAMPLE_FORMATS = /* @__PURE__ */ new Set(
19655
20138
  ["f32", "f32-planar", "s16", "s16-planar", "s32", "s32-planar", "u8", "u8-planar"]
19656
20139
  );
20140
+ var AudioSampleResource = class {
20141
+ constructor() {
20142
+ /** @internal */
20143
+ this._referenceCount = 0;
20144
+ }
20145
+ };
19657
20146
  var AudioSample = class _AudioSample {
19658
- /**
19659
- * Creates a new {@link AudioSample}, either from an existing
19660
- * [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData) or from raw bytes specified in
19661
- * {@link AudioSampleInit}.
19662
- */
19663
20147
  constructor(init) {
19664
20148
  /** @internal */
19665
20149
  this._closed = false;
@@ -19674,6 +20158,30 @@ var Mediabunny = (() => {
19674
20158
  this.numberOfChannels = init.numberOfChannels;
19675
20159
  this.timestamp = init.timestamp / 1e6;
19676
20160
  this.duration = init.numberOfFrames / init.sampleRate;
20161
+ } else if (init instanceof AudioSampleResource) {
20162
+ this._data = init;
20163
+ init._referenceCount++;
20164
+ this.format = init.getFormat();
20165
+ if (!AUDIO_SAMPLE_FORMATS.has(this.format)) {
20166
+ throw new TypeError("getFormat() must return an AudioSampleFormat.");
20167
+ }
20168
+ this.sampleRate = init.getSampleRate();
20169
+ if (!Number.isInteger(this.sampleRate) || this.sampleRate <= 0) {
20170
+ throw new TypeError("getSampleRate() must return a positive integer.");
20171
+ }
20172
+ this.numberOfFrames = init.getNumberOfFrames();
20173
+ if (!Number.isInteger(this.numberOfFrames) || this.numberOfFrames < 0) {
20174
+ throw new TypeError("getNumberOfFrames() must return a non-negative integer.");
20175
+ }
20176
+ this.numberOfChannels = init.getNumberOfChannels();
20177
+ if (!Number.isInteger(this.numberOfChannels) || this.numberOfChannels <= 0) {
20178
+ throw new TypeError("getNumberOfChannels() must return a positive integer.");
20179
+ }
20180
+ this.timestamp = init.getTimestamp();
20181
+ if (!Number.isFinite(this.timestamp)) {
20182
+ throw new TypeError("getTimestamp() must return a finite number.");
20183
+ }
20184
+ this.duration = this.numberOfFrames / this.sampleRate;
19677
20185
  } else {
19678
20186
  if (!init || typeof init !== "object") {
19679
20187
  throw new TypeError("Invalid AudioDataInit: must be an object.");
@@ -19787,7 +20295,8 @@ var Mediabunny = (() => {
19787
20295
  if (this._closed) {
19788
20296
  throw new Error("AudioSample is closed.");
19789
20297
  }
19790
- const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
20298
+ const { format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
20299
+ let { planeIndex } = options;
19791
20300
  const srcFormat = this.format;
19792
20301
  const destFormat = format ?? this.format;
19793
20302
  if (!destFormat) throw new Error("Destination format not determined");
@@ -19837,11 +20346,42 @@ var Mediabunny = (() => {
19837
20346
  });
19838
20347
  }
19839
20348
  } else {
19840
- const uint8Data = this._data;
19841
- const srcView = toDataView(uint8Data);
19842
20349
  const readFn = getReadFunction(srcFormat);
19843
20350
  const srcBytesPerSample = getBytesPerSample(srcFormat);
19844
20351
  const srcIsPlanar = formatIsPlanar(srcFormat);
20352
+ let uint8Data;
20353
+ if (this._data instanceof AudioSampleResource) {
20354
+ const getDataPlaneValidated = (index) => {
20355
+ const result = this._data.getDataPlane(index);
20356
+ if (!(result instanceof Uint8Array)) {
20357
+ throw new TypeError("getDataPlane() must return a Uint8Array.");
20358
+ }
20359
+ const expectedSize = numFrames * srcBytesPerSample * (srcIsPlanar ? 1 : numChannels);
20360
+ if (result.byteLength !== expectedSize) {
20361
+ throw new TypeError(
20362
+ `Data plane ${index} has invalid size. Expected exactly ${expectedSize} bytes, got ${result.byteLength} bytes.`
20363
+ );
20364
+ }
20365
+ return result;
20366
+ };
20367
+ if (srcIsPlanar) {
20368
+ if (destIsPlanar) {
20369
+ uint8Data = getDataPlaneValidated(planeIndex);
20370
+ planeIndex = 0;
20371
+ } else {
20372
+ uint8Data = new Uint8Array(numFrames * srcBytesPerSample * numChannels);
20373
+ for (let ch = 0; ch < numChannels; ch++) {
20374
+ const planeData = getDataPlaneValidated(ch);
20375
+ uint8Data.set(planeData, ch * numFrames * srcBytesPerSample);
20376
+ }
20377
+ }
20378
+ } else {
20379
+ uint8Data = getDataPlaneValidated(0);
20380
+ }
20381
+ } else {
20382
+ uint8Data = this._data;
20383
+ }
20384
+ const srcView = toDataView(uint8Data);
19845
20385
  for (let i = 0; i < copyFrameCount; i++) {
19846
20386
  if (destIsPlanar) {
19847
20387
  const destOffset = i * destBytesPerSample;
@@ -19875,7 +20415,11 @@ var Mediabunny = (() => {
19875
20415
  if (this._closed) {
19876
20416
  throw new Error("AudioSample is closed.");
19877
20417
  }
19878
- if (isAudioData(this._data)) {
20418
+ if (this._data instanceof AudioSampleResource) {
20419
+ const sample = new _AudioSample(this._data);
20420
+ sample.setTimestamp(this.timestamp);
20421
+ return sample;
20422
+ } else if (isAudioData(this._data)) {
19879
20423
  const sample = new _AudioSample(this._data.clone());
19880
20424
  sample.setTimestamp(this.timestamp);
19881
20425
  return sample;
@@ -19899,7 +20443,12 @@ var Mediabunny = (() => {
19899
20443
  return;
19900
20444
  }
19901
20445
  finalizationRegistry?.unregister(this);
19902
- if (isAudioData(this._data)) {
20446
+ if (this._data instanceof AudioSampleResource) {
20447
+ this._data._referenceCount--;
20448
+ if (this._data._referenceCount === 0) {
20449
+ this._data.close();
20450
+ }
20451
+ } else if (isAudioData(this._data)) {
19903
20452
  this._data.close();
19904
20453
  } else {
19905
20454
  this._data = new Uint8Array(0);
@@ -19914,36 +20463,13 @@ var Mediabunny = (() => {
19914
20463
  if (this._closed) {
19915
20464
  throw new Error("AudioSample is closed.");
19916
20465
  }
19917
- if (isAudioData(this._data)) {
20466
+ if (this._data instanceof AudioSampleResource) {
20467
+ return this._createAudioDataFromData();
20468
+ } else if (isAudioData(this._data)) {
19918
20469
  if (this._data.timestamp === this.microsecondTimestamp) {
19919
20470
  return this._data.clone();
19920
20471
  } else {
19921
- if (formatIsPlanar(this.format)) {
19922
- const size = this.allocationSize({ planeIndex: 0, format: this.format });
19923
- const data = new ArrayBuffer(size * this.numberOfChannels);
19924
- for (let i = 0; i < this.numberOfChannels; i++) {
19925
- this.copyTo(new Uint8Array(data, i * size, size), { planeIndex: i, format: this.format });
19926
- }
19927
- return new AudioData({
19928
- format: this.format,
19929
- sampleRate: this.sampleRate,
19930
- numberOfFrames: this.numberOfFrames,
19931
- numberOfChannels: this.numberOfChannels,
19932
- timestamp: this.microsecondTimestamp,
19933
- data
19934
- });
19935
- } else {
19936
- const data = new ArrayBuffer(this.allocationSize({ planeIndex: 0, format: this.format }));
19937
- this.copyTo(data, { planeIndex: 0, format: this.format });
19938
- return new AudioData({
19939
- format: this.format,
19940
- sampleRate: this.sampleRate,
19941
- numberOfFrames: this.numberOfFrames,
19942
- numberOfChannels: this.numberOfChannels,
19943
- timestamp: this.microsecondTimestamp,
19944
- data
19945
- });
19946
- }
20472
+ return this._createAudioDataFromData();
19947
20473
  }
19948
20474
  } else {
19949
20475
  return new AudioData({
@@ -19957,6 +20483,35 @@ var Mediabunny = (() => {
19957
20483
  });
19958
20484
  }
19959
20485
  }
20486
+ /** @internal */
20487
+ _createAudioDataFromData() {
20488
+ if (formatIsPlanar(this.format)) {
20489
+ const size = this.allocationSize({ planeIndex: 0, format: this.format });
20490
+ const data = new ArrayBuffer(size * this.numberOfChannels);
20491
+ for (let i = 0; i < this.numberOfChannels; i++) {
20492
+ this.copyTo(new Uint8Array(data, i * size, size), { planeIndex: i, format: this.format });
20493
+ }
20494
+ return new AudioData({
20495
+ format: this.format,
20496
+ sampleRate: this.sampleRate,
20497
+ numberOfFrames: this.numberOfFrames,
20498
+ numberOfChannels: this.numberOfChannels,
20499
+ timestamp: this.microsecondTimestamp,
20500
+ data
20501
+ });
20502
+ } else {
20503
+ const data = new ArrayBuffer(this.allocationSize({ planeIndex: 0, format: this.format }));
20504
+ this.copyTo(data, { planeIndex: 0, format: this.format });
20505
+ return new AudioData({
20506
+ format: this.format,
20507
+ sampleRate: this.sampleRate,
20508
+ numberOfFrames: this.numberOfFrames,
20509
+ numberOfChannels: this.numberOfChannels,
20510
+ timestamp: this.microsecondTimestamp,
20511
+ data
20512
+ });
20513
+ }
20514
+ }
19960
20515
  /** Convert this audio sample to an AudioBuffer for use with the Web Audio API. */
19961
20516
  toAudioBuffer() {
19962
20517
  if (this._closed) {
@@ -20270,9 +20825,9 @@ var Mediabunny = (() => {
20270
20825
  "When both config.transform.width and config.transform.height are provided, config.transform.fit must also be provided."
20271
20826
  );
20272
20827
  }
20273
- if (config.transform.fit !== void 0 && ["fill", "contain", "cover"].includes(config.sizeChangeBehavior)) {
20828
+ if (config.transform.fit !== void 0 && ["fill", "contain", "cover"].includes(config.sizeChangeBehavior) && config.transform.fit !== config.sizeChangeBehavior) {
20274
20829
  throw new TypeError(
20275
- "config.transform.fit cannot be used when config.sizeChangeBehavior is 'fill', 'contain' or 'cover', as sizeChangeBehavior already determines the fitting algorithm."
20830
+ "config.transform.fit, when provided, cannot differ from config.sizeChangeBehavior when config.sizeChangeBehavior is 'fill', 'contain' or 'cover', as sizeChangeBehavior already determines the fitting algorithm."
20276
20831
  );
20277
20832
  }
20278
20833
  if (config.transform.rotate !== void 0 && ![0, 90, 180, 270].includes(config.transform.rotate)) {
@@ -21117,7 +21672,7 @@ var Mediabunny = (() => {
21117
21672
  };
21118
21673
  var BaseMediaSampleSink = class {
21119
21674
  /** @internal */
21120
- mediaSamplesInRange(startTimestamp = 0, endTimestamp = Infinity, options) {
21675
+ mediaSamplesInRange(startTimestamp = -Infinity, endTimestamp = Infinity, options) {
21121
21676
  validateTimestamp(startTimestamp);
21122
21677
  validateTimestamp(endTimestamp);
21123
21678
  const sampleQueue = [];
@@ -21414,7 +21969,6 @@ var Mediabunny = (() => {
21414
21969
  return decodedSampleQueueSize === 0 ? 40 : 8;
21415
21970
  };
21416
21971
  var VideoDecoderWrapper = class extends DecoderWrapper {
21417
- // For HEVC stuff
21418
21972
  constructor(onSample, onError, codec, decoderConfig, rotation, timeResolution) {
21419
21973
  super(onSample, onError);
21420
21974
  this.codec = codec;
@@ -21438,13 +21992,14 @@ var Mediabunny = (() => {
21438
21992
  this.colorQueue = [];
21439
21993
  this.alphaQueue = [];
21440
21994
  this.merger = null;
21441
- this.mergerCreationFailed = false;
21442
21995
  this.decodedAlphaChunkCount = 0;
21443
21996
  this.alphaDecoderQueueSize = 0;
21444
21997
  /** Each value is the number of decoded alpha chunks at which a null alpha frame should be added. */
21445
21998
  this.nullAlphaFrameQueue = [];
21446
21999
  this.currentAlphaPacketIndex = 0;
21447
22000
  this.alphaRaslSkipped = false;
22001
+ // For HEVC stuff
22002
+ this.frameHandlerSerializer = new CallSerializer();
21448
22003
  const MatchingCustomDecoder = customVideoDecoders.find((x) => x.supports(codec, decoderConfig));
21449
22004
  if (MatchingCustomDecoder) {
21450
22005
  this.customDecoder = new MatchingCustomDecoder();
@@ -21459,13 +22014,15 @@ var Mediabunny = (() => {
21459
22014
  void this.customDecoderCallSerializer.call(() => this.customDecoder.init());
21460
22015
  } else {
21461
22016
  const colorHandler = (frame) => {
21462
- if (this.alphaQueue.length > 0) {
21463
- const alphaFrame = this.alphaQueue.shift();
21464
- assert(alphaFrame !== void 0);
21465
- this.mergeAlpha(frame, alphaFrame);
21466
- } else {
21467
- this.colorQueue.push(frame);
21468
- }
22017
+ this.frameHandlerSerializer.call(async () => {
22018
+ if (this.alphaQueue.length > 0) {
22019
+ const alphaFrame = this.alphaQueue.shift();
22020
+ assert(alphaFrame !== void 0);
22021
+ await this.mergeAlpha(frame, alphaFrame);
22022
+ } else {
22023
+ this.colorQueue.push(frame);
22024
+ }
22025
+ }).catch((error) => this.onError(error));
21469
22026
  };
21470
22027
  if (codec === "avc" && this.decoderConfig.description && isChromium()) {
21471
22028
  const record = deserializeAvcDecoderConfigurationRecord(toUint8Array(this.decoderConfig.description));
@@ -21546,41 +22103,36 @@ var Mediabunny = (() => {
21546
22103
  this.currentPacketIndex++;
21547
22104
  }
21548
22105
  decodeAlphaData(packet) {
21549
- if (!packet.sideData.alpha || this.mergerCreationFailed) {
22106
+ if (!packet.sideData.alpha) {
21550
22107
  this.pushNullAlphaFrame();
21551
22108
  return;
21552
22109
  }
21553
22110
  if (!this.merger) {
21554
- try {
21555
- this.merger = new ColorAlphaMerger();
21556
- } catch (error) {
21557
- console.error("Due to an error, only color data will be decoded.", error);
21558
- this.mergerCreationFailed = true;
21559
- this.decodeAlphaData(packet);
21560
- return;
21561
- }
22111
+ this.merger = new ColorAlphaMerger();
21562
22112
  }
21563
22113
  if (!this.alphaDecoder) {
21564
22114
  const alphaHandler = (frame) => {
21565
- this.alphaDecoderQueueSize--;
21566
- if (this.colorQueue.length > 0) {
21567
- const colorFrame = this.colorQueue.shift();
21568
- assert(colorFrame !== void 0);
21569
- this.mergeAlpha(colorFrame, frame);
21570
- } else {
21571
- this.alphaQueue.push(frame);
21572
- }
21573
- this.decodedAlphaChunkCount++;
21574
- while (this.nullAlphaFrameQueue.length > 0 && this.nullAlphaFrameQueue[0] === this.decodedAlphaChunkCount) {
21575
- this.nullAlphaFrameQueue.shift();
22115
+ this.frameHandlerSerializer.call(async () => {
21576
22116
  if (this.colorQueue.length > 0) {
21577
22117
  const colorFrame = this.colorQueue.shift();
21578
22118
  assert(colorFrame !== void 0);
21579
- this.mergeAlpha(colorFrame, null);
22119
+ await this.mergeAlpha(colorFrame, frame);
21580
22120
  } else {
21581
- this.alphaQueue.push(null);
22121
+ this.alphaQueue.push(frame);
22122
+ }
22123
+ this.decodedAlphaChunkCount++;
22124
+ while (this.nullAlphaFrameQueue.length > 0 && this.nullAlphaFrameQueue[0] === this.decodedAlphaChunkCount) {
22125
+ this.nullAlphaFrameQueue.shift();
22126
+ if (this.colorQueue.length > 0) {
22127
+ const colorFrame = this.colorQueue.shift();
22128
+ assert(colorFrame !== void 0);
22129
+ await this.mergeAlpha(colorFrame, null);
22130
+ } else {
22131
+ this.alphaQueue.push(null);
22132
+ }
21582
22133
  }
21583
- }
22134
+ this.alphaDecoderQueueSize--;
22135
+ }).catch((error) => this.onError(error));
21584
22136
  };
21585
22137
  const stack = new Error("Decoding error").stack;
21586
22138
  this.alphaDecoder = new VideoDecoder({
@@ -21662,20 +22214,14 @@ var Mediabunny = (() => {
21662
22214
  sample.setRotation(this.rotation);
21663
22215
  this.onSample(sample);
21664
22216
  }
21665
- mergeAlpha(color, alpha) {
22217
+ async mergeAlpha(color, alpha) {
21666
22218
  if (!alpha) {
21667
22219
  const finalSample2 = new VideoSample(color);
21668
22220
  this.sampleHandler(finalSample2);
21669
22221
  return;
21670
22222
  }
21671
22223
  assert(this.merger);
21672
- this.merger.update(color, alpha);
21673
- color.close();
21674
- alpha.close();
21675
- const finalFrame = new VideoFrame(this.merger.canvas, {
21676
- timestamp: color.timestamp,
21677
- duration: color.duration ?? void 0
21678
- });
22224
+ const finalFrame = await this.merger.update(color, alpha);
21679
22225
  const finalSample = new VideoSample(finalFrame);
21680
22226
  this.sampleHandler(finalSample);
21681
22227
  }
@@ -21688,6 +22234,7 @@ var Mediabunny = (() => {
21688
22234
  this.decoder.flush(),
21689
22235
  this.alphaDecoder?.flush()
21690
22236
  ]);
22237
+ await this.frameHandlerSerializer.currentPromise;
21691
22238
  this.colorQueue.forEach((x) => x.close());
21692
22239
  this.colorQueue.length = 0;
21693
22240
  this.alphaQueue.forEach((x) => x?.close());
@@ -21727,29 +22274,57 @@ var Mediabunny = (() => {
21727
22274
  this.sampleQueue.length = 0;
21728
22275
  }
21729
22276
  };
21730
- var ColorAlphaMerger = class {
22277
+ var mergerGpuUnavailable = false;
22278
+ var _ColorAlphaMerger = class _ColorAlphaMerger {
21731
22279
  constructor() {
21732
- if (typeof OffscreenCanvas !== "undefined") {
21733
- this.canvas = new OffscreenCanvas(300, 150);
22280
+ this.canvas = null;
22281
+ this.gl = null;
22282
+ this.program = null;
22283
+ this.vao = null;
22284
+ this.colorTexture = null;
22285
+ this.alphaTexture = null;
22286
+ this.worker = null;
22287
+ this.pendingRequests = /* @__PURE__ */ new Map();
22288
+ this.nextRequestId = 0;
22289
+ const canMakeCanvas = typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined" && typeof document.createElement === "function";
22290
+ if (!_ColorAlphaMerger.forceCpu && canMakeCanvas && !mergerGpuUnavailable) {
22291
+ try {
22292
+ if (typeof OffscreenCanvas !== "undefined") {
22293
+ this.canvas = new OffscreenCanvas(300, 150);
22294
+ } else {
22295
+ this.canvas = document.createElement("canvas");
22296
+ }
22297
+ const gl = this.canvas.getContext("webgl2", {
22298
+ premultipliedAlpha: false
22299
+ });
22300
+ if (!gl) {
22301
+ throw new Error("Couldn't acquire WebGL 2 context.");
22302
+ }
22303
+ this.gl = gl;
22304
+ this.program = this.createProgram();
22305
+ this.vao = this.createVAO();
22306
+ this.colorTexture = this.createTexture();
22307
+ this.alphaTexture = this.createTexture();
22308
+ this.gl.useProgram(this.program);
22309
+ this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_colorTexture"), 0);
22310
+ this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_alphaTexture"), 1);
22311
+ } catch (error) {
22312
+ this.gl = null;
22313
+ this.canvas = null;
22314
+ mergerGpuUnavailable = true;
22315
+ console.warn("Falling back to CPU for color/alpha merging.", error);
22316
+ }
22317
+ }
22318
+ }
22319
+ async update(color, alpha) {
22320
+ if (this.gl) {
22321
+ return this.updateGpu(color, alpha);
21734
22322
  } else {
21735
- this.canvas = document.createElement("canvas");
22323
+ return this.updateCpu(color, alpha);
21736
22324
  }
21737
- const gl = this.canvas.getContext("webgl2", {
21738
- premultipliedAlpha: false
21739
- });
21740
- if (!gl) {
21741
- throw new Error("Couldn't acquire WebGL 2 context.");
21742
- }
21743
- this.gl = gl;
21744
- this.program = this.createProgram();
21745
- this.vao = this.createVAO();
21746
- this.colorTexture = this.createTexture();
21747
- this.alphaTexture = this.createTexture();
21748
- this.gl.useProgram(this.program);
21749
- this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_colorTexture"), 0);
21750
- this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_alphaTexture"), 1);
21751
22325
  }
21752
22326
  createProgram() {
22327
+ assert(this.gl);
21753
22328
  const vertexShader = this.createShader(this.gl.VERTEX_SHADER, `#version 300 es
21754
22329
  in vec2 a_position;
21755
22330
  in vec2 a_texCoord;
@@ -21781,12 +22356,15 @@ var Mediabunny = (() => {
21781
22356
  return program;
21782
22357
  }
21783
22358
  createShader(type, source) {
22359
+ assert(this.gl);
21784
22360
  const shader = this.gl.createShader(type);
21785
22361
  this.gl.shaderSource(shader, source);
21786
22362
  this.gl.compileShader(shader);
21787
22363
  return shader;
21788
22364
  }
21789
22365
  createVAO() {
22366
+ assert(this.gl);
22367
+ assert(this.program);
21790
22368
  const vao = this.gl.createVertexArray();
21791
22369
  this.gl.bindVertexArray(vao);
21792
22370
  const vertices = new Float32Array([
@@ -21819,6 +22397,7 @@ var Mediabunny = (() => {
21819
22397
  return vao;
21820
22398
  }
21821
22399
  createTexture() {
22400
+ assert(this.gl);
21822
22401
  const texture = this.gl.createTexture();
21823
22402
  this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
21824
22403
  this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
@@ -21827,7 +22406,9 @@ var Mediabunny = (() => {
21827
22406
  this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
21828
22407
  return texture;
21829
22408
  }
21830
- update(color, alpha) {
22409
+ updateGpu(color, alpha) {
22410
+ assert(this.gl);
22411
+ assert(this.canvas);
21831
22412
  if (color.displayWidth !== this.canvas.width || color.displayHeight !== this.canvas.height) {
21832
22413
  this.canvas.width = color.displayWidth;
21833
22414
  this.canvas.height = color.displayHeight;
@@ -21842,11 +22423,215 @@ var Mediabunny = (() => {
21842
22423
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
21843
22424
  this.gl.bindVertexArray(this.vao);
21844
22425
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
22426
+ const finalFrame = new VideoFrame(this.canvas, {
22427
+ timestamp: color.timestamp,
22428
+ duration: color.duration ?? void 0
22429
+ });
22430
+ color.close();
22431
+ alpha.close();
22432
+ return finalFrame;
22433
+ }
22434
+ updateCpu(color, alpha) {
22435
+ if (!this.worker) {
22436
+ const blob = new Blob(
22437
+ [`(${colorAlphaMergerWorkerCode.toString()})()`],
22438
+ { type: "application/javascript" }
22439
+ );
22440
+ const url2 = URL.createObjectURL(blob);
22441
+ this.worker = new Worker(url2);
22442
+ URL.revokeObjectURL(url2);
22443
+ this.worker.addEventListener("message", (event) => {
22444
+ const data = event.data;
22445
+ const pending2 = this.pendingRequests.get(data.id);
22446
+ if (!pending2) {
22447
+ return;
22448
+ }
22449
+ this.pendingRequests.delete(data.id);
22450
+ if ("error" in data) {
22451
+ pending2.reject(new Error(data.error));
22452
+ } else {
22453
+ pending2.resolve(data.frame);
22454
+ }
22455
+ });
22456
+ this.worker.addEventListener("error", (event) => {
22457
+ const error = new Error(event.message || "Color/alpha merge worker error.");
22458
+ for (const pending2 of this.pendingRequests.values()) {
22459
+ pending2.reject(error);
22460
+ }
22461
+ this.pendingRequests.clear();
22462
+ });
22463
+ }
22464
+ const id = this.nextRequestId++;
22465
+ const pending = promiseWithResolvers();
22466
+ this.pendingRequests.set(id, pending);
22467
+ this.worker.postMessage({ id, color, alpha }, { transfer: [color, alpha] });
22468
+ return pending.promise;
21845
22469
  }
21846
22470
  close() {
21847
- this.gl.getExtension("WEBGL_lose_context")?.loseContext();
22471
+ this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
21848
22472
  this.gl = null;
21849
- }
22473
+ this.canvas = null;
22474
+ this.worker?.terminate();
22475
+ this.worker = null;
22476
+ const error = new Error("Color/alpha merger closed.");
22477
+ for (const pending of this.pendingRequests.values()) {
22478
+ pending.reject(error);
22479
+ }
22480
+ this.pendingRequests.clear();
22481
+ }
22482
+ };
22483
+ _ColorAlphaMerger.forceCpu = true;
22484
+ var ColorAlphaMerger = _ColorAlphaMerger;
22485
+ var colorAlphaMergerWorkerCode = () => {
22486
+ let cpuAlphaBuffer = null;
22487
+ let cpuColorBuffer = null;
22488
+ let chain = Promise.resolve();
22489
+ self.addEventListener("message", (event) => {
22490
+ const { id, color, alpha } = event.data;
22491
+ chain = chain.then(async () => {
22492
+ try {
22493
+ const frame = await merge(color, alpha);
22494
+ self.postMessage({ id, frame }, { transfer: [frame] });
22495
+ } catch (error) {
22496
+ self.postMessage({ id, error: error.message });
22497
+ } finally {
22498
+ color.close();
22499
+ alpha.close();
22500
+ }
22501
+ });
22502
+ });
22503
+ const merge = async (color, alpha) => {
22504
+ const format = color.format;
22505
+ const alphaFormat = alpha.format;
22506
+ if (!format || !alphaFormat) {
22507
+ throw new Error("CPU color/alpha merging requires a known VideoFrame format.");
22508
+ }
22509
+ const colorIs10 = format.includes("P10");
22510
+ const colorIs12 = format.includes("P12");
22511
+ const alphaIs10 = alphaFormat.includes("P10");
22512
+ const alphaIs12 = alphaFormat.includes("P12");
22513
+ if (alphaIs10 !== colorIs10 || alphaIs12 !== colorIs12) {
22514
+ throw new Error(
22515
+ `CPU color/alpha merging requires the alpha frame to have the same bit depth as the color frame (color: '${format}', alpha: '${alphaFormat}').`
22516
+ );
22517
+ }
22518
+ const width = color.codedWidth;
22519
+ const height = color.codedHeight;
22520
+ if (format === "RGBX" || format === "RGBA" || format === "BGRX" || format === "BGRA") {
22521
+ return await mergeInterleavedRgba(color, alpha, width, height, format);
22522
+ } else if (format === "I420" || format === "I420P10" || format === "I420P12" || format === "I422" || format === "I422P10" || format === "I422P12" || format === "I444" || format === "I444P10" || format === "I444P12") {
22523
+ return await mergePlanarYuv(color, alpha, width, height, format);
22524
+ } else if (format === "NV12") {
22525
+ return await mergeNv12(color, alpha, width, height);
22526
+ }
22527
+ throw new Error(`CPU color/alpha merging does not support format '${format}'.`);
22528
+ };
22529
+ const mergeInterleavedRgba = async (color, alpha, width, height, format) => {
22530
+ const pixelCount = width * height;
22531
+ const output = new Uint8Array(pixelCount * 4);
22532
+ await color.copyTo(output);
22533
+ const alphaY = await readAlpha(alpha, width, height, 1);
22534
+ for (let i = 0, j = 3; i < pixelCount; i++, j += 4) {
22535
+ output[j] = alphaY[i];
22536
+ }
22537
+ const outputFormat = format === "RGBX" || format === "RGBA" ? "RGBA" : "BGRA";
22538
+ const init = {
22539
+ format: outputFormat,
22540
+ codedWidth: width,
22541
+ codedHeight: height,
22542
+ timestamp: color.timestamp,
22543
+ duration: color.duration ?? void 0,
22544
+ transfer: [output.buffer]
22545
+ };
22546
+ return new VideoFrame(output, init);
22547
+ };
22548
+ const mergePlanarYuv = async (color, alpha, width, height, format) => {
22549
+ const is10 = format.includes("P10");
22550
+ const is12 = format.includes("P12");
22551
+ const bytesPerSample = is10 || is12 ? 2 : 1;
22552
+ let chromaW;
22553
+ let chromaH;
22554
+ if (format.startsWith("I420")) {
22555
+ chromaW = Math.ceil(width / 2);
22556
+ chromaH = Math.ceil(height / 2);
22557
+ } else if (format.startsWith("I422")) {
22558
+ chromaW = Math.ceil(width / 2);
22559
+ chromaH = height;
22560
+ } else {
22561
+ chromaW = width;
22562
+ chromaH = height;
22563
+ }
22564
+ const ySamples = width * height;
22565
+ const uvSamples = chromaW * chromaH;
22566
+ const yBytes = ySamples * bytesPerSample;
22567
+ const uvBytes = uvSamples * bytesPerSample;
22568
+ const aBytes = ySamples * bytesPerSample;
22569
+ const outputBytes = yBytes + 2 * uvBytes + aBytes;
22570
+ const output = new Uint8Array(outputBytes);
22571
+ await color.copyTo(output);
22572
+ const alphaY = await readAlpha(alpha, width, height, bytesPerSample);
22573
+ const aOffset = yBytes + 2 * uvBytes;
22574
+ output.set(alphaY, aOffset);
22575
+ const outputFormat = format.slice(0, 4) + "A" + format.slice(4);
22576
+ const init = {
22577
+ format: outputFormat,
22578
+ codedWidth: width,
22579
+ codedHeight: height,
22580
+ timestamp: color.timestamp,
22581
+ duration: color.duration ?? void 0,
22582
+ transfer: [output.buffer]
22583
+ };
22584
+ return new VideoFrame(output, init);
22585
+ };
22586
+ const mergeNv12 = async (color, alpha, width, height) => {
22587
+ const ySize = width * height;
22588
+ const chromaW = Math.ceil(width / 2);
22589
+ const chromaH = Math.ceil(height / 2);
22590
+ const uvSize = chromaW * chromaH;
22591
+ const sourceSize = color.allocationSize();
22592
+ if (!cpuColorBuffer || cpuColorBuffer.byteLength !== sourceSize) {
22593
+ cpuColorBuffer = new Uint8Array(sourceSize);
22594
+ }
22595
+ await color.copyTo(cpuColorBuffer);
22596
+ const output = new Uint8Array(ySize + 2 * uvSize + ySize);
22597
+ output.set(cpuColorBuffer.subarray(0, ySize), 0);
22598
+ const uOffset = ySize;
22599
+ const vOffset = ySize + uvSize;
22600
+ const uvStart = ySize;
22601
+ for (let i = 0; i < uvSize; i++) {
22602
+ output[uOffset + i] = cpuColorBuffer[uvStart + i * 2];
22603
+ output[vOffset + i] = cpuColorBuffer[uvStart + i * 2 + 1];
22604
+ }
22605
+ const alphaY = await readAlpha(alpha, width, height, 1);
22606
+ output.set(alphaY, ySize + 2 * uvSize);
22607
+ const init = {
22608
+ format: "I420A",
22609
+ codedWidth: width,
22610
+ codedHeight: height,
22611
+ timestamp: color.timestamp,
22612
+ duration: color.duration ?? void 0,
22613
+ transfer: [output.buffer]
22614
+ };
22615
+ return new VideoFrame(output, init);
22616
+ };
22617
+ const readAlpha = async (alpha, width, height, bytesPerSample) => {
22618
+ const size = alpha.allocationSize();
22619
+ if (!cpuAlphaBuffer || cpuAlphaBuffer.byteLength !== size) {
22620
+ cpuAlphaBuffer = new Uint8Array(size);
22621
+ }
22622
+ await alpha.copyTo(cpuAlphaBuffer);
22623
+ const format = alpha.format;
22624
+ if (format === "RGBA" || format === "BGRA" || format === "RGBX" || format === "BGRX") {
22625
+ const rOffset = format === "RGBA" || format === "RGBX" ? 0 : 2;
22626
+ const pixelCount = width * height;
22627
+ for (let i = 0; i < pixelCount; i++) {
22628
+ cpuAlphaBuffer[i] = cpuAlphaBuffer[i * 4 + rOffset];
22629
+ }
22630
+ return cpuAlphaBuffer.subarray(0, pixelCount);
22631
+ } else {
22632
+ return cpuAlphaBuffer.subarray(0, width * height * bytesPerSample);
22633
+ }
22634
+ };
21850
22635
  };
21851
22636
  var VideoSampleSink = class extends BaseMediaSampleSink {
21852
22637
  /** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
@@ -21898,7 +22683,7 @@ var Mediabunny = (() => {
21898
22683
  * @param endTimestamp - The timestamp in seconds at which to stop yielding samples (exclusive).
21899
22684
  * @param options - Options used for the underlying packet retrieval.
21900
22685
  */
21901
- samples(startTimestamp = 0, endTimestamp = Infinity, options = {}) {
22686
+ samples(startTimestamp, endTimestamp, options = {}) {
21902
22687
  return this.mediaSamplesInRange(startTimestamp, endTimestamp, options);
21903
22688
  }
21904
22689
  /**
@@ -21907,6 +22692,9 @@ var Mediabunny = (() => {
21907
22692
  * once, and is therefore more efficient than manually getting the sample for every timestamp. The iterator may
21908
22693
  * yield null if no frame is available for a given timestamp.
21909
22694
  *
22695
+ * This method is good for sparse access of media data. If you want primarily sequential media access, prefer
22696
+ * {@link VideoSampleSink.samples} instead.
22697
+ *
21910
22698
  * @param timestamps - An iterable or async iterable of timestamps in seconds.
21911
22699
  * @param options - Options used for the underlying packet retrieval.
21912
22700
  */
@@ -22064,7 +22852,7 @@ var Mediabunny = (() => {
22064
22852
  * @param endTimestamp - The timestamp in seconds at which to stop yielding canvases (exclusive).
22065
22853
  * @param options - Options used for the underlying packet retrieval.
22066
22854
  */
22067
- async *canvases(startTimestamp = 0, endTimestamp = Infinity, options) {
22855
+ async *canvases(startTimestamp, endTimestamp, options) {
22068
22856
  await this._ensureInit();
22069
22857
  yield* mapAsyncGenerator(
22070
22858
  this._videoSampleSink.samples(startTimestamp, endTimestamp, options),
@@ -22077,6 +22865,9 @@ var Mediabunny = (() => {
22077
22865
  * therefore more efficient than manually getting the canvas for every timestamp. The iterator may yield null if
22078
22866
  * no frame is available for a given timestamp.
22079
22867
  *
22868
+ * This method is good for sparse access of media data. If you want primarily sequential media access, prefer
22869
+ * {@link CanvasSink.canvases} instead.
22870
+ *
22080
22871
  * @param timestamps - An iterable or async iterable of timestamps in seconds.
22081
22872
  * @param options - Options used for the underlying packet retrieval.
22082
22873
  */
@@ -22098,9 +22889,18 @@ var Mediabunny = (() => {
22098
22889
  // Internal state to accumulate a precise current timestamp based on audio durations, not the (potentially
22099
22890
  // inaccurate) packet timestamps.
22100
22891
  this.currentTimestamp = null;
22892
+ // Chromium does not respect negative packet timestamps, so we must do the fixin' ourselves
22893
+ this.expectedFirstTimestamp = null;
22894
+ this.timestampOffset = 0;
22101
22895
  const sampleHandler = (sample) => {
22102
- if (this.currentTimestamp === null || Math.abs(sample.timestamp - this.currentTimestamp) >= sample.duration) {
22103
- this.currentTimestamp = sample.timestamp;
22896
+ let sampleTimestamp = sample.timestamp;
22897
+ if (this.expectedFirstTimestamp && this.currentTimestamp === null) {
22898
+ this.timestampOffset = this.expectedFirstTimestamp - sampleTimestamp;
22899
+ ;
22900
+ }
22901
+ sampleTimestamp += this.timestampOffset;
22902
+ if (this.currentTimestamp === null || Math.abs(sampleTimestamp - this.currentTimestamp) >= sample.duration) {
22903
+ this.currentTimestamp = sampleTimestamp;
22104
22904
  }
22105
22905
  const preciseTimestamp = this.currentTimestamp;
22106
22906
  this.currentTimestamp += sample.duration;
@@ -22156,16 +22956,20 @@ var Mediabunny = (() => {
22156
22956
  void this.customDecoderCallSerializer.call(() => this.customDecoder.decode(packet)).then(() => this.customDecoderQueueSize--);
22157
22957
  } else {
22158
22958
  assert(this.decoder);
22959
+ this.expectedFirstTimestamp ??= packet.timestamp;
22159
22960
  this.decoder.decode(packet.toEncodedAudioChunk());
22160
22961
  }
22161
22962
  }
22162
- flush() {
22963
+ async flush() {
22163
22964
  if (this.customDecoder) {
22164
- return this.customDecoderCallSerializer.call(() => this.customDecoder.flush());
22965
+ await this.customDecoderCallSerializer.call(() => this.customDecoder.flush());
22165
22966
  } else {
22166
22967
  assert(this.decoder);
22167
- return this.decoder.flush();
22968
+ await this.decoder.flush();
22168
22969
  }
22970
+ this.currentTimestamp = null;
22971
+ this.expectedFirstTimestamp = null;
22972
+ this.timestampOffset = 0;
22169
22973
  }
22170
22974
  close() {
22171
22975
  if (this.customDecoder) {
@@ -22408,7 +23212,7 @@ var Mediabunny = (() => {
22408
23212
  * @param endTimestamp - The timestamp in seconds at which to stop yielding samples (exclusive).
22409
23213
  * @param options - Options used for the underlying packet retrieval.
22410
23214
  */
22411
- samples(startTimestamp = 0, endTimestamp = Infinity, options = {}) {
23215
+ samples(startTimestamp, endTimestamp, options = {}) {
22412
23216
  return this.mediaSamplesInRange(startTimestamp, endTimestamp, options);
22413
23217
  }
22414
23218
  /**
@@ -22417,6 +23221,9 @@ var Mediabunny = (() => {
22417
23221
  * once, and is therefore more efficient than manually getting the sample for every timestamp. The iterator may
22418
23222
  * yield null if no sample is available for a given timestamp.
22419
23223
  *
23224
+ * This method is good for sparse access of media data. If you want primarily sequential media access, prefer
23225
+ * {@link AudioSampleSink.samples} instead.
23226
+ *
22420
23227
  * @param timestamps - An iterable or async iterable of timestamps in seconds.
22421
23228
  * @param options - Options used for the underlying packet retrieval.
22422
23229
  */
@@ -22463,7 +23270,7 @@ var Mediabunny = (() => {
22463
23270
  * @param endTimestamp - The timestamp in seconds at which to stop yielding buffers (exclusive).
22464
23271
  * @param options - Options used for the underlying packet retrieval.
22465
23272
  */
22466
- buffers(startTimestamp = 0, endTimestamp = Infinity, options) {
23273
+ buffers(startTimestamp, endTimestamp, options) {
22467
23274
  return mapAsyncGenerator(
22468
23275
  this._audioSampleSink.samples(startTimestamp, endTimestamp, options),
22469
23276
  (data) => this._audioSampleToWrappedArrayBuffer(data)
@@ -27814,7 +28621,7 @@ var Mediabunny = (() => {
27814
28621
  const timescale = computeRationalApproximation(
27815
28622
  1 / (track.metadata.frameRate ?? GLOBAL_TIMESCALE),
27816
28623
  1e6
27817
- ).denominator;
28624
+ ).den;
27818
28625
  const displayAspectWidth = decoderConfig.displayAspectWidth;
27819
28626
  const displayAspectHeight = decoderConfig.displayAspectHeight;
27820
28627
  const pixelAspectRatio = displayAspectWidth === void 0 || displayAspectHeight === void 0 ? { num: 1, den: 1 } : simplifyRational({
@@ -31324,7 +32131,6 @@ ${cue.notes ?? ""}`;
31324
32131
  this.encoder = null;
31325
32132
  this.muxer = null;
31326
32133
  this.lastMultipleOfKeyFrameInterval = -1;
31327
- this.resizeCanvas = null;
31328
32134
  // Tracks the input dimensions of the first frame
31329
32135
  this.codedWidth = null;
31330
32136
  this.codedHeight = null;
@@ -31353,12 +32159,6 @@ ${cue.notes ?? ""}`;
31353
32159
  */
31354
32160
  this.error = null;
31355
32161
  this.lastMuxerPromise = Promise.resolve();
31356
- const sizeChangeBehavior = encodingConfig.sizeChangeBehavior ?? "deny";
31357
- if (["fill", "contain", "cover"].includes(sizeChangeBehavior) && encodingConfig.transform?.fit !== void 0) {
31358
- throw new TypeError(
31359
- `Cannot set 'fit' when 'sizeChangeBehavior' is '${sizeChangeBehavior}'. The size change behavior determines the fit in this case.`
31360
- );
31361
- }
31362
32162
  }
31363
32163
  async add(videoSample, shouldClose, encodeOptions) {
31364
32164
  const originalSample = videoSample;
@@ -31384,17 +32184,8 @@ ${cue.notes ?? ""}`;
31384
32184
  const hasTransformConfig = config.transform?.width !== void 0 || config.transform?.height !== void 0 || config.transform?.rotate !== void 0 || config.transform?.crop !== void 0 || config.transform?.force === true;
31385
32185
  const needsTransform = hasTransformConfig || isSizeChange && sizeChangeBehavior !== "passThrough";
31386
32186
  if (needsTransform) {
31387
- const rotation = normalizeRotation(videoSample.rotation + (config.transform?.rotate ?? 0));
31388
- const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [videoSample.codedWidth, videoSample.codedHeight] : [videoSample.codedHeight, videoSample.codedWidth];
31389
- let finalCrop = config.transform?.crop;
31390
- if (finalCrop) {
31391
- finalCrop = clampCropRectangle(finalCrop, rotatedWidth, rotatedHeight);
31392
- }
31393
- const cropWidth = finalCrop ? finalCrop.width : rotatedWidth;
31394
- const cropHeight = finalCrop ? finalCrop.height : rotatedHeight;
31395
- const originalAspectRatio = cropWidth / cropHeight;
31396
- let targetWidth;
31397
- let targetHeight;
32187
+ let targetWidth = config.transform?.width;
32188
+ let targetHeight = config.transform?.height;
31398
32189
  let appliedFit = config.transform?.fit ?? "fill";
31399
32190
  if (isSizeChange && sizeChangeBehavior !== "passThrough") {
31400
32191
  assert(this.outputWidth);
@@ -31403,69 +32194,24 @@ ${cue.notes ?? ""}`;
31403
32194
  targetWidth = this.outputWidth;
31404
32195
  targetHeight = this.outputHeight;
31405
32196
  appliedFit = sizeChangeBehavior;
31406
- } else {
31407
- if (config.transform?.width !== void 0 && config.transform?.height === void 0) {
31408
- targetWidth = config.transform.width;
31409
- targetHeight = ceilToMultipleOfTwo(Math.round(targetWidth / originalAspectRatio));
31410
- } else if (config.transform?.width === void 0 && config.transform?.height !== void 0) {
31411
- targetHeight = config.transform.height;
31412
- targetWidth = ceilToMultipleOfTwo(Math.round(targetHeight * originalAspectRatio));
31413
- } else if (config.transform?.width !== void 0 && config.transform?.height !== void 0) {
31414
- targetWidth = config.transform?.width;
31415
- targetHeight = config.transform?.height;
31416
- } else {
31417
- targetWidth = cropWidth;
31418
- targetHeight = cropHeight;
31419
- }
31420
32197
  }
31421
- if (this.outputWidth === null || this.outputHeight === null) {
31422
- this.outputWidth = targetWidth;
31423
- this.outputHeight = targetHeight;
31424
- }
31425
- let canvasIsNew = false;
31426
- if (!this.resizeCanvas) {
31427
- if (typeof document !== "undefined") {
31428
- this.resizeCanvas = document.createElement("canvas");
31429
- this.resizeCanvas.width = targetWidth;
31430
- this.resizeCanvas.height = targetHeight;
31431
- } else {
31432
- this.resizeCanvas = new OffscreenCanvas(targetWidth, targetHeight);
31433
- }
31434
- canvasIsNew = true;
31435
- } else if (this.resizeCanvas.width !== targetWidth || this.resizeCanvas.height !== targetHeight) {
31436
- this.resizeCanvas.width = targetWidth;
31437
- this.resizeCanvas.height = targetHeight;
31438
- }
31439
- const context = this.resizeCanvas.getContext("2d", {
31440
- // Firefox has VideoFrame glitches with opaque canvases
31441
- alpha: this.encodingConfig.alpha === "keep" || isFirefox()
31442
- });
31443
- assert(context);
31444
- if (typeof context.resetTransform === "function") {
31445
- context.resetTransform();
31446
- }
31447
- if (!canvasIsNew) {
31448
- if (isFirefox()) {
31449
- context.fillStyle = "black";
31450
- context.fillRect(0, 0, targetWidth, targetHeight);
31451
- } else {
31452
- context.clearRect(0, 0, targetWidth, targetHeight);
31453
- }
31454
- }
31455
- videoSample.drawWithFit(context, {
32198
+ const transformed = await videoSample.transform({
32199
+ width: targetWidth,
32200
+ height: targetHeight,
32201
+ roundDimensionsTo: 2,
32202
+ crop: config.transform?.crop,
32203
+ rotate: config.transform?.rotate,
31456
32204
  fit: appliedFit,
31457
- rotation,
31458
- crop: finalCrop
32205
+ alpha: config.alpha
31459
32206
  });
32207
+ if (this.outputWidth === null || this.outputHeight === null) {
32208
+ this.outputWidth = transformed.displayWidth;
32209
+ this.outputHeight = transformed.displayHeight;
32210
+ }
31460
32211
  if (shouldClose) {
31461
32212
  videoSample.close();
31462
32213
  }
31463
- videoSample = new VideoSample(this.resizeCanvas, {
31464
- timestamp: videoSample.timestamp,
31465
- duration: videoSample.duration,
31466
- rotation: 0
31467
- // Rotation is now baked into the canvas
31468
- });
32214
+ videoSample = transformed;
31469
32215
  shouldClose = true;
31470
32216
  } else {
31471
32217
  if (this.outputWidth === null || this.outputHeight === null) {
@@ -31605,24 +32351,12 @@ ${cue.notes ?? ""}`;
31605
32351
  const width = videoFrame.displayWidth;
31606
32352
  const height = videoFrame.displayHeight;
31607
32353
  if (!this.splitter) {
31608
- try {
31609
- this.splitter = new ColorAlphaSplitter(width, height);
31610
- } catch (error) {
31611
- console.error("Due to an error, only color data will be encoded.", error);
31612
- this.splitterCreationFailed = true;
31613
- this.alphaFrameQueue.push(null);
31614
- this.encoder.encode(videoFrame, finalEncodeOptions);
31615
- videoFrame.close();
31616
- }
31617
- }
31618
- if (this.splitter) {
31619
- const colorFrame = this.splitter.extractColor(videoFrame);
31620
- const alphaFrame = this.splitter.extractAlpha(videoFrame);
31621
- this.alphaFrameQueue.push(alphaFrame);
31622
- this.encoder.encode(colorFrame, finalEncodeOptions);
31623
- colorFrame.close();
31624
- videoFrame.close();
32354
+ this.splitter = new ColorAlphaSplitter(width, height);
31625
32355
  }
32356
+ const { colorFrame, alphaFrame } = await this.splitter.update(videoFrame);
32357
+ this.alphaFrameQueue.push(alphaFrame);
32358
+ this.encoder.encode(colorFrame, finalEncodeOptions);
32359
+ colorFrame.close();
31626
32360
  }
31627
32361
  }
31628
32362
  if (this.encoder.encodeQueueSize >= 4) {
@@ -31849,35 +32583,78 @@ ${cue.notes ?? ""}`;
31849
32583
  }
31850
32584
  }
31851
32585
  };
31852
- var ColorAlphaSplitter = class {
32586
+ var splitterGpuUnavailable = false;
32587
+ var _ColorAlphaSplitter = class _ColorAlphaSplitter {
31853
32588
  constructor(initialWidth, initialHeight) {
31854
- this.lastFrame = null;
31855
- if (typeof OffscreenCanvas !== "undefined") {
31856
- this.canvas = new OffscreenCanvas(initialWidth, initialHeight);
32589
+ this.canvas = null;
32590
+ this.gl = null;
32591
+ this.colorProgram = null;
32592
+ this.alphaProgram = null;
32593
+ this.vao = null;
32594
+ this.sourceTexture = null;
32595
+ this.alphaResolutionLocation = null;
32596
+ this.worker = null;
32597
+ this.pendingRequests = /* @__PURE__ */ new Map();
32598
+ this.nextRequestId = 0;
32599
+ const canMakeCanvas = typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined" && typeof document.createElement === "function";
32600
+ if (!_ColorAlphaSplitter.forceCpu && canMakeCanvas && !splitterGpuUnavailable) {
32601
+ try {
32602
+ if (typeof OffscreenCanvas !== "undefined") {
32603
+ this.canvas = new OffscreenCanvas(initialWidth, initialHeight);
32604
+ } else {
32605
+ this.canvas = document.createElement("canvas");
32606
+ this.canvas.width = initialWidth;
32607
+ this.canvas.height = initialHeight;
32608
+ }
32609
+ const gl = this.canvas.getContext("webgl2", {
32610
+ alpha: true
32611
+ // Needed due to the YUV thing we do for alpha
32612
+ });
32613
+ if (!gl) {
32614
+ throw new Error("Couldn't acquire WebGL 2 context.");
32615
+ }
32616
+ this.gl = gl;
32617
+ this.colorProgram = this.createColorProgram();
32618
+ this.alphaProgram = this.createAlphaProgram();
32619
+ this.vao = this.createVAO();
32620
+ this.sourceTexture = this.createTexture();
32621
+ this.alphaResolutionLocation = this.gl.getUniformLocation(this.alphaProgram, "u_resolution");
32622
+ this.gl.useProgram(this.colorProgram);
32623
+ this.gl.uniform1i(this.gl.getUniformLocation(this.colorProgram, "u_sourceTexture"), 0);
32624
+ this.gl.useProgram(this.alphaProgram);
32625
+ this.gl.uniform1i(this.gl.getUniformLocation(this.alphaProgram, "u_sourceTexture"), 0);
32626
+ } catch (error) {
32627
+ this.gl = null;
32628
+ this.canvas = null;
32629
+ splitterGpuUnavailable = true;
32630
+ console.warn("Falling back to CPU for color/alpha splitting.", error);
32631
+ }
32632
+ }
32633
+ }
32634
+ async update(sourceFrame) {
32635
+ if (this.gl) {
32636
+ return this.updateGpu(sourceFrame);
31857
32637
  } else {
31858
- this.canvas = document.createElement("canvas");
31859
- this.canvas.width = initialWidth;
31860
- this.canvas.height = initialHeight;
32638
+ return this.updateCpu(sourceFrame);
31861
32639
  }
31862
- const gl = this.canvas.getContext("webgl2", {
31863
- alpha: true
31864
- // Needed due to the YUV thing we do for alpha
31865
- });
31866
- if (!gl) {
31867
- throw new Error("Couldn't acquire WebGL 2 context.");
31868
- }
31869
- this.gl = gl;
31870
- this.colorProgram = this.createColorProgram();
31871
- this.alphaProgram = this.createAlphaProgram();
31872
- this.vao = this.createVAO();
31873
- this.sourceTexture = this.createTexture();
31874
- this.alphaResolutionLocation = this.gl.getUniformLocation(this.alphaProgram, "u_resolution");
31875
- this.gl.useProgram(this.colorProgram);
31876
- this.gl.uniform1i(this.gl.getUniformLocation(this.colorProgram, "u_sourceTexture"), 0);
31877
- this.gl.useProgram(this.alphaProgram);
31878
- this.gl.uniform1i(this.gl.getUniformLocation(this.alphaProgram, "u_sourceTexture"), 0);
32640
+ }
32641
+ updateGpu(sourceFrame) {
32642
+ assert(this.gl);
32643
+ assert(this.canvas);
32644
+ if (sourceFrame.displayWidth !== this.canvas.width || sourceFrame.displayHeight !== this.canvas.height) {
32645
+ this.canvas.width = sourceFrame.displayWidth;
32646
+ this.canvas.height = sourceFrame.displayHeight;
32647
+ }
32648
+ this.gl.activeTexture(this.gl.TEXTURE0);
32649
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.sourceTexture);
32650
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceFrame);
32651
+ const colorFrame = this.runColorProgram(sourceFrame);
32652
+ const alphaFrame = this.runAlphaProgram(sourceFrame);
32653
+ sourceFrame.close();
32654
+ return { colorFrame, alphaFrame };
31879
32655
  }
31880
32656
  createVertexShader() {
32657
+ assert(this.gl);
31881
32658
  return this.createShader(this.gl.VERTEX_SHADER, `#version 300 es
31882
32659
  in vec2 a_position;
31883
32660
  in vec2 a_texCoord;
@@ -31890,6 +32667,7 @@ ${cue.notes ?? ""}`;
31890
32667
  `);
31891
32668
  }
31892
32669
  createColorProgram() {
32670
+ assert(this.gl);
31893
32671
  const vertexShader = this.createVertexShader();
31894
32672
  const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `#version 300 es
31895
32673
  precision highp float;
@@ -31910,6 +32688,7 @@ ${cue.notes ?? ""}`;
31910
32688
  return program;
31911
32689
  }
31912
32690
  createAlphaProgram() {
32691
+ assert(this.gl);
31913
32692
  const vertexShader = this.createVertexShader();
31914
32693
  const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `#version 300 es
31915
32694
  precision highp float;
@@ -31963,6 +32742,7 @@ ${cue.notes ?? ""}`;
31963
32742
  return program;
31964
32743
  }
31965
32744
  createShader(type, source) {
32745
+ assert(this.gl);
31966
32746
  const shader = this.gl.createShader(type);
31967
32747
  this.gl.shaderSource(shader, source);
31968
32748
  this.gl.compileShader(shader);
@@ -31972,6 +32752,8 @@ ${cue.notes ?? ""}`;
31972
32752
  return shader;
31973
32753
  }
31974
32754
  createVAO() {
32755
+ assert(this.gl);
32756
+ assert(this.colorProgram);
31975
32757
  const vao = this.gl.createVertexArray();
31976
32758
  this.gl.bindVertexArray(vao);
31977
32759
  const vertices = new Float32Array([
@@ -32004,6 +32786,7 @@ ${cue.notes ?? ""}`;
32004
32786
  return vao;
32005
32787
  }
32006
32788
  createTexture() {
32789
+ assert(this.gl);
32007
32790
  const texture = this.gl.createTexture();
32008
32791
  this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
32009
32792
  this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
@@ -32012,21 +32795,9 @@ ${cue.notes ?? ""}`;
32012
32795
  this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
32013
32796
  return texture;
32014
32797
  }
32015
- updateTexture(sourceFrame) {
32016
- if (this.lastFrame === sourceFrame) {
32017
- return;
32018
- }
32019
- if (sourceFrame.displayWidth !== this.canvas.width || sourceFrame.displayHeight !== this.canvas.height) {
32020
- this.canvas.width = sourceFrame.displayWidth;
32021
- this.canvas.height = sourceFrame.displayHeight;
32022
- }
32023
- this.gl.activeTexture(this.gl.TEXTURE0);
32024
- this.gl.bindTexture(this.gl.TEXTURE_2D, this.sourceTexture);
32025
- this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceFrame);
32026
- this.lastFrame = sourceFrame;
32027
- }
32028
- extractColor(sourceFrame) {
32029
- this.updateTexture(sourceFrame);
32798
+ runColorProgram(sourceFrame) {
32799
+ assert(this.gl);
32800
+ assert(this.canvas);
32030
32801
  this.gl.useProgram(this.colorProgram);
32031
32802
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
32032
32803
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
@@ -32038,8 +32809,9 @@ ${cue.notes ?? ""}`;
32038
32809
  alpha: "discard"
32039
32810
  });
32040
32811
  }
32041
- extractAlpha(sourceFrame) {
32042
- this.updateTexture(sourceFrame);
32812
+ runAlphaProgram(sourceFrame) {
32813
+ assert(this.gl);
32814
+ assert(this.canvas);
32043
32815
  this.gl.useProgram(this.alphaProgram);
32044
32816
  this.gl.uniform2f(this.alphaResolutionLocation, this.canvas.width, this.canvas.height);
32045
32817
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
@@ -32065,11 +32837,180 @@ ${cue.notes ?? ""}`;
32065
32837
  };
32066
32838
  return new VideoFrame(yuv, init);
32067
32839
  }
32840
+ updateCpu(sourceFrame) {
32841
+ if (!this.worker) {
32842
+ const blob = new Blob(
32843
+ [`(${colorAlphaSplitterWorkerCode.toString()})()`],
32844
+ { type: "application/javascript" }
32845
+ );
32846
+ const url2 = URL.createObjectURL(blob);
32847
+ this.worker = new Worker(url2);
32848
+ URL.revokeObjectURL(url2);
32849
+ this.worker.addEventListener("message", (event) => {
32850
+ const data = event.data;
32851
+ const pending2 = this.pendingRequests.get(data.id);
32852
+ if (!pending2) {
32853
+ return;
32854
+ }
32855
+ this.pendingRequests.delete(data.id);
32856
+ if ("error" in data) {
32857
+ pending2.reject(new Error(data.error));
32858
+ } else {
32859
+ pending2.resolve({ colorFrame: data.colorFrame, alphaFrame: data.alphaFrame });
32860
+ }
32861
+ });
32862
+ this.worker.addEventListener("error", (event) => {
32863
+ const error = new Error(event.message || "Color/alpha splitter worker error.");
32864
+ for (const pending2 of this.pendingRequests.values()) {
32865
+ pending2.reject(error);
32866
+ }
32867
+ this.pendingRequests.clear();
32868
+ });
32869
+ }
32870
+ const id = this.nextRequestId++;
32871
+ const pending = promiseWithResolvers();
32872
+ this.pendingRequests.set(id, pending);
32873
+ this.worker.postMessage({ id, sourceFrame }, { transfer: [sourceFrame] });
32874
+ return pending.promise;
32875
+ }
32068
32876
  close() {
32069
- this.gl.getExtension("WEBGL_lose_context")?.loseContext();
32877
+ this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
32070
32878
  this.gl = null;
32879
+ this.canvas = null;
32880
+ this.worker?.terminate();
32881
+ this.worker = null;
32882
+ const error = new Error("Color/alpha splitter closed.");
32883
+ for (const pending of this.pendingRequests.values()) {
32884
+ pending.reject(error);
32885
+ }
32886
+ this.pendingRequests.clear();
32071
32887
  }
32072
32888
  };
32889
+ _ColorAlphaSplitter.forceCpu = true;
32890
+ var ColorAlphaSplitter = _ColorAlphaSplitter;
32891
+ var colorAlphaSplitterWorkerCode = () => {
32892
+ let cpuSourceBuffer = null;
32893
+ let chain = Promise.resolve();
32894
+ self.addEventListener("message", (event) => {
32895
+ const { id, sourceFrame } = event.data;
32896
+ chain = chain.then(async () => {
32897
+ try {
32898
+ const { colorFrame, alphaFrame } = await split(sourceFrame);
32899
+ self.postMessage({ id, colorFrame, alphaFrame }, { transfer: [colorFrame, alphaFrame] });
32900
+ } catch (error) {
32901
+ self.postMessage({ id, error: error.message });
32902
+ } finally {
32903
+ sourceFrame.close();
32904
+ }
32905
+ });
32906
+ });
32907
+ const split = async (sourceFrame) => {
32908
+ const format = sourceFrame.format;
32909
+ if (!format) {
32910
+ throw new Error("CPU color/alpha splitting requires a known VideoFrame format.");
32911
+ }
32912
+ const width = sourceFrame.codedWidth;
32913
+ const height = sourceFrame.codedHeight;
32914
+ const sourceSize = sourceFrame.allocationSize();
32915
+ if (!cpuSourceBuffer || cpuSourceBuffer.byteLength !== sourceSize) {
32916
+ cpuSourceBuffer = new Uint8Array(sourceSize);
32917
+ }
32918
+ await sourceFrame.copyTo(cpuSourceBuffer);
32919
+ if (format === "RGBA" || format === "BGRA") {
32920
+ return splitInterleavedRgba(cpuSourceBuffer, width, height, format, sourceFrame);
32921
+ } else if (format === "I420A" || format === "I420AP10" || format === "I420AP12" || format === "I422A" || format === "I422AP10" || format === "I422AP12" || format === "I444A" || format === "I444AP10" || format === "I444AP12") {
32922
+ return splitPlanarYuvA(cpuSourceBuffer, width, height, format, sourceFrame);
32923
+ }
32924
+ throw new Error(`CPU color/alpha splitting does not support format '${format}'.`);
32925
+ };
32926
+ const splitInterleavedRgba = (source, width, height, format, sourceFrame) => {
32927
+ const pixelCount = width * height;
32928
+ const chromaW = Math.ceil(width / 2);
32929
+ const chromaH = Math.ceil(height / 2);
32930
+ const alphaSize = pixelCount + chromaW * chromaH * 2;
32931
+ const alphaBuffer = new Uint8Array(alphaSize);
32932
+ for (let i = 0, j = 3; i < pixelCount; i++, j += 4) {
32933
+ alphaBuffer[i] = source[j];
32934
+ }
32935
+ alphaBuffer.fill(128, pixelCount);
32936
+ const colorFrame = new VideoFrame(source, {
32937
+ format: format === "RGBA" ? "RGBX" : "BGRX",
32938
+ codedWidth: width,
32939
+ codedHeight: height,
32940
+ timestamp: sourceFrame.timestamp,
32941
+ duration: sourceFrame.duration ?? void 0
32942
+ // No transfer!
32943
+ });
32944
+ const alphaInit = {
32945
+ format: "I420",
32946
+ codedWidth: width,
32947
+ codedHeight: height,
32948
+ timestamp: sourceFrame.timestamp,
32949
+ duration: sourceFrame.duration ?? void 0,
32950
+ transfer: [alphaBuffer.buffer]
32951
+ };
32952
+ const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
32953
+ return { colorFrame, alphaFrame };
32954
+ };
32955
+ const splitPlanarYuvA = (source, width, height, format, sourceFrame) => {
32956
+ const is10 = format.includes("P10");
32957
+ const is12 = format.includes("P12");
32958
+ const bytesPerSample = is10 || is12 ? 2 : 1;
32959
+ let chromaW;
32960
+ let chromaH;
32961
+ if (format.startsWith("I420")) {
32962
+ chromaW = Math.ceil(width / 2);
32963
+ chromaH = Math.ceil(height / 2);
32964
+ } else if (format.startsWith("I422")) {
32965
+ chromaW = Math.ceil(width / 2);
32966
+ chromaH = height;
32967
+ } else {
32968
+ chromaW = width;
32969
+ chromaH = height;
32970
+ }
32971
+ const ySamples = width * height;
32972
+ const uvSamples = chromaW * chromaH;
32973
+ const yBytes = ySamples * bytesPerSample;
32974
+ const uvBytes = uvSamples * bytesPerSample;
32975
+ const aBytes = ySamples * bytesPerSample;
32976
+ const colorBytes = yBytes + uvBytes * 2;
32977
+ const colorFormat = format.replace("A", "");
32978
+ const alphaChromaW = Math.ceil(width / 2);
32979
+ const alphaChromaH = Math.ceil(height / 2);
32980
+ const alphaUvSamples = alphaChromaW * alphaChromaH;
32981
+ const alphaUvBytes = alphaUvSamples * bytesPerSample;
32982
+ const alphaSize = aBytes + 2 * alphaUvBytes;
32983
+ const alphaBuffer = new Uint8Array(alphaSize);
32984
+ const aPlaneStart = colorBytes;
32985
+ alphaBuffer.set(source.subarray(aPlaneStart, aPlaneStart + aBytes), 0);
32986
+ const uvOffset = aBytes;
32987
+ const neutralChroma = is10 ? 512 : is12 ? 2048 : 128;
32988
+ if (bytesPerSample === 1) {
32989
+ alphaBuffer.fill(neutralChroma, uvOffset);
32990
+ } else {
32991
+ const uvView = new Uint16Array(alphaBuffer.buffer, uvOffset, 2 * alphaUvSamples);
32992
+ uvView.fill(neutralChroma);
32993
+ }
32994
+ const alphaFormat = is10 ? "I420P10" : is12 ? "I420P12" : "I420";
32995
+ const colorFrame = new VideoFrame(source.subarray(0, colorBytes), {
32996
+ format: colorFormat,
32997
+ codedWidth: width,
32998
+ codedHeight: height,
32999
+ timestamp: sourceFrame.timestamp,
33000
+ duration: sourceFrame.duration ?? void 0
33001
+ });
33002
+ const alphaInit = {
33003
+ format: alphaFormat,
33004
+ codedWidth: width,
33005
+ codedHeight: height,
33006
+ timestamp: sourceFrame.timestamp,
33007
+ duration: sourceFrame.duration ?? void 0,
33008
+ transfer: [alphaBuffer.buffer]
33009
+ };
33010
+ const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
33011
+ return { colorFrame, alphaFrame };
33012
+ };
33013
+ };
32073
33014
  var VideoSampleSource = class extends VideoSource {
32074
33015
  /**
32075
33016
  * Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified
@@ -36036,6 +36977,9 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36036
36977
  throw new Error("Conversion cannot be executed twice.");
36037
36978
  }
36038
36979
  this._executed = true;
36980
+ for (const id of this._outputTrackIds) {
36981
+ this._synchronizer.declareTrack(id);
36982
+ }
36039
36983
  if (this.onProgress) {
36040
36984
  const uniqueUtilizedTracks = new Set(this.utilizedTracks);
36041
36985
  const durationPromises = [...uniqueUtilizedTracks].map(async (track) => {
@@ -36101,7 +37045,8 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36101
37045
  return;
36102
37046
  }
36103
37047
  let videoSource;
36104
- const totalRotation = normalizeRotation(await track.getRotation() + (trackOptions.rotate ?? 0));
37048
+ const innateRotation = await track.getRotation();
37049
+ const totalRotation = normalizeRotation(innateRotation + (trackOptions.rotate ?? 0));
36105
37050
  let outputTrackRotation = totalRotation;
36106
37051
  const canUseRotationMetadata = this.output.format.supportsVideoRotationMetadata && (trackOptions.allowRotationMetadata ?? true);
36107
37052
  const squarePixelWidth = await track.getSquarePixelWidth();
@@ -36191,10 +37136,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36191
37136
  keyFrameInterval: trackOptions.keyFrameInterval,
36192
37137
  sizeChangeBehavior: trackOptions.fit ?? "passThrough",
36193
37138
  alpha,
36194
- hardwareAcceleration: trackOptions.hardwareAcceleration
37139
+ hardwareAcceleration: trackOptions.hardwareAcceleration,
37140
+ transform: {}
36195
37141
  };
36196
- const source = new VideoSampleSource(encodingConfig);
36197
- videoSource = source;
37142
+ assert(encodingConfig.transform);
36198
37143
  let needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== void 0) || !!crop || squarePixelWidth !== await track.getCodedWidth() || squarePixelHeight !== await track.getCodedHeight();
36199
37144
  if (!needsRerender) {
36200
37145
  const tempOutput = new Output({
@@ -36221,136 +37166,36 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36221
37166
  await tempOutput.cancel();
36222
37167
  }
36223
37168
  }
37169
+ if (trackOptions.frameRate) {
37170
+ encodingConfig.transform.frameRate = trackOptions.frameRate;
37171
+ }
36224
37172
  if (needsRerender) {
36225
37173
  outputTrackRotation = 0;
36226
- this._trackPromises.push((async () => {
36227
- await this._started;
36228
- const sink = new CanvasSink(track, {
36229
- width,
36230
- height,
36231
- fit: trackOptions.fit ?? "fill",
36232
- rotation: totalRotation,
36233
- // Bake the rotation into the output
36234
- crop: trackOptions.crop,
36235
- poolSize: 1,
36236
- alpha: alpha === "keep"
36237
- });
36238
- const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
36239
- const frameRate = trackOptions.frameRate;
36240
- let lastCanvas = null;
36241
- let lastCanvasTimestamp = null;
36242
- let lastCanvasEndTimestamp = null;
36243
- const padFrames = async (until) => {
36244
- assert(lastCanvas);
36245
- assert(frameRate !== void 0);
36246
- const frameDifference = Math.round((until - lastCanvasTimestamp) * frameRate);
36247
- for (let i = 1; i < frameDifference; i++) {
36248
- const sample = new VideoSample(lastCanvas, {
36249
- timestamp: lastCanvasTimestamp + i / frameRate,
36250
- duration: 1 / frameRate
36251
- });
36252
- await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
36253
- sample.close();
36254
- }
36255
- };
36256
- for await (const { canvas, timestamp, duration } of iterator) {
36257
- if (this._canceled) {
36258
- return;
36259
- }
36260
- let adjustedSampleTimestamp = Math.max(timestamp - this._startTimestamp, 0);
36261
- lastCanvasEndTimestamp = adjustedSampleTimestamp + duration;
36262
- if (frameRate !== void 0) {
36263
- const alignedTimestamp = floorToDivisor(adjustedSampleTimestamp, frameRate);
36264
- if (lastCanvas !== null) {
36265
- if (alignedTimestamp <= lastCanvasTimestamp) {
36266
- lastCanvas = canvas;
36267
- lastCanvasTimestamp = alignedTimestamp;
36268
- continue;
36269
- } else {
36270
- await padFrames(alignedTimestamp);
36271
- }
36272
- }
36273
- adjustedSampleTimestamp = alignedTimestamp;
36274
- }
36275
- const sample = new VideoSample(canvas, {
36276
- timestamp: adjustedSampleTimestamp,
36277
- duration: frameRate !== void 0 ? 1 / frameRate : duration
36278
- });
36279
- await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
37174
+ encodingConfig.transform.width = width;
37175
+ encodingConfig.transform.height = height;
37176
+ encodingConfig.transform.fit = trackOptions.fit ?? "fill";
37177
+ encodingConfig.transform.rotate = normalizeRotation(totalRotation - innateRotation);
37178
+ encodingConfig.transform.crop = crop;
37179
+ encodingConfig.transform.alpha = alpha;
37180
+ }
37181
+ const source = new VideoSampleSource(encodingConfig);
37182
+ videoSource = source;
37183
+ this._trackPromises.push((async () => {
37184
+ await this._started;
37185
+ const sink = new VideoSampleSink(track);
37186
+ for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37187
+ if (this._canceled) {
36280
37188
  sample.close();
36281
- if (frameRate !== void 0) {
36282
- lastCanvas = canvas;
36283
- lastCanvasTimestamp = adjustedSampleTimestamp;
36284
- }
36285
- }
36286
- if (lastCanvas) {
36287
- assert(lastCanvasEndTimestamp !== null);
36288
- assert(frameRate !== void 0);
36289
- await padFrames(floorToDivisor(lastCanvasEndTimestamp, frameRate));
36290
- }
36291
- source.close();
36292
- this._synchronizer.closeTrack(outputTrackId);
36293
- })());
36294
- } else {
36295
- this._trackPromises.push((async () => {
36296
- await this._started;
36297
- const sink = new VideoSampleSink(track);
36298
- const frameRate = trackOptions.frameRate;
36299
- let lastSample = null;
36300
- let lastSampleTimestamp = null;
36301
- let lastSampleEndTimestamp = null;
36302
- const padFrames = async (until) => {
36303
- assert(lastSample);
36304
- assert(frameRate !== void 0);
36305
- const frameDifference = Math.round((until - lastSampleTimestamp) * frameRate);
36306
- for (let i = 1; i < frameDifference; i++) {
36307
- lastSample.setTimestamp(lastSampleTimestamp + i / frameRate);
36308
- lastSample.setDuration(1 / frameRate);
36309
- await this._registerVideoSample(trackOptions, outputTrackId, source, lastSample);
36310
- }
36311
- lastSample.close();
36312
- };
36313
- for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
36314
- if (this._canceled) {
36315
- sample.close();
36316
- lastSample?.close();
36317
- return;
36318
- }
36319
- let adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
36320
- lastSampleEndTimestamp = adjustedSampleTimestamp + sample.duration;
36321
- if (frameRate !== void 0) {
36322
- const alignedTimestamp = floorToDivisor(adjustedSampleTimestamp, frameRate);
36323
- if (lastSample !== null) {
36324
- if (alignedTimestamp <= lastSampleTimestamp) {
36325
- lastSample.close();
36326
- lastSample = sample;
36327
- lastSampleTimestamp = alignedTimestamp;
36328
- continue;
36329
- } else {
36330
- await padFrames(alignedTimestamp);
36331
- }
36332
- }
36333
- adjustedSampleTimestamp = alignedTimestamp;
36334
- sample.setDuration(1 / frameRate);
36335
- }
36336
- sample.setTimestamp(adjustedSampleTimestamp);
36337
- await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
36338
- if (frameRate !== void 0) {
36339
- lastSample = sample;
36340
- lastSampleTimestamp = adjustedSampleTimestamp;
36341
- } else {
36342
- sample.close();
36343
- }
36344
- }
36345
- if (lastSample) {
36346
- assert(lastSampleEndTimestamp !== null);
36347
- assert(frameRate !== void 0);
36348
- await padFrames(floorToDivisor(lastSampleEndTimestamp, frameRate));
37189
+ return;
36349
37190
  }
36350
- source.close();
36351
- this._synchronizer.closeTrack(outputTrackId);
36352
- })());
36353
- }
37191
+ const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37192
+ sample.setTimestamp(adjustedSampleTimestamp);
37193
+ await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
37194
+ sample.close();
37195
+ }
37196
+ source.close();
37197
+ this._synchronizer.closeTrack(outputTrackId);
37198
+ })());
36354
37199
  }
36355
37200
  let ownGroup = null;
36356
37201
  if (!trackOptions.group) {
@@ -36668,32 +37513,22 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36668
37513
  this.name = "ConversionCanceledError";
36669
37514
  }
36670
37515
  };
36671
- var MAX_TIMESTAMP_GAP = 5;
37516
+ var MAX_TIMESTAMP_GAP = 1;
36672
37517
  var TrackSynchronizer = class {
36673
37518
  constructor() {
36674
37519
  this.maxTimestamps = /* @__PURE__ */ new Map();
36675
37520
  // Track ID -> timestamp
36676
37521
  this.resolvers = [];
36677
37522
  }
36678
- computeMinAndMaybeResolve() {
36679
- let newMin = Infinity;
36680
- for (const [, timestamp] of this.maxTimestamps) {
36681
- newMin = Math.min(newMin, timestamp);
36682
- }
36683
- for (let i = 0; i < this.resolvers.length; i++) {
36684
- const entry = this.resolvers[i];
36685
- if (entry.timestamp - newMin < MAX_TIMESTAMP_GAP) {
36686
- entry.resolve();
36687
- this.resolvers.splice(i, 1);
36688
- i--;
36689
- }
36690
- }
36691
- return newMin;
37523
+ declareTrack(trackId) {
37524
+ this.maxTimestamps.set(trackId, 0);
36692
37525
  }
36693
37526
  shouldWait(trackId, timestamp) {
36694
- this.maxTimestamps.set(trackId, Math.max(timestamp, this.maxTimestamps.get(trackId) ?? -Infinity));
37527
+ const currentValue = this.maxTimestamps.get(trackId);
37528
+ assert(currentValue !== void 0);
37529
+ this.maxTimestamps.set(trackId, Math.max(timestamp, currentValue));
36695
37530
  const newMin = this.computeMinAndMaybeResolve();
36696
- return timestamp - newMin >= MAX_TIMESTAMP_GAP;
37531
+ return timestamp - newMin > MAX_TIMESTAMP_GAP;
36697
37532
  }
36698
37533
  wait(timestamp) {
36699
37534
  const { promise, resolve } = promiseWithResolvers();
@@ -36707,6 +37542,21 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
36707
37542
  this.maxTimestamps.delete(trackId);
36708
37543
  this.computeMinAndMaybeResolve();
36709
37544
  }
37545
+ computeMinAndMaybeResolve() {
37546
+ let newMin = Infinity;
37547
+ for (const [, timestamp] of this.maxTimestamps) {
37548
+ newMin = Math.min(newMin, timestamp);
37549
+ }
37550
+ for (let i = 0; i < this.resolvers.length; i++) {
37551
+ const entry = this.resolvers[i];
37552
+ if (entry.timestamp - newMin < MAX_TIMESTAMP_GAP) {
37553
+ entry.resolve();
37554
+ this.resolvers.splice(i, 1);
37555
+ i--;
37556
+ }
37557
+ }
37558
+ return newMin;
37559
+ }
36710
37560
  };
36711
37561
 
36712
37562
  // src/index.ts