mediabunny 1.19.0 → 1.20.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 (72) hide show
  1. package/dist/bundles/mediabunny.cjs +904 -104
  2. package/dist/bundles/mediabunny.min.cjs +90 -4
  3. package/dist/bundles/mediabunny.min.mjs +90 -4
  4. package/dist/bundles/mediabunny.mjs +904 -104
  5. package/dist/mediabunny.d.ts +76 -12
  6. package/dist/modules/src/codec-data.d.ts +3 -3
  7. package/dist/modules/src/codec-data.d.ts.map +1 -1
  8. package/dist/modules/src/codec-data.js +8 -13
  9. package/dist/modules/src/encode.d.ts +8 -0
  10. package/dist/modules/src/encode.d.ts.map +1 -1
  11. package/dist/modules/src/encode.js +6 -1
  12. package/dist/modules/src/index.d.ts +1 -1
  13. package/dist/modules/src/index.d.ts.map +1 -1
  14. package/dist/modules/src/input-track.d.ts +3 -0
  15. package/dist/modules/src/input-track.d.ts.map +1 -1
  16. package/dist/modules/src/input-track.js +7 -1
  17. package/dist/modules/src/input.d.ts.map +1 -1
  18. package/dist/modules/src/input.js +2 -4
  19. package/dist/modules/src/isobmff/isobmff-demuxer.js +3 -0
  20. package/dist/modules/src/matroska/ebml.d.ts +1 -0
  21. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  22. package/dist/modules/src/matroska/ebml.js +1 -0
  23. package/dist/modules/src/matroska/matroska-demuxer.d.ts +6 -0
  24. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  25. package/dist/modules/src/matroska/matroska-demuxer.js +60 -1
  26. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  27. package/dist/modules/src/matroska/matroska-muxer.js +9 -4
  28. package/dist/modules/src/media-sink.d.ts +5 -0
  29. package/dist/modules/src/media-sink.d.ts.map +1 -1
  30. package/dist/modules/src/media-sink.js +340 -50
  31. package/dist/modules/src/media-source.d.ts.map +1 -1
  32. package/dist/modules/src/media-source.js +413 -27
  33. package/dist/modules/src/misc.d.ts +1 -0
  34. package/dist/modules/src/misc.d.ts.map +1 -1
  35. package/dist/modules/src/misc.js +7 -2
  36. package/dist/modules/src/output-format.d.ts +8 -0
  37. package/dist/modules/src/output-format.d.ts.map +1 -1
  38. package/dist/modules/src/output-format.js +8 -0
  39. package/dist/modules/src/packet.d.ts +37 -6
  40. package/dist/modules/src/packet.d.ts.map +1 -1
  41. package/dist/modules/src/packet.js +49 -7
  42. package/dist/modules/src/reader.d.ts.map +1 -1
  43. package/dist/modules/src/reader.js +23 -4
  44. package/dist/modules/src/sample.d.ts +11 -2
  45. package/dist/modules/src/sample.d.ts.map +1 -1
  46. package/dist/modules/src/sample.js +17 -1
  47. package/dist/modules/src/source.d.ts.map +1 -1
  48. package/dist/modules/src/source.js +7 -3
  49. package/dist/modules/src/tags.d.ts +4 -4
  50. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  51. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  52. package/dist/modules/src/wave/wave-demuxer.js +7 -2
  53. package/package.json +5 -5
  54. package/src/codec-data.ts +14 -22
  55. package/src/encode.ts +14 -1
  56. package/src/index.ts +1 -0
  57. package/src/input-track.ts +10 -1
  58. package/src/input.ts +3 -5
  59. package/src/isobmff/isobmff-demuxer.ts +4 -0
  60. package/src/matroska/ebml.ts +1 -0
  61. package/src/matroska/matroska-demuxer.ts +62 -1
  62. package/src/matroska/matroska-muxer.ts +11 -4
  63. package/src/media-sink.ts +423 -63
  64. package/src/media-source.ts +498 -31
  65. package/src/misc.ts +8 -2
  66. package/src/output-format.ts +8 -0
  67. package/src/packet.ts +80 -5
  68. package/src/reader.ts +38 -4
  69. package/src/sample.ts +23 -2
  70. package/src/source.ts +9 -3
  71. package/src/tags.ts +4 -4
  72. package/src/wave/wave-demuxer.ts +6 -2
@@ -259,7 +259,7 @@ var Mediabunny = (() => {
259
259
  }
260
260
  };
261
261
  var toUint8Array = (source) => {
262
- if (source instanceof Uint8Array) {
262
+ if (source.constructor === Uint8Array) {
263
263
  return source;
264
264
  } else if (source instanceof ArrayBuffer) {
265
265
  return new Uint8Array(source);
@@ -268,7 +268,7 @@ var Mediabunny = (() => {
268
268
  }
269
269
  };
270
270
  var toDataView = (source) => {
271
- if (source instanceof DataView) {
271
+ if (source.constructor === DataView) {
272
272
  return source;
273
273
  } else if (source instanceof ArrayBuffer) {
274
274
  return new DataView(source);
@@ -706,6 +706,9 @@ var Mediabunny = (() => {
706
706
  }
707
707
  return true;
708
708
  };
709
+ var polyfillSymbolDispose = () => {
710
+ Symbol.dispose ??= Symbol("Symbol.dispose");
711
+ };
709
712
 
710
713
  // src/tags.ts
711
714
  var RichImageData = class {
@@ -2755,23 +2758,18 @@ var Mediabunny = (() => {
2755
2758
  }
2756
2759
  return { modeBlockflags };
2757
2760
  };
2758
- var determineVideoPacketType = async (videoTrack, packet) => {
2759
- assert(videoTrack.codec);
2760
- switch (videoTrack.codec) {
2761
+ var determineVideoPacketType = (codec, decoderConfig, packetData) => {
2762
+ switch (codec) {
2761
2763
  case "avc":
2762
2764
  {
2763
- const decoderConfig = await videoTrack.getDecoderConfig();
2764
- assert(decoderConfig);
2765
- const nalUnits = extractAvcNalUnits(packet.data, decoderConfig);
2765
+ const nalUnits = extractAvcNalUnits(packetData, decoderConfig);
2766
2766
  const isKeyframe = nalUnits.some((x) => extractNalUnitTypeForAvc(x) === 5 /* IDR */);
2767
2767
  return isKeyframe ? "key" : "delta";
2768
2768
  }
2769
2769
  ;
2770
2770
  case "hevc":
2771
2771
  {
2772
- const decoderConfig = await videoTrack.getDecoderConfig();
2773
- assert(decoderConfig);
2774
- const nalUnits = extractHevcNalUnits(packet.data, decoderConfig);
2772
+ const nalUnits = extractHevcNalUnits(packetData, decoderConfig);
2775
2773
  const isKeyframe = nalUnits.some((x) => {
2776
2774
  const type = extractNalUnitTypeForHevc(x);
2777
2775
  return 16 /* BLA_W_LP */ <= type && type <= 23 /* RSV_IRAP_VCL23 */;
@@ -2781,13 +2779,13 @@ var Mediabunny = (() => {
2781
2779
  ;
2782
2780
  case "vp8":
2783
2781
  {
2784
- const frameType = packet.data[0] & 1;
2782
+ const frameType = packetData[0] & 1;
2785
2783
  return frameType === 0 ? "key" : "delta";
2786
2784
  }
2787
2785
  ;
2788
2786
  case "vp9":
2789
2787
  {
2790
- const bitstream = new Bitstream(packet.data);
2788
+ const bitstream = new Bitstream(packetData);
2791
2789
  if (bitstream.readBits(2) !== 2) {
2792
2790
  return null;
2793
2791
  }
@@ -2809,7 +2807,7 @@ var Mediabunny = (() => {
2809
2807
  case "av1":
2810
2808
  {
2811
2809
  let reducedStillPictureHeader = false;
2812
- for (const { type, data } of iterateAv1PacketObus(packet.data)) {
2810
+ for (const { type, data } of iterateAv1PacketObus(packetData)) {
2813
2811
  if (type === 1) {
2814
2812
  const bitstream = new Bitstream(data);
2815
2813
  bitstream.skipBits(4);
@@ -2832,7 +2830,7 @@ var Mediabunny = (() => {
2832
2830
  ;
2833
2831
  default:
2834
2832
  {
2835
- assertNever(videoTrack.codec);
2833
+ assertNever(codec);
2836
2834
  assert(false);
2837
2835
  }
2838
2836
  ;
@@ -3263,7 +3261,7 @@ var Mediabunny = (() => {
3263
3261
  var PLACEHOLDER_DATA = new Uint8Array(0);
3264
3262
  var EncodedPacket = class _EncodedPacket {
3265
3263
  /** Creates a new {@link EncodedPacket} from raw bytes and timing information. */
3266
- constructor(data, type, timestamp, duration, sequenceNumber = -1, byteLength) {
3264
+ constructor(data, type, timestamp, duration, sequenceNumber = -1, byteLength, sideData) {
3267
3265
  this.data = data;
3268
3266
  this.type = type;
3269
3267
  this.timestamp = timestamp;
@@ -3295,7 +3293,20 @@ var Mediabunny = (() => {
3295
3293
  if (!Number.isInteger(byteLength) || byteLength < 0) {
3296
3294
  throw new TypeError("byteLength must be a non-negative integer.");
3297
3295
  }
3296
+ if (sideData !== void 0 && (typeof sideData !== "object" || !sideData)) {
3297
+ throw new TypeError("sideData, when provided, must be an object.");
3298
+ }
3299
+ if (sideData?.alpha !== void 0 && !(sideData.alpha instanceof Uint8Array)) {
3300
+ throw new TypeError("sideData.alpha, when provided, must be a Uint8Array.");
3301
+ }
3302
+ if (sideData?.alphaByteLength !== void 0 && (!Number.isInteger(sideData.alphaByteLength) || sideData.alphaByteLength < 0)) {
3303
+ throw new TypeError("sideData.alphaByteLength, when provided, must be a non-negative integer.");
3304
+ }
3298
3305
  this.byteLength = byteLength;
3306
+ this.sideData = sideData ?? {};
3307
+ if (this.sideData.alpha && this.sideData.alphaByteLength === void 0) {
3308
+ this.sideData.alphaByteLength = this.sideData.alpha.byteLength;
3309
+ }
3299
3310
  }
3300
3311
  /** If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. */
3301
3312
  get isMetadataOnly() {
@@ -3309,7 +3320,9 @@ var Mediabunny = (() => {
3309
3320
  get microsecondDuration() {
3310
3321
  return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
3311
3322
  }
3312
- /** Converts this packet to an EncodedVideoChunk for use with the WebCodecs API. */
3323
+ /** Converts this packet to an
3324
+ * [`EncodedVideoChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) for use with the
3325
+ * WebCodecs API. */
3313
3326
  toEncodedVideoChunk() {
3314
3327
  if (this.isMetadataOnly) {
3315
3328
  throw new TypeError("Metadata-only packets cannot be converted to a video chunk.");
@@ -3324,7 +3337,31 @@ var Mediabunny = (() => {
3324
3337
  duration: this.microsecondDuration
3325
3338
  });
3326
3339
  }
3327
- /** Converts this packet to an EncodedAudioChunk for use with the WebCodecs API. */
3340
+ /**
3341
+ * Converts this packet to an
3342
+ * [`EncodedVideoChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) for use with the
3343
+ * WebCodecs API, using the alpha side data instead of the color data. Throws if no alpha side data is defined.
3344
+ */
3345
+ alphaToEncodedVideoChunk(type = this.type) {
3346
+ if (!this.sideData.alpha) {
3347
+ throw new TypeError("This packet does not contain alpha side data.");
3348
+ }
3349
+ if (this.isMetadataOnly) {
3350
+ throw new TypeError("Metadata-only packets cannot be converted to a video chunk.");
3351
+ }
3352
+ if (typeof EncodedVideoChunk === "undefined") {
3353
+ throw new Error("Your browser does not support EncodedVideoChunk.");
3354
+ }
3355
+ return new EncodedVideoChunk({
3356
+ data: this.sideData.alpha,
3357
+ type,
3358
+ timestamp: this.microsecondTimestamp,
3359
+ duration: this.microsecondDuration
3360
+ });
3361
+ }
3362
+ /** Converts this packet to an
3363
+ * [`EncodedAudioChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk) for use with the
3364
+ * WebCodecs API. */
3328
3365
  toEncodedAudioChunk() {
3329
3366
  if (this.isMetadataOnly) {
3330
3367
  throw new TypeError("Metadata-only packets cannot be converted to an audio chunk.");
@@ -3340,10 +3377,12 @@ var Mediabunny = (() => {
3340
3377
  });
3341
3378
  }
3342
3379
  /**
3343
- * Creates an EncodedPacket from an EncodedVideoChunk or EncodedAudioChunk. This method is useful for converting
3344
- * chunks from the WebCodecs API to EncodedPackets.
3380
+ * Creates an {@link EncodedPacket} from an
3381
+ * [`EncodedVideoChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) or
3382
+ * [`EncodedAudioChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk). This method is useful
3383
+ * for converting chunks from the WebCodecs API to `EncodedPacket` instances.
3345
3384
  */
3346
- static fromEncodedChunk(chunk) {
3385
+ static fromEncodedChunk(chunk, sideData) {
3347
3386
  if (!(chunk instanceof EncodedVideoChunk || chunk instanceof EncodedAudioChunk)) {
3348
3387
  throw new TypeError("chunk must be an EncodedVideoChunk or EncodedAudioChunk.");
3349
3388
  }
@@ -3353,7 +3392,10 @@ var Mediabunny = (() => {
3353
3392
  data,
3354
3393
  chunk.type,
3355
3394
  chunk.timestamp / 1e6,
3356
- (chunk.duration ?? 0) / 1e6
3395
+ (chunk.duration ?? 0) / 1e6,
3396
+ void 0,
3397
+ void 0,
3398
+ sideData
3357
3399
  );
3358
3400
  }
3359
3401
  /** Clones this packet while optionally updating timing information. */
@@ -3455,6 +3497,7 @@ var Mediabunny = (() => {
3455
3497
  };
3456
3498
 
3457
3499
  // src/sample.ts
3500
+ polyfillSymbolDispose();
3458
3501
  var VideoSample = class _VideoSample {
3459
3502
  constructor(data, init) {
3460
3503
  /** @internal */
@@ -3586,6 +3629,13 @@ var Mediabunny = (() => {
3586
3629
  get microsecondDuration() {
3587
3630
  return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
3588
3631
  }
3632
+ /**
3633
+ * Whether this sample uses a pixel format that can hold transparency data. Note that this doesn't necessarily mean
3634
+ * that the sample is transparent.
3635
+ */
3636
+ get hasAlpha() {
3637
+ return this.format && this.format.includes("A");
3638
+ }
3589
3639
  /** Clones this video sample. */
3590
3640
  clone() {
3591
3641
  if (this._closed) {
@@ -3908,6 +3958,10 @@ var Mediabunny = (() => {
3908
3958
  }
3909
3959
  this.duration = newDuration;
3910
3960
  }
3961
+ /** Calls `.close()`. */
3962
+ [Symbol.dispose]() {
3963
+ this.close();
3964
+ }
3911
3965
  };
3912
3966
  var isVideoFrame = (x) => {
3913
3967
  return typeof VideoFrame !== "undefined" && x instanceof VideoFrame;
@@ -4286,6 +4340,10 @@ var Mediabunny = (() => {
4286
4340
  }
4287
4341
  this.timestamp = newTimestamp;
4288
4342
  }
4343
+ /** Calls `.close()`. */
4344
+ [Symbol.dispose]() {
4345
+ this.close();
4346
+ }
4289
4347
  /** @internal */
4290
4348
  static *_fromAudioBuffer(audioBuffer, timestamp) {
4291
4349
  if (!(audioBuffer instanceof AudioBuffer)) {
@@ -4988,6 +5046,20 @@ var Mediabunny = (() => {
4988
5046
  // Safari-specific thing, check usage.
4989
5047
  this.currentPacketIndex = 0;
4990
5048
  this.raslSkipped = false;
5049
+ // For HEVC stuff
5050
+ // Alpha stuff
5051
+ this.alphaDecoder = null;
5052
+ this.alphaHadKeyframe = false;
5053
+ this.colorQueue = [];
5054
+ this.alphaQueue = [];
5055
+ this.merger = null;
5056
+ this.mergerCreationFailed = false;
5057
+ this.decodedAlphaChunkCount = 0;
5058
+ this.alphaDecoderQueueSize = 0;
5059
+ /** Each value is the number of decoded alpha chunks at which a null alpha frame should be added. */
5060
+ this.nullAlphaFrameQueue = [];
5061
+ this.currentAlphaPacketIndex = 0;
5062
+ this.alphaRaslSkipped = false;
4991
5063
  const MatchingCustomDecoder = customVideoDecoders.find((x) => x.supports(codec, decoderConfig));
4992
5064
  if (MatchingCustomDecoder) {
4993
5065
  this.customDecoder = new MatchingCustomDecoder();
@@ -5001,51 +5073,42 @@ var Mediabunny = (() => {
5001
5073
  };
5002
5074
  void this.customDecoderCallSerializer.call(() => this.customDecoder.init());
5003
5075
  } else {
5004
- const sampleHandler = (sample) => {
5005
- if (isSafari()) {
5006
- if (this.sampleQueue.length > 0 && sample.timestamp >= last(this.sampleQueue).timestamp) {
5007
- for (const sample2 of this.sampleQueue) {
5008
- this.finalizeAndEmitSample(sample2);
5009
- }
5010
- this.sampleQueue.length = 0;
5011
- }
5012
- insertSorted(this.sampleQueue, sample, (x) => x.timestamp);
5076
+ const colorHandler = (frame) => {
5077
+ if (this.alphaQueue.length > 0) {
5078
+ const alphaFrame = this.alphaQueue.shift();
5079
+ assert(alphaFrame !== void 0);
5080
+ this.mergeAlpha(frame, alphaFrame);
5013
5081
  } else {
5014
- const timestamp = this.inputTimestamps.shift();
5015
- assert(timestamp !== void 0);
5016
- sample.setTimestamp(timestamp);
5017
- this.finalizeAndEmitSample(sample);
5082
+ this.colorQueue.push(frame);
5018
5083
  }
5019
5084
  };
5020
5085
  this.decoder = new VideoDecoder({
5021
- output: (frame) => sampleHandler(new VideoSample(frame)),
5086
+ output: (frame) => {
5087
+ try {
5088
+ colorHandler(frame);
5089
+ } catch (error) {
5090
+ this.onError(error);
5091
+ }
5092
+ },
5022
5093
  error: onError
5023
5094
  });
5024
5095
  this.decoder.configure(decoderConfig);
5025
5096
  }
5026
5097
  }
5027
- finalizeAndEmitSample(sample) {
5028
- sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution);
5029
- sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution);
5030
- sample.setRotation(this.rotation);
5031
- this.onSample(sample);
5032
- }
5033
5098
  getDecodeQueueSize() {
5034
5099
  if (this.customDecoder) {
5035
5100
  return this.customDecoderQueueSize;
5036
5101
  } else {
5037
5102
  assert(this.decoder);
5038
- return this.decoder.decodeQueueSize;
5103
+ return Math.max(
5104
+ this.decoder.decodeQueueSize,
5105
+ this.alphaDecoder?.decodeQueueSize ?? 0
5106
+ );
5039
5107
  }
5040
5108
  }
5041
5109
  decode(packet) {
5042
5110
  if (this.codec === "hevc" && this.currentPacketIndex > 0 && !this.raslSkipped) {
5043
- const nalUnits = extractHevcNalUnits(packet.data, this.decoderConfig);
5044
- const hasRaslPicture = nalUnits.some((x) => {
5045
- const type = extractNalUnitTypeForHevc(x);
5046
- return type === 8 /* RASL_N */ || type === 9 /* RASL_R */;
5047
- });
5048
- if (hasRaslPicture) {
5111
+ if (this.hasHevcRaslPicture(packet.data)) {
5049
5112
  return;
5050
5113
  }
5051
5114
  this.raslSkipped = true;
@@ -5060,14 +5123,156 @@ var Mediabunny = (() => {
5060
5123
  insertSorted(this.inputTimestamps, packet.timestamp, (x) => x);
5061
5124
  }
5062
5125
  this.decoder.decode(packet.toEncodedVideoChunk());
5126
+ this.decodeAlphaData(packet);
5063
5127
  }
5064
5128
  }
5129
+ decodeAlphaData(packet) {
5130
+ if (!packet.sideData.alpha || this.mergerCreationFailed) {
5131
+ this.pushNullAlphaFrame();
5132
+ return;
5133
+ }
5134
+ if (!this.merger) {
5135
+ try {
5136
+ this.merger = new ColorAlphaMerger();
5137
+ } catch (error) {
5138
+ console.error("Due to an error, only color data will be decoded.", error);
5139
+ this.mergerCreationFailed = true;
5140
+ this.decodeAlphaData(packet);
5141
+ return;
5142
+ }
5143
+ }
5144
+ if (!this.alphaDecoder) {
5145
+ const alphaHandler = (frame) => {
5146
+ this.alphaDecoderQueueSize--;
5147
+ if (this.colorQueue.length > 0) {
5148
+ const colorFrame = this.colorQueue.shift();
5149
+ assert(colorFrame !== void 0);
5150
+ this.mergeAlpha(colorFrame, frame);
5151
+ } else {
5152
+ this.alphaQueue.push(frame);
5153
+ }
5154
+ this.decodedAlphaChunkCount++;
5155
+ while (this.nullAlphaFrameQueue.length > 0 && this.nullAlphaFrameQueue[0] === this.decodedAlphaChunkCount) {
5156
+ this.nullAlphaFrameQueue.shift();
5157
+ if (this.colorQueue.length > 0) {
5158
+ const colorFrame = this.colorQueue.shift();
5159
+ assert(colorFrame !== void 0);
5160
+ this.mergeAlpha(colorFrame, null);
5161
+ } else {
5162
+ this.alphaQueue.push(null);
5163
+ }
5164
+ }
5165
+ };
5166
+ this.alphaDecoder = new VideoDecoder({
5167
+ output: (frame) => {
5168
+ try {
5169
+ alphaHandler(frame);
5170
+ } catch (error) {
5171
+ this.onError(error);
5172
+ }
5173
+ },
5174
+ error: this.onError
5175
+ });
5176
+ this.alphaDecoder.configure(this.decoderConfig);
5177
+ }
5178
+ const type = determineVideoPacketType(this.codec, this.decoderConfig, packet.sideData.alpha);
5179
+ if (!this.alphaHadKeyframe) {
5180
+ this.alphaHadKeyframe = type === "key";
5181
+ }
5182
+ if (this.alphaHadKeyframe) {
5183
+ if (this.codec === "hevc" && this.currentAlphaPacketIndex > 0 && !this.alphaRaslSkipped) {
5184
+ if (this.hasHevcRaslPicture(packet.sideData.alpha)) {
5185
+ this.pushNullAlphaFrame();
5186
+ return;
5187
+ }
5188
+ this.alphaRaslSkipped = true;
5189
+ }
5190
+ this.currentAlphaPacketIndex++;
5191
+ this.alphaDecoder.decode(packet.alphaToEncodedVideoChunk(type ?? packet.type));
5192
+ this.alphaDecoderQueueSize++;
5193
+ } else {
5194
+ this.pushNullAlphaFrame();
5195
+ }
5196
+ }
5197
+ pushNullAlphaFrame() {
5198
+ if (this.alphaDecoderQueueSize === 0) {
5199
+ this.alphaQueue.push(null);
5200
+ } else {
5201
+ this.nullAlphaFrameQueue.push(this.decodedAlphaChunkCount + this.alphaDecoderQueueSize);
5202
+ }
5203
+ }
5204
+ /**
5205
+ * If we're using HEVC, we need to make sure to skip any RASL slices that follow a non-IDR key frame such as
5206
+ * CRA_NUT. This is because RASL slices cannot be decoded without data before the CRA_NUT. Browsers behave
5207
+ * differently here: Chromium drops the packets, Safari throws a decoder error. Either way, it's not good
5208
+ * and causes bugs upstream. So, let's take the dropping into our own hands.
5209
+ */
5210
+ hasHevcRaslPicture(packetData) {
5211
+ const nalUnits = extractHevcNalUnits(packetData, this.decoderConfig);
5212
+ return nalUnits.some((x) => {
5213
+ const type = extractNalUnitTypeForHevc(x);
5214
+ return type === 8 /* RASL_N */ || type === 9 /* RASL_R */;
5215
+ });
5216
+ }
5217
+ /** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */
5218
+ sampleHandler(sample) {
5219
+ if (isSafari()) {
5220
+ if (this.sampleQueue.length > 0 && sample.timestamp >= last(this.sampleQueue).timestamp) {
5221
+ for (const sample2 of this.sampleQueue) {
5222
+ this.finalizeAndEmitSample(sample2);
5223
+ }
5224
+ this.sampleQueue.length = 0;
5225
+ }
5226
+ insertSorted(this.sampleQueue, sample, (x) => x.timestamp);
5227
+ } else {
5228
+ const timestamp = this.inputTimestamps.shift();
5229
+ assert(timestamp !== void 0);
5230
+ sample.setTimestamp(timestamp);
5231
+ this.finalizeAndEmitSample(sample);
5232
+ }
5233
+ }
5234
+ finalizeAndEmitSample(sample) {
5235
+ sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution);
5236
+ sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution);
5237
+ sample.setRotation(this.rotation);
5238
+ this.onSample(sample);
5239
+ }
5240
+ mergeAlpha(color, alpha) {
5241
+ if (!alpha) {
5242
+ const finalSample2 = new VideoSample(color);
5243
+ this.sampleHandler(finalSample2);
5244
+ return;
5245
+ }
5246
+ assert(this.merger);
5247
+ this.merger.update(color, alpha);
5248
+ color.close();
5249
+ alpha.close();
5250
+ const finalFrame = new VideoFrame(this.merger.canvas, {
5251
+ timestamp: color.timestamp,
5252
+ duration: color.duration ?? void 0
5253
+ });
5254
+ const finalSample = new VideoSample(finalFrame);
5255
+ this.sampleHandler(finalSample);
5256
+ }
5065
5257
  async flush() {
5066
5258
  if (this.customDecoder) {
5067
5259
  await this.customDecoderCallSerializer.call(() => this.customDecoder.flush());
5068
5260
  } else {
5069
5261
  assert(this.decoder);
5070
- await this.decoder.flush();
5262
+ await Promise.all([
5263
+ this.decoder.flush(),
5264
+ this.alphaDecoder?.flush()
5265
+ ]);
5266
+ this.colorQueue.forEach((x) => x.close());
5267
+ this.colorQueue.length = 0;
5268
+ this.alphaQueue.forEach((x) => x?.close());
5269
+ this.alphaQueue.length = 0;
5270
+ this.alphaHadKeyframe = false;
5271
+ this.decodedAlphaChunkCount = 0;
5272
+ this.alphaDecoderQueueSize = 0;
5273
+ this.nullAlphaFrameQueue.length = 0;
5274
+ this.currentAlphaPacketIndex = 0;
5275
+ this.alphaRaslSkipped = false;
5071
5276
  }
5072
5277
  if (isSafari()) {
5073
5278
  for (const sample of this.sampleQueue) {
@@ -5084,6 +5289,12 @@ var Mediabunny = (() => {
5084
5289
  } else {
5085
5290
  assert(this.decoder);
5086
5291
  this.decoder.close();
5292
+ this.alphaDecoder?.close();
5293
+ this.colorQueue.forEach((x) => x.close());
5294
+ this.colorQueue.length = 0;
5295
+ this.alphaQueue.forEach((x) => x?.close());
5296
+ this.alphaQueue.length = 0;
5297
+ this.merger?.close();
5087
5298
  }
5088
5299
  for (const sample of this.sampleQueue) {
5089
5300
  sample.close();
@@ -5091,6 +5302,127 @@ var Mediabunny = (() => {
5091
5302
  this.sampleQueue.length = 0;
5092
5303
  }
5093
5304
  };
5305
+ var ColorAlphaMerger = class {
5306
+ constructor() {
5307
+ if (typeof OffscreenCanvas !== "undefined") {
5308
+ this.canvas = new OffscreenCanvas(300, 150);
5309
+ } else {
5310
+ this.canvas = document.createElement("canvas");
5311
+ }
5312
+ const gl = this.canvas.getContext("webgl2", {
5313
+ premultipliedAlpha: false
5314
+ });
5315
+ if (!gl) {
5316
+ throw new Error("Couldn't acquire WebGL 2 context.");
5317
+ }
5318
+ this.gl = gl;
5319
+ this.program = this.createProgram();
5320
+ this.vao = this.createVAO();
5321
+ this.colorTexture = this.createTexture();
5322
+ this.alphaTexture = this.createTexture();
5323
+ this.gl.useProgram(this.program);
5324
+ this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_colorTexture"), 0);
5325
+ this.gl.uniform1i(this.gl.getUniformLocation(this.program, "u_alphaTexture"), 1);
5326
+ }
5327
+ createProgram() {
5328
+ const vertexShader = this.createShader(this.gl.VERTEX_SHADER, `#version 300 es
5329
+ in vec2 a_position;
5330
+ in vec2 a_texCoord;
5331
+ out vec2 v_texCoord;
5332
+
5333
+ void main() {
5334
+ gl_Position = vec4(a_position, 0.0, 1.0);
5335
+ v_texCoord = a_texCoord;
5336
+ }
5337
+ `);
5338
+ const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `#version 300 es
5339
+ precision highp float;
5340
+
5341
+ uniform sampler2D u_colorTexture;
5342
+ uniform sampler2D u_alphaTexture;
5343
+ in vec2 v_texCoord;
5344
+ out vec4 fragColor;
5345
+
5346
+ void main() {
5347
+ vec3 color = texture(u_colorTexture, v_texCoord).rgb;
5348
+ float alpha = texture(u_alphaTexture, v_texCoord).r;
5349
+ fragColor = vec4(color, alpha);
5350
+ }
5351
+ `);
5352
+ const program = this.gl.createProgram();
5353
+ this.gl.attachShader(program, vertexShader);
5354
+ this.gl.attachShader(program, fragmentShader);
5355
+ this.gl.linkProgram(program);
5356
+ return program;
5357
+ }
5358
+ createShader(type, source) {
5359
+ const shader = this.gl.createShader(type);
5360
+ this.gl.shaderSource(shader, source);
5361
+ this.gl.compileShader(shader);
5362
+ return shader;
5363
+ }
5364
+ createVAO() {
5365
+ const vao = this.gl.createVertexArray();
5366
+ this.gl.bindVertexArray(vao);
5367
+ const vertices = new Float32Array([
5368
+ -1,
5369
+ -1,
5370
+ 0,
5371
+ 1,
5372
+ 1,
5373
+ -1,
5374
+ 1,
5375
+ 1,
5376
+ -1,
5377
+ 1,
5378
+ 0,
5379
+ 0,
5380
+ 1,
5381
+ 1,
5382
+ 1,
5383
+ 0
5384
+ ]);
5385
+ const buffer = this.gl.createBuffer();
5386
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
5387
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
5388
+ const positionLocation = this.gl.getAttribLocation(this.program, "a_position");
5389
+ const texCoordLocation = this.gl.getAttribLocation(this.program, "a_texCoord");
5390
+ this.gl.enableVertexAttribArray(positionLocation);
5391
+ this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 16, 0);
5392
+ this.gl.enableVertexAttribArray(texCoordLocation);
5393
+ this.gl.vertexAttribPointer(texCoordLocation, 2, this.gl.FLOAT, false, 16, 8);
5394
+ return vao;
5395
+ }
5396
+ createTexture() {
5397
+ const texture = this.gl.createTexture();
5398
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
5399
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
5400
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
5401
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
5402
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
5403
+ return texture;
5404
+ }
5405
+ update(color, alpha) {
5406
+ if (color.displayWidth !== this.canvas.width || color.displayHeight !== this.canvas.height) {
5407
+ this.canvas.width = color.displayWidth;
5408
+ this.canvas.height = color.displayHeight;
5409
+ }
5410
+ this.gl.activeTexture(this.gl.TEXTURE0);
5411
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.colorTexture);
5412
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, color);
5413
+ this.gl.activeTexture(this.gl.TEXTURE1);
5414
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.alphaTexture);
5415
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, alpha);
5416
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
5417
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
5418
+ this.gl.bindVertexArray(this.vao);
5419
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
5420
+ }
5421
+ close() {
5422
+ this.gl.getExtension("WEBGL_lose_context")?.loseContext();
5423
+ this.gl = null;
5424
+ }
5425
+ };
5094
5426
  var VideoSampleSink = class extends BaseMediaSampleSink {
5095
5427
  /** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
5096
5428
  constructor(videoTrack) {
@@ -5165,6 +5497,9 @@ var Mediabunny = (() => {
5165
5497
  if (options && typeof options !== "object") {
5166
5498
  throw new TypeError("options must be an object.");
5167
5499
  }
5500
+ if (options.alpha !== void 0 && typeof options.alpha !== "boolean") {
5501
+ throw new TypeError("options.alpha, when provided, must be a boolean.");
5502
+ }
5168
5503
  if (options.width !== void 0 && (!Number.isInteger(options.width) || options.width <= 0)) {
5169
5504
  throw new TypeError("options.width, when defined, must be a positive integer.");
5170
5505
  }
@@ -5207,6 +5542,7 @@ var Mediabunny = (() => {
5207
5542
  height = options.height;
5208
5543
  }
5209
5544
  this._videoTrack = videoTrack;
5545
+ this._alpha = options.alpha ?? false;
5210
5546
  this._width = width;
5211
5547
  this._height = height;
5212
5548
  this._rotation = rotation;
@@ -5236,13 +5572,13 @@ var Mediabunny = (() => {
5236
5572
  this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length;
5237
5573
  }
5238
5574
  const context = canvas.getContext("2d", {
5239
- alpha: isFirefox()
5575
+ alpha: this._alpha || isFirefox()
5240
5576
  // Firefox has VideoFrame glitches with opaque canvases
5241
5577
  });
5242
5578
  assert(context);
5243
5579
  context.resetTransform();
5244
5580
  if (!canvasIsNew) {
5245
- if (isFirefox()) {
5581
+ if (!this._alpha && isFirefox()) {
5246
5582
  context.fillStyle = "black";
5247
5583
  context.fillRect(0, 0, this._width, this._height);
5248
5584
  } else {
@@ -5340,7 +5676,13 @@ var Mediabunny = (() => {
5340
5676
  void this.customDecoderCallSerializer.call(() => this.customDecoder.init());
5341
5677
  } else {
5342
5678
  this.decoder = new AudioDecoder({
5343
- output: (data) => sampleHandler(new AudioSample(data)),
5679
+ output: (data) => {
5680
+ try {
5681
+ sampleHandler(new AudioSample(data));
5682
+ } catch (error) {
5683
+ this.onError(error);
5684
+ }
5685
+ },
5344
5686
  error: onError
5345
5687
  });
5346
5688
  this.decoder.configure(decoderConfig);
@@ -5818,6 +6160,10 @@ var Mediabunny = (() => {
5818
6160
  const colorSpace = await this._backing.getColorSpace();
5819
6161
  return colorSpace.primaries === "bt2020" || colorSpace.primaries === "smpte432" || colorSpace.transfer === "pg" || colorSpace.transfer === "hlg" || colorSpace.matrix === "bt2020-ncl";
5820
6162
  }
6163
+ /** Checks if this track may contain transparent samples with alpha data. */
6164
+ canBeTransparent() {
6165
+ return this._backing.canBeTransparent();
6166
+ }
5821
6167
  /**
5822
6168
  * Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#video-decoder-config) for decoding the
5823
6169
  * track's packets using a [`VideoDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder). Returns
@@ -5861,7 +6207,9 @@ var Mediabunny = (() => {
5861
6207
  if (this.codec === null) {
5862
6208
  return null;
5863
6209
  }
5864
- return determineVideoPacketType(this, packet);
6210
+ const decoderConfig = await this.getDecoderConfig();
6211
+ assert(decoderConfig);
6212
+ return determineVideoPacketType(this.codec, decoderConfig, packet.data);
5865
6213
  }
5866
6214
  };
5867
6215
  var InputAudioTrack = class extends InputTrack {
@@ -8441,6 +8789,9 @@ var Mediabunny = (() => {
8441
8789
  fullRange: this.internalTrack.info.colorSpace?.fullRange
8442
8790
  };
8443
8791
  }
8792
+ async canBeTransparent() {
8793
+ return false;
8794
+ }
8444
8795
  async getDecoderConfig() {
8445
8796
  if (!this.internalTrack.info.codec) {
8446
8797
  return null;
@@ -9100,6 +9451,7 @@ var Mediabunny = (() => {
9100
9451
  this.currentTrack = null;
9101
9452
  this.currentCluster = null;
9102
9453
  this.currentBlock = null;
9454
+ this.currentBlockAdditional = null;
9103
9455
  this.currentCueTime = null;
9104
9456
  this.currentDecodingInstruction = null;
9105
9457
  this.currentTagTargetIsMovie = true;
@@ -9531,7 +9883,8 @@ var Mediabunny = (() => {
9531
9883
  referencedTimestamps: originalBlock.referencedTimestamps,
9532
9884
  data: frameData,
9533
9885
  lacing: 0 /* None */,
9534
- decoded: true
9886
+ decoded: true,
9887
+ mainAdditional: originalBlock.mainAdditional
9535
9888
  });
9536
9889
  }
9537
9890
  blockIndex += frameCount;
@@ -9761,7 +10114,8 @@ var Mediabunny = (() => {
9761
10114
  rotation: 0,
9762
10115
  codec: null,
9763
10116
  codecDescription: null,
9764
- colorSpace: null
10117
+ colorSpace: null,
10118
+ alphaMode: false
9765
10119
  };
9766
10120
  } else if (type === 2) {
9767
10121
  this.currentTrack.info = {
@@ -9870,6 +10224,13 @@ var Mediabunny = (() => {
9870
10224
  }
9871
10225
  ;
9872
10226
  break;
10227
+ case 21440 /* AlphaMode */:
10228
+ {
10229
+ if (this.currentTrack?.info?.type !== "video") break;
10230
+ this.currentTrack.info.alphaMode = readUnsignedInt(slice, size) === 1;
10231
+ }
10232
+ ;
10233
+ break;
9873
10234
  case 21936 /* Colour */:
9874
10235
  {
9875
10236
  if (this.currentTrack?.info?.type !== "video") break;
@@ -10032,7 +10393,8 @@ var Mediabunny = (() => {
10032
10393
  referencedTimestamps: [],
10033
10394
  data: blockData,
10034
10395
  lacing,
10035
- decoded: !hasDecodingInstructions
10396
+ decoded: !hasDecodingInstructions,
10397
+ mainAdditional: null
10036
10398
  });
10037
10399
  }
10038
10400
  ;
@@ -10071,12 +10433,48 @@ var Mediabunny = (() => {
10071
10433
  referencedTimestamps: [],
10072
10434
  data: blockData,
10073
10435
  lacing,
10074
- decoded: !hasDecodingInstructions
10436
+ decoded: !hasDecodingInstructions,
10437
+ mainAdditional: null
10075
10438
  };
10076
10439
  trackData.blocks.push(this.currentBlock);
10077
10440
  }
10078
10441
  ;
10079
10442
  break;
10443
+ case 30113 /* BlockAdditions */:
10444
+ {
10445
+ this.readContiguousElements(slice.slice(dataStartPos, size));
10446
+ }
10447
+ ;
10448
+ break;
10449
+ case 166 /* BlockMore */:
10450
+ {
10451
+ if (!this.currentBlock) break;
10452
+ this.currentBlockAdditional = {
10453
+ addId: 1,
10454
+ data: null
10455
+ };
10456
+ this.readContiguousElements(slice.slice(dataStartPos, size));
10457
+ if (this.currentBlockAdditional.data && this.currentBlockAdditional.addId === 1) {
10458
+ this.currentBlock.mainAdditional = this.currentBlockAdditional.data;
10459
+ }
10460
+ this.currentBlockAdditional = null;
10461
+ }
10462
+ ;
10463
+ break;
10464
+ case 165 /* BlockAdditional */:
10465
+ {
10466
+ if (!this.currentBlockAdditional) break;
10467
+ this.currentBlockAdditional.data = readBytes(slice, size);
10468
+ }
10469
+ ;
10470
+ break;
10471
+ case 238 /* BlockAddID */:
10472
+ {
10473
+ if (!this.currentBlockAdditional) break;
10474
+ this.currentBlockAdditional.addId = readUnsignedInt(slice, size);
10475
+ }
10476
+ ;
10477
+ break;
10080
10478
  case 155 /* BlockDuration */:
10081
10479
  {
10082
10480
  if (!this.currentBlock) break;
@@ -10654,13 +11052,19 @@ var Mediabunny = (() => {
10654
11052
  const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data;
10655
11053
  const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor;
10656
11054
  const duration = block.duration / this.internalTrack.segment.timestampFactor;
11055
+ const sideData = {};
11056
+ if (block.mainAdditional && this.internalTrack.info?.type === "video" && this.internalTrack.info.alphaMode) {
11057
+ sideData.alpha = options.metadataOnly ? PLACEHOLDER_DATA : block.mainAdditional;
11058
+ sideData.alphaByteLength = block.mainAdditional.byteLength;
11059
+ }
10657
11060
  const packet = new EncodedPacket(
10658
11061
  data,
10659
11062
  block.isKeyFrame ? "key" : "delta",
10660
11063
  timestamp,
10661
11064
  duration,
10662
11065
  cluster.dataStartPos + blockIndex,
10663
- block.data.byteLength
11066
+ block.data.byteLength,
11067
+ sideData
10664
11068
  );
10665
11069
  this.packetToClusterLocation.set(packet, { cluster, blockIndex });
10666
11070
  return packet;
@@ -10879,6 +11283,9 @@ var Mediabunny = (() => {
10879
11283
  fullRange: this.internalTrack.info.colorSpace?.fullRange
10880
11284
  };
10881
11285
  }
11286
+ async canBeTransparent() {
11287
+ return this.internalTrack.info.alphaMode;
11288
+ }
10882
11289
  async getDecoderConfig() {
10883
11290
  if (!this.internalTrack.info.codec) {
10884
11291
  return null;
@@ -13345,8 +13752,11 @@ var Mediabunny = (() => {
13345
13752
  break;
13346
13753
  }
13347
13754
  } else if (chunkId === "ds64") {
13348
- const riffChunkSize = readU64(slice2, littleEndian);
13349
- dataChunkSize = readU64(slice2, littleEndian);
13755
+ let ds64Slice = this.reader.requestSlice(startPos, chunkSize);
13756
+ if (ds64Slice instanceof Promise) ds64Slice = await ds64Slice;
13757
+ if (!ds64Slice) break;
13758
+ const riffChunkSize = readU64(ds64Slice, littleEndian);
13759
+ dataChunkSize = readU64(ds64Slice, littleEndian);
13350
13760
  totalFileSize = Math.min(riffChunkSize + 8, this.reader.fileSize ?? Infinity);
13351
13761
  } else if (chunkId === "LIST") {
13352
13762
  await this.parseListChunk(startPos, chunkSize, littleEndian);
@@ -14840,7 +15250,7 @@ var Mediabunny = (() => {
14840
15250
 
14841
15251
  // src/source.ts
14842
15252
  var nodeAlias = __toESM(require_node(), 1);
14843
- var node = nodeAlias;
15253
+ var node = typeof nodeAlias !== "undefined" ? nodeAlias : void 0;
14844
15254
  var Source = class {
14845
15255
  constructor() {
14846
15256
  /** @internal */
@@ -15283,6 +15693,7 @@ var Mediabunny = (() => {
15283
15693
  let data = this._options.read(worker.currentPos, originalTargetPos);
15284
15694
  if (data instanceof Promise) data = await data;
15285
15695
  if (data instanceof Uint8Array) {
15696
+ data = toUint8Array(data);
15286
15697
  if (data.length !== originalTargetPos - worker.currentPos) {
15287
15698
  throw new Error(
15288
15699
  `options.read returned a Uint8Array with unexpected length: Requested ${originalTargetPos - worker.currentPos} bytes, but got ${data.length}.`
@@ -15305,8 +15716,9 @@ var Mediabunny = (() => {
15305
15716
  if (!(value instanceof Uint8Array)) {
15306
15717
  throw new TypeError("ReadableStream returned by options.read must yield Uint8Array chunks.");
15307
15718
  }
15308
- this.onread?.(worker.currentPos, worker.currentPos + value.length);
15309
- this._orchestrator.supplyWorkerData(worker, value);
15719
+ const data2 = toUint8Array(value);
15720
+ this.onread?.(worker.currentPos, worker.currentPos + data2.length);
15721
+ this._orchestrator.supplyWorkerData(worker, data2);
15310
15722
  }
15311
15723
  } else {
15312
15724
  throw new TypeError("options.read must return or resolve to a Uint8Array or a ReadableStream.");
@@ -15850,7 +16262,7 @@ var Mediabunny = (() => {
15850
16262
  };
15851
16263
 
15852
16264
  // src/input.ts
15853
- Symbol.dispose ??= Symbol("Symbol.dispose");
16265
+ polyfillSymbolDispose();
15854
16266
  var Input = class {
15855
16267
  /**
15856
16268
  * Creates a new input file from the specified options. No reading operations will be performed until methods are
@@ -16101,53 +16513,73 @@ var Mediabunny = (() => {
16101
16513
  );
16102
16514
  }
16103
16515
  };
16516
+ var checkIsInRange = (slice, bytesToRead) => {
16517
+ if (slice.filePos < slice.start || slice.filePos + bytesToRead > slice.end) {
16518
+ throw new RangeError(
16519
+ `Tried reading [${slice.filePos}, ${slice.filePos + bytesToRead}), but slice is [${slice.start}, ${slice.end}).`
16520
+ );
16521
+ }
16522
+ };
16104
16523
  var readBytes = (slice, length) => {
16524
+ checkIsInRange(slice, length);
16105
16525
  const bytes2 = slice.bytes.subarray(slice.bufferPos, slice.bufferPos + length);
16106
16526
  slice.bufferPos += length;
16107
16527
  return bytes2;
16108
16528
  };
16109
- var readU8 = (slice) => slice.view.getUint8(slice.bufferPos++);
16529
+ var readU8 = (slice) => {
16530
+ checkIsInRange(slice, 1);
16531
+ return slice.view.getUint8(slice.bufferPos++);
16532
+ };
16110
16533
  var readU16 = (slice, littleEndian) => {
16534
+ checkIsInRange(slice, 2);
16111
16535
  const value = slice.view.getUint16(slice.bufferPos, littleEndian);
16112
16536
  slice.bufferPos += 2;
16113
16537
  return value;
16114
16538
  };
16115
16539
  var readU16Be = (slice) => {
16540
+ checkIsInRange(slice, 2);
16116
16541
  const value = slice.view.getUint16(slice.bufferPos, false);
16117
16542
  slice.bufferPos += 2;
16118
16543
  return value;
16119
16544
  };
16120
16545
  var readU24Be = (slice) => {
16546
+ checkIsInRange(slice, 3);
16121
16547
  const value = getUint24(slice.view, slice.bufferPos, false);
16122
16548
  slice.bufferPos += 3;
16123
16549
  return value;
16124
16550
  };
16125
16551
  var readI16Be = (slice) => {
16552
+ checkIsInRange(slice, 2);
16126
16553
  const value = slice.view.getInt16(slice.bufferPos, false);
16127
16554
  slice.bufferPos += 2;
16128
16555
  return value;
16129
16556
  };
16130
16557
  var readU32 = (slice, littleEndian) => {
16558
+ checkIsInRange(slice, 4);
16131
16559
  const value = slice.view.getUint32(slice.bufferPos, littleEndian);
16132
16560
  slice.bufferPos += 4;
16133
16561
  return value;
16134
16562
  };
16135
16563
  var readU32Be = (slice) => {
16564
+ checkIsInRange(slice, 4);
16136
16565
  const value = slice.view.getUint32(slice.bufferPos, false);
16137
16566
  slice.bufferPos += 4;
16138
16567
  return value;
16139
16568
  };
16140
16569
  var readU32Le = (slice) => {
16570
+ checkIsInRange(slice, 4);
16141
16571
  const value = slice.view.getUint32(slice.bufferPos, true);
16142
16572
  slice.bufferPos += 4;
16143
16573
  return value;
16144
16574
  };
16145
16575
  var readI32Be = (slice) => {
16576
+ checkIsInRange(slice, 4);
16146
16577
  const value = slice.view.getInt32(slice.bufferPos, false);
16147
16578
  slice.bufferPos += 4;
16148
16579
  return value;
16149
16580
  };
16150
16581
  var readI32Le = (slice) => {
16582
+ checkIsInRange(slice, 4);
16151
16583
  const value = slice.view.getInt32(slice.bufferPos, true);
16152
16584
  slice.bufferPos += 4;
16153
16585
  return value;
@@ -16180,19 +16612,19 @@ var Mediabunny = (() => {
16180
16612
  return high * 4294967296 + low;
16181
16613
  };
16182
16614
  var readF32Be = (slice) => {
16615
+ checkIsInRange(slice, 4);
16183
16616
  const value = slice.view.getFloat32(slice.bufferPos, false);
16184
16617
  slice.bufferPos += 4;
16185
16618
  return value;
16186
16619
  };
16187
16620
  var readF64Be = (slice) => {
16621
+ checkIsInRange(slice, 8);
16188
16622
  const value = slice.view.getFloat64(slice.bufferPos, false);
16189
16623
  slice.bufferPos += 8;
16190
16624
  return value;
16191
16625
  };
16192
16626
  var readAscii = (slice, length) => {
16193
- if (slice.bufferPos + length > slice.bytes.length) {
16194
- throw new RangeError("Reading past end of slice.");
16195
- }
16627
+ checkIsInRange(slice, length);
16196
16628
  let str = "";
16197
16629
  for (let i = 0; i < length; i++) {
16198
16630
  str += String.fromCharCode(slice.bytes[slice.bufferPos++]);
@@ -19468,6 +19900,7 @@ var Mediabunny = (() => {
19468
19900
  const videoElement = { id: 224 /* Video */, data: [
19469
19901
  { id: 176 /* PixelWidth */, data: trackData.info.width },
19470
19902
  { id: 186 /* PixelHeight */, data: trackData.info.height },
19903
+ trackData.info.alphaMode ? { id: 21440 /* AlphaMode */, data: 1 } : null,
19471
19904
  colorSpaceIsComplete(colorSpace) ? {
19472
19905
  id: 21936 /* Colour */,
19473
19906
  data: [
@@ -19785,7 +20218,7 @@ var Mediabunny = (() => {
19785
20218
  codecStrings
19786
20219
  });
19787
20220
  }
19788
- getVideoTrackData(track, meta) {
20221
+ getVideoTrackData(track, packet, meta) {
19789
20222
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
19790
20223
  if (existingTrackData) {
19791
20224
  return existingTrackData;
@@ -19801,7 +20234,9 @@ var Mediabunny = (() => {
19801
20234
  info: {
19802
20235
  width: meta.decoderConfig.codedWidth,
19803
20236
  height: meta.decoderConfig.codedHeight,
19804
- decoderConfig: meta.decoderConfig
20237
+ decoderConfig: meta.decoderConfig,
20238
+ alphaMode: !!packet.sideData.alpha
20239
+ // The first packet determines if this track has alpha or not
19805
20240
  },
19806
20241
  chunkQueue: [],
19807
20242
  lastWrittenMsTimestamp: null
@@ -19881,7 +20316,7 @@ var Mediabunny = (() => {
19881
20316
  async addEncodedVideoPacket(track, packet, meta) {
19882
20317
  const release = await this.mutex.acquire();
19883
20318
  try {
19884
- const trackData = this.getVideoTrackData(track, meta);
20319
+ const trackData = this.getVideoTrackData(track, packet, meta);
19885
20320
  const isKeyFrame = packet.type === "key";
19886
20321
  let timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
19887
20322
  let duration = packet.duration;
@@ -19889,7 +20324,8 @@ var Mediabunny = (() => {
19889
20324
  timestamp = roundToMultiple(timestamp, 1 / track.metadata.frameRate);
19890
20325
  duration = roundToMultiple(duration, 1 / track.metadata.frameRate);
19891
20326
  }
19892
- const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type);
20327
+ const additions = trackData.info.alphaMode ? packet.sideData.alpha ?? null : null;
20328
+ const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type, additions);
19893
20329
  if (track.source._codec === "vp9") this.fixVP9ColorSpace(trackData, videoChunk);
19894
20330
  trackData.chunkQueue.push(videoChunk);
19895
20331
  await this.interleaveChunks();
@@ -20063,8 +20499,9 @@ ${cue.notes ?? ""}`;
20063
20499
  } : null,
20064
20500
  chunk.additions ? { id: 30113 /* BlockAdditions */, data: [
20065
20501
  { id: 166 /* BlockMore */, data: [
20066
- { id: 165 /* BlockAdditional */, data: chunk.additions },
20067
- { id: 238 /* BlockAddID */, data: 1 }
20502
+ { id: 238 /* BlockAddID */, data: 1 },
20503
+ // Some players expect BlockAddID to come first
20504
+ { id: 165 /* BlockAdditional */, data: chunk.additions }
20068
20505
  ] }
20069
20506
  ] } : null,
20070
20507
  msDuration > 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null
@@ -21471,6 +21908,9 @@ ${cue.notes ?? ""}`;
21471
21908
  if (!options || typeof options !== "object") {
21472
21909
  throw new TypeError("Encoding options must be an object.");
21473
21910
  }
21911
+ if (options.alpha !== void 0 && !["discard", "keep"].includes(options.alpha)) {
21912
+ throw new TypeError("options.alpha, when provided, must be 'discard' or 'keep'.");
21913
+ }
21474
21914
  if (options.bitrateMode !== void 0 && !["constant", "variable"].includes(options.bitrateMode)) {
21475
21915
  throw new TypeError("bitrateMode, when provided, must be 'constant' or 'variable'.");
21476
21916
  }
@@ -21510,8 +21950,8 @@ ${cue.notes ?? ""}`;
21510
21950
  height: options.height,
21511
21951
  bitrate: resolvedBitrate,
21512
21952
  bitrateMode: options.bitrateMode,
21953
+ alpha: options.alpha ?? "discard",
21513
21954
  framerate: options.framerate,
21514
- // this.source._connectedTrack?.metadata.frameRate,
21515
21955
  latencyMode: options.latencyMode,
21516
21956
  hardwareAcceleration: options.hardwareAcceleration,
21517
21957
  scalabilityMode: options.scalabilityMode,
@@ -21714,7 +22154,9 @@ ${cue.notes ?? ""}`;
21714
22154
  height,
21715
22155
  bitrate,
21716
22156
  framerate: void 0,
21717
- ...restOptions
22157
+ ...restOptions,
22158
+ alpha: "discard"
22159
+ // Since we handle alpha ourselves
21718
22160
  });
21719
22161
  const support = await VideoEncoder.isConfigSupported(encoderConfig);
21720
22162
  return support.supported === true;
@@ -21949,12 +22391,18 @@ ${cue.notes ?? ""}`;
21949
22391
  this.customEncoder = null;
21950
22392
  this.customEncoderCallSerializer = new CallSerializer();
21951
22393
  this.customEncoderQueueSize = 0;
22394
+ // Alpha stuff
22395
+ this.alphaEncoder = null;
22396
+ this.splitter = null;
22397
+ this.splitterCreationFailed = false;
22398
+ this.alphaFrameQueue = [];
21952
22399
  /**
21953
22400
  * Encoders typically throw their errors "out of band", meaning asynchronously in some other execution context.
21954
22401
  * However, we want to surface these errors to the user within the normal control flow, so they don't go uncaught.
21955
22402
  * So, we keep track of the encoder error and throw it as soon as we get the chance.
21956
22403
  */
21957
- this.encoderError = null;
22404
+ this.error = null;
22405
+ this.errorNeedsNewStack = true;
21958
22406
  }
21959
22407
  async add(videoSample, shouldClose, encodeOptions) {
21960
22408
  try {
@@ -22028,7 +22476,7 @@ ${cue.notes ?? ""}`;
22028
22476
  if (this.customEncoder) {
22029
22477
  this.customEncoderQueueSize++;
22030
22478
  const clonedSample = videoSample.clone();
22031
- const promise = this.customEncoderCallSerializer.call(() => this.customEncoder.encode(clonedSample, finalEncodeOptions)).then(() => this.customEncoderQueueSize--).catch((error) => this.encoderError ??= error).finally(() => {
22479
+ const promise = this.customEncoderCallSerializer.call(() => this.customEncoder.encode(clonedSample, finalEncodeOptions)).then(() => this.customEncoderQueueSize--).catch((error) => this.error ??= error).finally(() => {
22032
22480
  clonedSample.close();
22033
22481
  });
22034
22482
  if (this.customEncoderQueueSize >= 4) {
@@ -22037,8 +22485,39 @@ ${cue.notes ?? ""}`;
22037
22485
  } else {
22038
22486
  assert(this.encoder);
22039
22487
  const videoFrame = videoSample.toVideoFrame();
22040
- this.encoder.encode(videoFrame, finalEncodeOptions);
22041
- videoFrame.close();
22488
+ if (!this.alphaEncoder) {
22489
+ this.encoder.encode(videoFrame, finalEncodeOptions);
22490
+ videoFrame.close();
22491
+ } else {
22492
+ const frameDefinitelyHasNoAlpha = !!videoFrame.format && !videoFrame.format.includes("A");
22493
+ if (frameDefinitelyHasNoAlpha || this.splitterCreationFailed) {
22494
+ this.alphaFrameQueue.push(null);
22495
+ this.encoder.encode(videoFrame, finalEncodeOptions);
22496
+ videoFrame.close();
22497
+ } else {
22498
+ const width = videoFrame.displayWidth;
22499
+ const height = videoFrame.displayHeight;
22500
+ if (!this.splitter) {
22501
+ try {
22502
+ this.splitter = new ColorAlphaSplitter(width, height);
22503
+ } catch (error) {
22504
+ console.error("Due to an error, only color data will be encoded.", error);
22505
+ this.splitterCreationFailed = true;
22506
+ this.alphaFrameQueue.push(null);
22507
+ this.encoder.encode(videoFrame, finalEncodeOptions);
22508
+ videoFrame.close();
22509
+ }
22510
+ }
22511
+ if (this.splitter) {
22512
+ const colorFrame = this.splitter.extractColor(videoFrame);
22513
+ const alphaFrame = this.splitter.extractAlpha(videoFrame);
22514
+ this.alphaFrameQueue.push(alphaFrame);
22515
+ this.encoder.encode(colorFrame, finalEncodeOptions);
22516
+ colorFrame.close();
22517
+ videoFrame.close();
22518
+ }
22519
+ }
22520
+ }
22042
22521
  if (shouldClose) {
22043
22522
  videoSample.close();
22044
22523
  }
@@ -22054,9 +22533,6 @@ ${cue.notes ?? ""}`;
22054
22533
  }
22055
22534
  }
22056
22535
  ensureEncoder(videoSample) {
22057
- if (this.encoder) {
22058
- return;
22059
- }
22060
22536
  const encoderError = new Error();
22061
22537
  this.ensureEncoderPromise = (async () => {
22062
22538
  const encoderConfig = buildVideoEncoderConfig({
@@ -22082,13 +22558,20 @@ ${cue.notes ?? ""}`;
22082
22558
  throw new TypeError("The second argument passed to onPacket must be an object or undefined.");
22083
22559
  }
22084
22560
  this.encodingConfig.onEncodedPacket?.(packet, meta);
22085
- void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta);
22561
+ void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta).catch((error) => {
22562
+ this.error ??= error;
22563
+ this.errorNeedsNewStack = false;
22564
+ });
22086
22565
  };
22087
22566
  await this.customEncoder.init();
22088
22567
  } else {
22089
22568
  if (typeof VideoEncoder === "undefined") {
22090
22569
  throw new Error("VideoEncoder is not supported by this browser.");
22091
22570
  }
22571
+ encoderConfig.alpha = "discard";
22572
+ if (this.encodingConfig.alpha === "keep") {
22573
+ encoderConfig.latencyMode = "quality";
22574
+ }
22092
22575
  const hasOddDimension = encoderConfig.width % 2 === 1 || encoderConfig.height % 2 === 1;
22093
22576
  if (hasOddDimension && (this.encodingConfig.codec === "avc" || this.encodingConfig.codec === "hevc")) {
22094
22577
  throw new Error(
@@ -22101,18 +22584,81 @@ ${cue.notes ?? ""}`;
22101
22584
  `This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps, ${encoderConfig.width}x${encoderConfig.height}, hardware acceleration: ${encoderConfig.hardwareAcceleration ?? "no-preference"}) is not supported by this browser. Consider using another codec or changing your video parameters.`
22102
22585
  );
22103
22586
  }
22587
+ const colorChunkQueue = [];
22588
+ const nullAlphaChunkQueue = [];
22589
+ let encodedAlphaChunkCount = 0;
22590
+ let alphaEncoderQueue = 0;
22591
+ const addPacket = (colorChunk, alphaChunk, meta) => {
22592
+ const sideData = {};
22593
+ if (alphaChunk) {
22594
+ const alphaData = new Uint8Array(alphaChunk.byteLength);
22595
+ alphaChunk.copyTo(alphaData);
22596
+ sideData.alpha = alphaData;
22597
+ }
22598
+ const packet = EncodedPacket.fromEncodedChunk(colorChunk, sideData);
22599
+ this.encodingConfig.onEncodedPacket?.(packet, meta);
22600
+ void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta).catch((error) => {
22601
+ this.error ??= error;
22602
+ this.errorNeedsNewStack = false;
22603
+ });
22604
+ };
22104
22605
  this.encoder = new VideoEncoder({
22105
22606
  output: (chunk, meta) => {
22106
- const packet = EncodedPacket.fromEncodedChunk(chunk);
22107
- this.encodingConfig.onEncodedPacket?.(packet, meta);
22108
- void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta);
22607
+ if (!this.alphaEncoder) {
22608
+ addPacket(chunk, null, meta);
22609
+ return;
22610
+ }
22611
+ const alphaFrame = this.alphaFrameQueue.shift();
22612
+ assert(alphaFrame !== void 0);
22613
+ if (alphaFrame) {
22614
+ this.alphaEncoder.encode(alphaFrame, {
22615
+ // Crucial: The alpha frame is forced to be a key frame whenever the color frame
22616
+ // also is. Without this, playback can glitch and even crash in some browsers.
22617
+ // This is the reason why the two encoders are wired in series and not in parallel.
22618
+ keyFrame: chunk.type === "key"
22619
+ });
22620
+ alphaEncoderQueue++;
22621
+ alphaFrame.close();
22622
+ colorChunkQueue.push({ chunk, meta });
22623
+ } else {
22624
+ if (alphaEncoderQueue === 0) {
22625
+ addPacket(chunk, null, meta);
22626
+ } else {
22627
+ nullAlphaChunkQueue.push(encodedAlphaChunkCount + alphaEncoderQueue);
22628
+ colorChunkQueue.push({ chunk, meta });
22629
+ }
22630
+ }
22109
22631
  },
22110
22632
  error: (error) => {
22111
22633
  error.stack = encoderError.stack;
22112
- this.encoderError ??= error;
22634
+ this.error ??= error;
22113
22635
  }
22114
22636
  });
22115
22637
  this.encoder.configure(encoderConfig);
22638
+ if (this.encodingConfig.alpha === "keep") {
22639
+ this.alphaEncoder = new VideoEncoder({
22640
+ // We ignore the alpha chunk's metadata
22641
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
22642
+ output: (chunk, meta) => {
22643
+ alphaEncoderQueue--;
22644
+ const colorChunk = colorChunkQueue.shift();
22645
+ assert(colorChunk !== void 0);
22646
+ addPacket(colorChunk.chunk, chunk, colorChunk.meta);
22647
+ encodedAlphaChunkCount++;
22648
+ while (nullAlphaChunkQueue.length > 0 && nullAlphaChunkQueue[0] === encodedAlphaChunkCount) {
22649
+ nullAlphaChunkQueue.shift();
22650
+ const colorChunk2 = colorChunkQueue.shift();
22651
+ assert(colorChunk2 !== void 0);
22652
+ addPacket(colorChunk2.chunk, null, colorChunk2.meta);
22653
+ }
22654
+ },
22655
+ error: (error) => {
22656
+ error.stack = encoderError.stack;
22657
+ this.error ??= error;
22658
+ }
22659
+ });
22660
+ this.alphaEncoder.configure(encoderConfig);
22661
+ }
22116
22662
  }
22117
22663
  assert(this.source._connectedTrack);
22118
22664
  this.muxer = this.source._connectedTrack.output._muxer;
@@ -22129,10 +22675,16 @@ ${cue.notes ?? ""}`;
22129
22675
  } else if (this.encoder) {
22130
22676
  if (!forceClose) {
22131
22677
  await this.encoder.flush();
22678
+ await this.alphaEncoder?.flush();
22132
22679
  }
22133
22680
  if (this.encoder.state !== "closed") {
22134
22681
  this.encoder.close();
22135
22682
  }
22683
+ if (this.alphaEncoder && this.alphaEncoder.state !== "closed") {
22684
+ this.alphaEncoder.close();
22685
+ }
22686
+ this.alphaFrameQueue.forEach((x) => x?.close());
22687
+ this.splitter?.close();
22136
22688
  }
22137
22689
  if (!forceClose) this.checkForEncoderError();
22138
22690
  }
@@ -22144,12 +22696,235 @@ ${cue.notes ?? ""}`;
22144
22696
  }
22145
22697
  }
22146
22698
  checkForEncoderError() {
22147
- if (this.encoderError) {
22148
- this.encoderError.stack = new Error().stack;
22149
- throw this.encoderError;
22699
+ if (this.error) {
22700
+ if (this.errorNeedsNewStack) {
22701
+ this.error.stack = new Error().stack;
22702
+ }
22703
+ throw this.error;
22150
22704
  }
22151
22705
  }
22152
22706
  };
22707
+ var ColorAlphaSplitter = class {
22708
+ constructor(initialWidth, initialHeight) {
22709
+ this.lastFrame = null;
22710
+ if (typeof OffscreenCanvas !== "undefined") {
22711
+ this.canvas = new OffscreenCanvas(initialWidth, initialHeight);
22712
+ } else {
22713
+ this.canvas = document.createElement("canvas");
22714
+ this.canvas.width = initialWidth;
22715
+ this.canvas.height = initialHeight;
22716
+ }
22717
+ const gl = this.canvas.getContext("webgl2", {
22718
+ alpha: true
22719
+ // Needed due to the YUV thing we do for alpha
22720
+ });
22721
+ if (!gl) {
22722
+ throw new Error("Couldn't acquire WebGL 2 context.");
22723
+ }
22724
+ this.gl = gl;
22725
+ this.colorProgram = this.createColorProgram();
22726
+ this.alphaProgram = this.createAlphaProgram();
22727
+ this.vao = this.createVAO();
22728
+ this.sourceTexture = this.createTexture();
22729
+ this.alphaResolutionLocation = this.gl.getUniformLocation(this.alphaProgram, "u_resolution");
22730
+ this.gl.useProgram(this.colorProgram);
22731
+ this.gl.uniform1i(this.gl.getUniformLocation(this.colorProgram, "u_sourceTexture"), 0);
22732
+ this.gl.useProgram(this.alphaProgram);
22733
+ this.gl.uniform1i(this.gl.getUniformLocation(this.alphaProgram, "u_sourceTexture"), 0);
22734
+ }
22735
+ createVertexShader() {
22736
+ return this.createShader(this.gl.VERTEX_SHADER, `#version 300 es
22737
+ in vec2 a_position;
22738
+ in vec2 a_texCoord;
22739
+ out vec2 v_texCoord;
22740
+
22741
+ void main() {
22742
+ gl_Position = vec4(a_position, 0.0, 1.0);
22743
+ v_texCoord = a_texCoord;
22744
+ }
22745
+ `);
22746
+ }
22747
+ createColorProgram() {
22748
+ const vertexShader = this.createVertexShader();
22749
+ const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `#version 300 es
22750
+ precision highp float;
22751
+
22752
+ uniform sampler2D u_sourceTexture;
22753
+ in vec2 v_texCoord;
22754
+ out vec4 fragColor;
22755
+
22756
+ void main() {
22757
+ vec4 source = texture(u_sourceTexture, v_texCoord);
22758
+ fragColor = vec4(source.rgb, 1.0);
22759
+ }
22760
+ `);
22761
+ const program = this.gl.createProgram();
22762
+ this.gl.attachShader(program, vertexShader);
22763
+ this.gl.attachShader(program, fragmentShader);
22764
+ this.gl.linkProgram(program);
22765
+ return program;
22766
+ }
22767
+ createAlphaProgram() {
22768
+ const vertexShader = this.createVertexShader();
22769
+ const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, `#version 300 es
22770
+ precision highp float;
22771
+
22772
+ uniform sampler2D u_sourceTexture;
22773
+ uniform vec2 u_resolution; // The width and height of the canvas
22774
+ in vec2 v_texCoord;
22775
+ out vec4 fragColor;
22776
+
22777
+ // This function determines the value for a single byte in the YUV stream
22778
+ float getByteValue(float byteOffset) {
22779
+ float width = u_resolution.x;
22780
+ float height = u_resolution.y;
22781
+
22782
+ float yPlaneSize = width * height;
22783
+
22784
+ if (byteOffset < yPlaneSize) {
22785
+ // This byte is in the luma plane. Find the corresponding pixel coordinates to sample from
22786
+ float y = floor(byteOffset / width);
22787
+ float x = mod(byteOffset, width);
22788
+
22789
+ // Add 0.5 to sample the center of the texel
22790
+ vec2 sampleCoord = (vec2(x, y) + 0.5) / u_resolution;
22791
+
22792
+ // The luma value is the alpha from the source texture
22793
+ return texture(u_sourceTexture, sampleCoord).a;
22794
+ } else {
22795
+ // Write a fixed value for chroma and beyond
22796
+ return 128.0 / 255.0;
22797
+ }
22798
+ }
22799
+
22800
+ void main() {
22801
+ // Each fragment writes 4 bytes (R, G, B, A)
22802
+ float pixelIndex = floor(gl_FragCoord.y) * u_resolution.x + floor(gl_FragCoord.x);
22803
+ float baseByteOffset = pixelIndex * 4.0;
22804
+
22805
+ vec4 result;
22806
+ for (int i = 0; i < 4; i++) {
22807
+ float currentByteOffset = baseByteOffset + float(i);
22808
+ result[i] = getByteValue(currentByteOffset);
22809
+ }
22810
+
22811
+ fragColor = result;
22812
+ }
22813
+ `);
22814
+ const program = this.gl.createProgram();
22815
+ this.gl.attachShader(program, vertexShader);
22816
+ this.gl.attachShader(program, fragmentShader);
22817
+ this.gl.linkProgram(program);
22818
+ return program;
22819
+ }
22820
+ createShader(type, source) {
22821
+ const shader = this.gl.createShader(type);
22822
+ this.gl.shaderSource(shader, source);
22823
+ this.gl.compileShader(shader);
22824
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
22825
+ console.error("Shader compile error:", this.gl.getShaderInfoLog(shader));
22826
+ }
22827
+ return shader;
22828
+ }
22829
+ createVAO() {
22830
+ const vao = this.gl.createVertexArray();
22831
+ this.gl.bindVertexArray(vao);
22832
+ const vertices = new Float32Array([
22833
+ -1,
22834
+ -1,
22835
+ 0,
22836
+ 1,
22837
+ 1,
22838
+ -1,
22839
+ 1,
22840
+ 1,
22841
+ -1,
22842
+ 1,
22843
+ 0,
22844
+ 0,
22845
+ 1,
22846
+ 1,
22847
+ 1,
22848
+ 0
22849
+ ]);
22850
+ const buffer = this.gl.createBuffer();
22851
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
22852
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
22853
+ const positionLocation = this.gl.getAttribLocation(this.colorProgram, "a_position");
22854
+ const texCoordLocation = this.gl.getAttribLocation(this.colorProgram, "a_texCoord");
22855
+ this.gl.enableVertexAttribArray(positionLocation);
22856
+ this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 16, 0);
22857
+ this.gl.enableVertexAttribArray(texCoordLocation);
22858
+ this.gl.vertexAttribPointer(texCoordLocation, 2, this.gl.FLOAT, false, 16, 8);
22859
+ return vao;
22860
+ }
22861
+ createTexture() {
22862
+ const texture = this.gl.createTexture();
22863
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
22864
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
22865
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
22866
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
22867
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
22868
+ return texture;
22869
+ }
22870
+ updateTexture(sourceFrame) {
22871
+ if (this.lastFrame === sourceFrame) {
22872
+ return;
22873
+ }
22874
+ if (sourceFrame.displayWidth !== this.canvas.width || sourceFrame.displayHeight !== this.canvas.height) {
22875
+ this.canvas.width = sourceFrame.displayWidth;
22876
+ this.canvas.height = sourceFrame.displayHeight;
22877
+ }
22878
+ this.gl.activeTexture(this.gl.TEXTURE0);
22879
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.sourceTexture);
22880
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceFrame);
22881
+ this.lastFrame = sourceFrame;
22882
+ }
22883
+ extractColor(sourceFrame) {
22884
+ this.updateTexture(sourceFrame);
22885
+ this.gl.useProgram(this.colorProgram);
22886
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
22887
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
22888
+ this.gl.bindVertexArray(this.vao);
22889
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
22890
+ return new VideoFrame(this.canvas, {
22891
+ timestamp: sourceFrame.timestamp,
22892
+ duration: sourceFrame.duration ?? void 0,
22893
+ alpha: "discard"
22894
+ });
22895
+ }
22896
+ extractAlpha(sourceFrame) {
22897
+ this.updateTexture(sourceFrame);
22898
+ this.gl.useProgram(this.alphaProgram);
22899
+ this.gl.uniform2f(this.alphaResolutionLocation, this.canvas.width, this.canvas.height);
22900
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
22901
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
22902
+ this.gl.bindVertexArray(this.vao);
22903
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
22904
+ const { width, height } = this.canvas;
22905
+ const chromaSamples = Math.ceil(width / 2) * Math.ceil(height / 2);
22906
+ const yuvSize = width * height + chromaSamples * 2;
22907
+ const requiredHeight = Math.ceil(yuvSize / (width * 4));
22908
+ let yuv = new Uint8Array(4 * width * requiredHeight);
22909
+ this.gl.readPixels(0, 0, width, requiredHeight, this.gl.RGBA, this.gl.UNSIGNED_BYTE, yuv);
22910
+ yuv = yuv.subarray(0, yuvSize);
22911
+ assert(yuv[width * height] === 128);
22912
+ assert(yuv[yuv.length - 1] === 128);
22913
+ const init = {
22914
+ format: "I420",
22915
+ codedWidth: width,
22916
+ codedHeight: height,
22917
+ timestamp: sourceFrame.timestamp,
22918
+ duration: sourceFrame.duration ?? void 0,
22919
+ transfer: [yuv.buffer]
22920
+ };
22921
+ return new VideoFrame(yuv, init);
22922
+ }
22923
+ close() {
22924
+ this.gl.getExtension("WEBGL_lose_context")?.loseContext();
22925
+ this.gl = null;
22926
+ }
22927
+ };
22153
22928
  var VideoSampleSource = class extends VideoSource {
22154
22929
  /**
22155
22930
  * Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified
@@ -22411,7 +23186,8 @@ ${cue.notes ?? ""}`;
22411
23186
  * However, we want to surface these errors to the user within the normal control flow, so they don't go uncaught.
22412
23187
  * So, we keep track of the encoder error and throw it as soon as we get the chance.
22413
23188
  */
22414
- this.encoderError = null;
23189
+ this.error = null;
23190
+ this.errorNeedsNewStack = true;
22415
23191
  }
22416
23192
  async add(audioSample, shouldClose) {
22417
23193
  try {
@@ -22439,7 +23215,7 @@ ${cue.notes ?? ""}`;
22439
23215
  if (this.customEncoder) {
22440
23216
  this.customEncoderQueueSize++;
22441
23217
  const clonedSample = audioSample.clone();
22442
- const promise = this.customEncoderCallSerializer.call(() => this.customEncoder.encode(clonedSample)).then(() => this.customEncoderQueueSize--).catch((error) => this.encoderError ??= error).finally(() => {
23218
+ const promise = this.customEncoderCallSerializer.call(() => this.customEncoder.encode(clonedSample)).then(() => this.customEncoderQueueSize--).catch((error) => this.error ??= error).finally(() => {
22443
23219
  clonedSample.close();
22444
23220
  });
22445
23221
  if (this.customEncoderQueueSize >= 4) {
@@ -22520,9 +23296,6 @@ ${cue.notes ?? ""}`;
22520
23296
  }
22521
23297
  }
22522
23298
  ensureEncoder(audioSample) {
22523
- if (this.encoderInitialized) {
22524
- return;
22525
- }
22526
23299
  const encoderError = new Error();
22527
23300
  this.ensureEncoderPromise = (async () => {
22528
23301
  const { numberOfChannels, sampleRate } = audioSample;
@@ -22548,7 +23321,10 @@ ${cue.notes ?? ""}`;
22548
23321
  throw new TypeError("The second argument passed to onPacket must be an object or undefined.");
22549
23322
  }
22550
23323
  this.encodingConfig.onEncodedPacket?.(packet, meta);
22551
- void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta);
23324
+ void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta).catch((error) => {
23325
+ this.error ??= error;
23326
+ this.errorNeedsNewStack = false;
23327
+ });
22552
23328
  };
22553
23329
  await this.customEncoder.init();
22554
23330
  } else if (PCM_AUDIO_CODECS.includes(this.encodingConfig.codec)) {
@@ -22567,11 +23343,14 @@ ${cue.notes ?? ""}`;
22567
23343
  output: (chunk, meta) => {
22568
23344
  const packet = EncodedPacket.fromEncodedChunk(chunk);
22569
23345
  this.encodingConfig.onEncodedPacket?.(packet, meta);
22570
- void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta);
23346
+ void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta).catch((error) => {
23347
+ this.error ??= error;
23348
+ this.errorNeedsNewStack = false;
23349
+ });
22571
23350
  },
22572
23351
  error: (error) => {
22573
23352
  error.stack = encoderError.stack;
22574
- this.encoderError ??= error;
23353
+ this.error ??= error;
22575
23354
  }
22576
23355
  });
22577
23356
  this.encoder.configure(encoderConfig);
@@ -22703,9 +23482,11 @@ ${cue.notes ?? ""}`;
22703
23482
  }
22704
23483
  }
22705
23484
  checkForEncoderError() {
22706
- if (this.encoderError) {
22707
- this.encoderError.stack = new Error().stack;
22708
- throw this.encoderError;
23485
+ if (this.error) {
23486
+ if (this.errorNeedsNewStack) {
23487
+ this.error.stack = new Error().stack;
23488
+ }
23489
+ throw this.error;
22709
23490
  }
22710
23491
  }
22711
23492
  };
@@ -23014,9 +23795,15 @@ ${cue.notes ?? ""}`;
23014
23795
  /** Creates a new {@link TextSubtitleSource} where added text chunks are in the specified `codec`. */
23015
23796
  constructor(codec) {
23016
23797
  super(codec);
23798
+ /** @internal */
23799
+ this._error = null;
23017
23800
  this._parser = new SubtitleParser({
23018
23801
  codec,
23019
- output: (cue, metadata) => this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack, cue, metadata)
23802
+ output: (cue, metadata) => {
23803
+ void this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack, cue, metadata).catch((error) => {
23804
+ this._error ??= error;
23805
+ });
23806
+ }
23020
23807
  });
23021
23808
  }
23022
23809
  /**
@@ -23030,10 +23817,23 @@ ${cue.notes ?? ""}`;
23030
23817
  if (typeof text !== "string") {
23031
23818
  throw new TypeError("text must be a string.");
23032
23819
  }
23820
+ this._checkForError();
23033
23821
  this._ensureValidAdd();
23034
23822
  this._parser.parse(text);
23035
23823
  return this._connectedTrack.output._muxer.mutex.currentPromise;
23036
23824
  }
23825
+ /** @internal */
23826
+ _checkForError() {
23827
+ if (this._error) {
23828
+ throw this._error;
23829
+ }
23830
+ }
23831
+ /** @internal */
23832
+ async _flushAndClose(forceClose) {
23833
+ if (!forceClose) {
23834
+ this._checkForError();
23835
+ }
23836
+ }
23037
23837
  };
23038
23838
 
23039
23839
  // src/output.ts