mediabunny 1.51.0 → 1.52.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 (46) hide show
  1. package/dist/bundles/mediabunny.cjs +564 -205
  2. package/dist/bundles/mediabunny.min.cjs +15 -15
  3. package/dist/bundles/mediabunny.min.mjs +15 -15
  4. package/dist/bundles/mediabunny.mjs +564 -205
  5. package/dist/bundles/mediabunny.node.cjs +564 -205
  6. package/dist/mediabunny.d.ts +117 -11
  7. package/dist/modules/src/conversion.d.ts +12 -2
  8. package/dist/modules/src/conversion.d.ts.map +1 -1
  9. package/dist/modules/src/conversion.js +44 -20
  10. package/dist/modules/src/encode.d.ts +140 -12
  11. package/dist/modules/src/encode.d.ts.map +1 -1
  12. package/dist/modules/src/encode.js +318 -77
  13. package/dist/modules/src/index.d.ts +1 -1
  14. package/dist/modules/src/index.d.ts.map +1 -1
  15. package/dist/modules/src/index.js +11 -1
  16. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  17. package/dist/modules/src/isobmff/isobmff-boxes.js +4 -2
  18. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +1 -0
  19. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  20. package/dist/modules/src/isobmff/isobmff-muxer.js +3 -0
  21. package/dist/modules/src/media-sink.d.ts.map +1 -1
  22. package/dist/modules/src/media-sink.js +11 -5
  23. package/dist/modules/src/media-source.d.ts.map +1 -1
  24. package/dist/modules/src/media-source.js +287 -113
  25. package/dist/modules/src/misc.d.ts +1 -0
  26. package/dist/modules/src/misc.d.ts.map +1 -1
  27. package/dist/modules/src/misc.js +3 -0
  28. package/dist/modules/src/output-format.d.ts.map +1 -1
  29. package/dist/modules/src/output.d.ts +1 -1
  30. package/dist/modules/src/output.d.ts.map +1 -1
  31. package/dist/modules/src/output.js +2 -0
  32. package/dist/modules/src/source.d.ts.map +1 -1
  33. package/dist/modules/src/source.js +20 -5
  34. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +1 -1
  36. package/src/conversion.ts +58 -26
  37. package/src/encode.ts +524 -93
  38. package/src/index.ts +9 -0
  39. package/src/isobmff/isobmff-boxes.ts +6 -2
  40. package/src/isobmff/isobmff-muxer.ts +5 -0
  41. package/src/media-sink.ts +11 -5
  42. package/src/media-source.ts +251 -136
  43. package/src/misc.ts +4 -0
  44. package/src/output-format.ts +3 -3
  45. package/src/output.ts +4 -1
  46. package/src/source.ts +23 -5
@@ -558,6 +558,9 @@ var Mediabunny = (() => {
558
558
  var clamp = (value, min, max) => {
559
559
  return Math.max(min, Math.min(max, value));
560
560
  };
561
+ var lerp = (from, to, t) => {
562
+ return from + (to - from) * t;
563
+ };
561
564
  var UNDETERMINED_LANGUAGE = "und";
562
565
  var roundIfAlmostInteger = (value) => {
563
566
  const rounded = Math.round(value);
@@ -16837,6 +16840,9 @@ var Mediabunny = (() => {
16837
16840
  }
16838
16841
  /** @internal */
16839
16842
  _dispose() {
16843
+ for (const pendingSlice of this._pendingSlices) {
16844
+ pendingSlice.reject(new InputDisposedError());
16845
+ }
16840
16846
  this._pendingSlices.length = 0;
16841
16847
  this._cache.length = 0;
16842
16848
  void this._reader?.cancel();
@@ -17288,7 +17294,9 @@ var Mediabunny = (() => {
17288
17294
  }
17289
17295
  signalWorkerStoppedRunning(worker) {
17290
17296
  worker.running = false;
17291
- worker.pendingSlices.length = 0;
17297
+ if (!worker.aborted) {
17298
+ worker.pendingSlices.length = 0;
17299
+ }
17292
17300
  }
17293
17301
  /** Called when a worker reaches the end of the underlying data and must be cleaned up. */
17294
17302
  onWorkerFinished(worker) {
@@ -17371,10 +17379,20 @@ var Mediabunny = (() => {
17371
17379
  }
17372
17380
  dispose() {
17373
17381
  for (const worker of this.workers) {
17382
+ for (const slice of worker.pendingSlices) {
17383
+ slice.reject(new InputDisposedError());
17384
+ }
17385
+ worker.pendingSlices.length = 0;
17374
17386
  worker.aborted = true;
17375
17387
  }
17388
+ for (const queuedRead of this.queuedReads) {
17389
+ for (const slice of queuedRead.pendingSlices) {
17390
+ slice.reject(new InputDisposedError());
17391
+ }
17392
+ }
17376
17393
  this.workers.length = 0;
17377
17394
  this.cache.length = 0;
17395
+ this.queuedReads.length = 0;
17378
17396
  this.disposed = true;
17379
17397
  }
17380
17398
  };
@@ -21343,8 +21361,18 @@ var Mediabunny = (() => {
21343
21361
  if (!VIDEO_CODECS.includes(config.codec)) {
21344
21362
  throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`);
21345
21363
  }
21346
- if (!(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
21347
- throw new TypeError("config.bitrate must be a positive integer or a quality.");
21364
+ const bitrate = config.bitrate;
21365
+ if (config.quality === void 0 && bitrate === void 0) {
21366
+ throw new TypeError("config.quality must be provided.");
21367
+ }
21368
+ if (config.quality !== void 0 && bitrate !== void 0) {
21369
+ throw new TypeError("config.quality and config.bitrate cannot both be provided.");
21370
+ }
21371
+ if (config.quality !== void 0 && !(config.quality instanceof Quality)) {
21372
+ throw new TypeError("config.quality, when provided, must be a Quality.");
21373
+ }
21374
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21375
+ throw new TypeError("config.bitrate, when provided, must be a positive integer or a quality.");
21348
21376
  }
21349
21377
  if (config.keyFrameInterval !== void 0 && (!Number.isFinite(config.keyFrameInterval) || config.keyFrameInterval < 0)) {
21350
21378
  throw new TypeError("config.keyFrameInterval, when provided, must be a non-negative number.");
@@ -21411,7 +21439,8 @@ var Mediabunny = (() => {
21411
21439
  if (options.alpha !== void 0 && !["discard", "keep"].includes(options.alpha)) {
21412
21440
  throw new TypeError("options.alpha, when provided, must be 'discard' or 'keep'.");
21413
21441
  }
21414
- if (options.bitrateMode !== void 0 && !["constant", "variable"].includes(options.bitrateMode)) {
21442
+ const bitrateMode = options.bitrateMode;
21443
+ if (bitrateMode !== void 0 && !["constant", "variable"].includes(bitrateMode)) {
21415
21444
  throw new TypeError("bitrateMode, when provided, must be 'constant' or 'variable'.");
21416
21445
  }
21417
21446
  if (options.latencyMode !== void 0 && !["quality", "realtime"].includes(options.latencyMode)) {
@@ -21437,22 +21466,28 @@ var Mediabunny = (() => {
21437
21466
  throw new TypeError("contentHint, when provided, must be a string.");
21438
21467
  }
21439
21468
  };
21440
- var buildVideoEncoderConfig = (options) => {
21441
- const resolvedBitrate = options.bitrate instanceof Quality ? options.bitrate._toVideoBitrate(options.codec, options.width, options.height) : options.bitrate;
21442
- return {
21469
+ var buildVideoEncoderConfigs = (options) => {
21470
+ const fallbackBitrateMode = options.bitrateMode;
21471
+ const rateControl = options.quality._toVideoRateControl(
21472
+ options.codec,
21473
+ options.width,
21474
+ options.height,
21475
+ fallbackBitrateMode
21476
+ );
21477
+ const buildConfig = (bitrate, bitrateMode, bitrateEstimate) => ({
21443
21478
  codec: options.fullCodecString ?? buildVideoCodecString(
21444
21479
  options.codec,
21445
21480
  options.width,
21446
21481
  options.height,
21447
- resolvedBitrate,
21482
+ bitrateEstimate,
21448
21483
  options.alpha === "keep"
21449
21484
  ),
21450
21485
  width: options.width,
21451
21486
  height: options.height,
21452
21487
  displayWidth: options.squarePixelWidth,
21453
21488
  displayHeight: options.squarePixelHeight,
21454
- bitrate: resolvedBitrate,
21455
- bitrateMode: options.bitrateMode,
21489
+ bitrate,
21490
+ bitrateMode,
21456
21491
  alpha: options.alpha ?? "discard",
21457
21492
  framerate: options.framerate,
21458
21493
  latencyMode: options.latencyMode,
@@ -21460,7 +21495,22 @@ var Mediabunny = (() => {
21460
21495
  scalabilityMode: options.scalabilityMode,
21461
21496
  contentHint: options.contentHint,
21462
21497
  ...getVideoEncoderConfigExtension(options.codec)
21463
- };
21498
+ });
21499
+ const candidates = [];
21500
+ if (rateControl.quantizer !== null) {
21501
+ candidates.push({
21502
+ config: buildConfig(void 0, "quantizer", rateControl.bitrate),
21503
+ quantizer: rateControl.quantizer
21504
+ });
21505
+ }
21506
+ if (rateControl.bitrateMode !== "quantizer") {
21507
+ candidates.push({
21508
+ config: buildConfig(rateControl.bitrate, rateControl.bitrateMode, rateControl.bitrate),
21509
+ quantizer: null
21510
+ });
21511
+ }
21512
+ assert(candidates.length > 0);
21513
+ return candidates;
21464
21514
  };
21465
21515
  var validateAudioEncodingConfig = (config) => {
21466
21516
  if (!config || typeof config !== "object") {
@@ -21469,10 +21519,17 @@ var Mediabunny = (() => {
21469
21519
  if (!AUDIO_CODECS.includes(config.codec)) {
21470
21520
  throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`);
21471
21521
  }
21472
- if (config.bitrate === void 0 && !(PCM_AUDIO_CODECS.includes(config.codec) || config.codec === "flac")) {
21473
- throw new TypeError("config.bitrate must be provided for compressed audio codecs.");
21522
+ const bitrate = config.bitrate;
21523
+ if (config.quality === void 0 && bitrate === void 0 && !(PCM_AUDIO_CODECS.includes(config.codec) || config.codec === "flac")) {
21524
+ throw new TypeError("config.quality must be provided for compressed audio codecs.");
21525
+ }
21526
+ if (config.quality !== void 0 && bitrate !== void 0) {
21527
+ throw new TypeError("config.quality and config.bitrate cannot both be provided.");
21474
21528
  }
21475
- if (config.bitrate !== void 0 && !(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
21529
+ if (config.quality !== void 0 && !(config.quality instanceof Quality)) {
21530
+ throw new TypeError("config.quality, when provided, must be a Quality.");
21531
+ }
21532
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21476
21533
  throw new TypeError("config.bitrate, when provided, must be a positive integer or a quality.");
21477
21534
  }
21478
21535
  if (config.transform !== void 0) {
@@ -21507,7 +21564,8 @@ var Mediabunny = (() => {
21507
21564
  if (!options || typeof options !== "object") {
21508
21565
  throw new TypeError("Encoding options must be an object.");
21509
21566
  }
21510
- if (options.bitrateMode !== void 0 && !["constant", "variable"].includes(options.bitrateMode)) {
21567
+ const bitrateMode = options.bitrateMode;
21568
+ if (bitrateMode !== void 0 && !["constant", "variable"].includes(bitrateMode)) {
21511
21569
  throw new TypeError("bitrateMode, when provided, must be 'constant' or 'variable'.");
21512
21570
  }
21513
21571
  if (options.fullCodecString !== void 0 && typeof options.fullCodecString !== "string") {
@@ -21520,7 +21578,7 @@ var Mediabunny = (() => {
21520
21578
  }
21521
21579
  };
21522
21580
  var buildAudioEncoderConfig = (options) => {
21523
- const resolvedBitrate = options.bitrate instanceof Quality ? options.bitrate._toAudioBitrate(options.codec) : options.bitrate;
21581
+ const fallbackBitrateMode = options.bitrateMode;
21524
21582
  return {
21525
21583
  codec: options.fullCodecString ?? buildAudioCodecString(
21526
21584
  options.codec,
@@ -21529,46 +21587,132 @@ var Mediabunny = (() => {
21529
21587
  ),
21530
21588
  numberOfChannels: options.numberOfChannels,
21531
21589
  sampleRate: options.sampleRate,
21532
- bitrate: resolvedBitrate,
21533
- bitrateMode: options.bitrateMode,
21590
+ bitrate: options.quality?._toAudioBitrate(options.codec),
21591
+ bitrateMode: options.quality?._bitrateMode ?? fallbackBitrateMode,
21534
21592
  ...getAudioEncoderConfigExtension(options.codec)
21535
21593
  };
21536
21594
  };
21537
21595
  var Quality = class {
21538
- /** @internal */
21539
- constructor(factor) {
21540
- this._factor = factor;
21596
+ constructor(options) {
21597
+ if (typeof options === "number" || typeof options === "string") {
21598
+ options = { quality: options };
21599
+ }
21600
+ if (!options || typeof options !== "object") {
21601
+ throw new TypeError("options must be an object.");
21602
+ }
21603
+ if (options.bitrateMode !== void 0 && !["constant", "variable"].includes(options.bitrateMode)) {
21604
+ throw new TypeError("options.bitrateMode, when provided, must be 'constant' or 'variable'.");
21605
+ }
21606
+ if ("quality" in options) {
21607
+ if (typeof options.quality === "string" ? !(options.quality in QUALITY_LEVELS) : typeof options.quality !== "number" || Number.isNaN(options.quality)) {
21608
+ throw new TypeError(
21609
+ "options.quality must be a number, or one of 'very-low', 'low', 'medium', 'high' or 'very-high'."
21610
+ );
21611
+ }
21612
+ if (options.preferBitrate !== void 0 && typeof options.preferBitrate !== "boolean") {
21613
+ throw new TypeError("options.preferBitrate, when provided, must be a boolean.");
21614
+ }
21615
+ if ("bitrate" in options || "quantizer" in options) {
21616
+ throw new TypeError("options.quality cannot be combined with options.bitrate or options.quantizer.");
21617
+ }
21618
+ this._quality = typeof options.quality === "string" ? QUALITY_LEVELS[options.quality] : options.quality;
21619
+ this._preferBitrate = options.preferBitrate ?? false;
21620
+ this._bitrate = void 0;
21621
+ this._quantizer = void 0;
21622
+ } else {
21623
+ if (options.bitrate !== void 0 && (!Number.isInteger(options.bitrate) || options.bitrate <= 0)) {
21624
+ throw new TypeError("options.bitrate, when provided, must be a positive integer.");
21625
+ }
21626
+ if (options.quantizer !== void 0 && (!Number.isInteger(options.quantizer) || options.quantizer < 0)) {
21627
+ throw new TypeError("options.quantizer, when provided, must be a non-negative integer.");
21628
+ }
21629
+ if (options.bitrate === void 0 && options.quantizer === void 0) {
21630
+ throw new TypeError("At least one of options.bitrate or options.quantizer must be set.");
21631
+ }
21632
+ if ("preferBitrate" in options) {
21633
+ throw new TypeError("options.preferBitrate can only be combined with options.quality.");
21634
+ }
21635
+ this._quality = void 0;
21636
+ this._preferBitrate = false;
21637
+ this._bitrate = options.bitrate;
21638
+ this._quantizer = options.quantizer;
21639
+ }
21640
+ this._bitrateMode = options.bitrateMode;
21641
+ }
21642
+ /**
21643
+ * Determines the rate control methods usable for the given codec.
21644
+ * @internal
21645
+ */
21646
+ _toVideoRateControl(codec, width, height, fallbackBitrateMode) {
21647
+ const quantizerSupport = VIDEO_CODEC_QUANTIZER_SUPPORT[codec];
21648
+ let quantizer = null;
21649
+ let bitrateMode = this._bitrateMode ?? fallbackBitrateMode ?? "variable";
21650
+ if (this._quantizer !== void 0) {
21651
+ if (!quantizerSupport) {
21652
+ if (this._bitrate === void 0) {
21653
+ throw new Error(
21654
+ `Codec '${codec}' does not support quantizer-based encoding. Provide a bitrate in the Quality to define a fallback.`
21655
+ );
21656
+ }
21657
+ } else if (this._quantizer < quantizerSupport.min || this._quantizer > quantizerSupport.max) {
21658
+ if (this._bitrate === void 0) {
21659
+ throw new Error(
21660
+ `Quantizer ${this._quantizer} is out of range for codec '${codec}'; must be between ${quantizerSupport.min} and ${quantizerSupport.max}.`
21661
+ );
21662
+ }
21663
+ } else {
21664
+ quantizer = this._quantizer;
21665
+ if (this._bitrate === void 0) {
21666
+ bitrateMode = "quantizer";
21667
+ }
21668
+ }
21669
+ } else if (this._bitrate === void 0 && quantizerSupport && !this._preferBitrate) {
21670
+ assert(this._quality !== void 0);
21671
+ quantizer = clamp(
21672
+ Math.round(lerp(quantizerSupport.worst, quantizerSupport.best, this._quality)),
21673
+ quantizerSupport.min,
21674
+ quantizerSupport.max
21675
+ );
21676
+ }
21677
+ let bitrate;
21678
+ if (this._bitrate !== void 0) {
21679
+ bitrate = this._bitrate;
21680
+ } else {
21681
+ let quality = this._quality;
21682
+ if (quality === void 0) {
21683
+ assert(quantizer !== null && quantizerSupport);
21684
+ quality = clamp(
21685
+ (quantizer - quantizerSupport.worst) / (quantizerSupport.best - quantizerSupport.worst),
21686
+ 0,
21687
+ 1
21688
+ );
21689
+ }
21690
+ bitrate = computeVideoBitrate(codec, width, height, qualityToBitrateFactor(quality));
21691
+ }
21692
+ return { quantizer, bitrate, bitrateMode };
21541
21693
  }
21542
21694
  /** @internal */
21543
21695
  _toVideoBitrate(codec, width, height) {
21544
- const pixels = width * height;
21545
- const referencePixels = 1920 * 1080;
21546
- const referenceBitrate = 3e6;
21547
- const scaleFactor = Math.pow(pixels / referencePixels, 0.95);
21548
- const baseBitrate = referenceBitrate * scaleFactor;
21549
- const codecEfficiencyFactors = {
21550
- avc: 1,
21551
- // H.264/AVC (baseline)
21552
- hevc: 0.6,
21553
- // H.265/HEVC (~40% more efficient than AVC)
21554
- vp9: 0.6,
21555
- // Similar to HEVC
21556
- av1: 0.4,
21557
- // ~60% more efficient than AVC
21558
- vp8: 1.2,
21559
- // Slightly less efficient than AVC
21560
- prores: 22e7 / referenceBitrate
21561
- // Apple ProRes white paper claims 220 Mbps for 1080p 422 HQ @30Hz
21562
- };
21563
- const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
21564
- const finalBitrate = codecAdjustedBitrate * this._factor;
21565
- return Math.ceil(finalBitrate / 1e3) * 1e3;
21696
+ if (this._bitrate !== void 0) {
21697
+ return this._bitrate;
21698
+ }
21699
+ assert(this._quality !== void 0);
21700
+ return computeVideoBitrate(codec, width, height, qualityToBitrateFactor(this._quality));
21566
21701
  }
21567
21702
  /** @internal */
21568
21703
  _toAudioBitrate(codec) {
21569
21704
  if (PCM_AUDIO_CODECS.includes(codec) || codec === "flac") {
21570
21705
  return void 0;
21571
21706
  }
21707
+ if (this._bitrate !== void 0) {
21708
+ return this._bitrate;
21709
+ }
21710
+ if (this._quality === void 0) {
21711
+ throw new Error(
21712
+ "This Quality defines neither a quality level nor a bitrate and therefore cannot be used for audio encoding."
21713
+ );
21714
+ }
21715
+ const factor = qualityToBitrateFactor(this._quality);
21572
21716
  const baseRates = {
21573
21717
  aac: 128e3,
21574
21718
  // 128kbps base for AAC
@@ -21587,7 +21731,7 @@ var Mediabunny = (() => {
21587
21731
  if (!baseBitrate) {
21588
21732
  throw new Error(`Unhandled codec: ${codec}`);
21589
21733
  }
21590
- let finalBitrate = baseBitrate * this._factor;
21734
+ let finalBitrate = baseBitrate * factor;
21591
21735
  if (codec === "aac") {
21592
21736
  const validRates = [96e3, 128e3, 16e4, 192e3];
21593
21737
  finalBitrate = validRates.reduce(
@@ -21621,11 +21765,61 @@ var Mediabunny = (() => {
21621
21765
  return Math.round(finalBitrate / 1e3) * 1e3;
21622
21766
  }
21623
21767
  };
21624
- var QUALITY_VERY_LOW = /* @__PURE__ */ new Quality(0.3);
21625
- var QUALITY_LOW = /* @__PURE__ */ new Quality(0.6);
21626
- var QUALITY_MEDIUM = /* @__PURE__ */ new Quality(1);
21627
- var QUALITY_HIGH = /* @__PURE__ */ new Quality(2);
21628
- var QUALITY_VERY_HIGH = /* @__PURE__ */ new Quality(4);
21768
+ var QUALITY_LEVELS = {
21769
+ "very-low": 0,
21770
+ "low": 0.25,
21771
+ "medium": 0.5,
21772
+ "high": 0.75,
21773
+ "very-high": 1
21774
+ };
21775
+ var VIDEO_CODEC_QUANTIZER_SUPPORT = {
21776
+ avc: { min: 0, max: 51, worst: 41, best: 16 },
21777
+ hevc: { min: 0, max: 51, worst: 41, best: 16 },
21778
+ vp9: { min: 0, max: 63, worst: 52, best: 20 },
21779
+ av1: { min: 0, max: 255, worst: 208, best: 80 }
21780
+ };
21781
+ var qualityToBitrateFactor = (quality) => 0.3 * Math.exp(2.5538 * quality);
21782
+ var computeVideoBitrate = (codec, width, height, factor) => {
21783
+ const pixels = width * height;
21784
+ const referencePixels = 1920 * 1080;
21785
+ const referenceBitrate = 3e6;
21786
+ const scaleFactor = Math.pow(pixels / referencePixels, 0.95);
21787
+ const baseBitrate = referenceBitrate * scaleFactor;
21788
+ const codecEfficiencyFactors = {
21789
+ avc: 1,
21790
+ // H.264/AVC (baseline)
21791
+ hevc: 0.6,
21792
+ // H.265/HEVC (~40% more efficient than AVC)
21793
+ vp9: 0.6,
21794
+ // Similar to HEVC
21795
+ av1: 0.4,
21796
+ // ~60% more efficient than AVC
21797
+ vp8: 1.2,
21798
+ // Slightly less efficient than AVC
21799
+ prores: 22e7 / referenceBitrate
21800
+ // Apple ProRes white paper claims 220 Mbps for 1080p 422 HQ @30Hz
21801
+ };
21802
+ const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
21803
+ const finalBitrate = codecAdjustedBitrate * factor;
21804
+ return Math.ceil(finalBitrate / 1e3) * 1e3;
21805
+ };
21806
+ var buildQuantizerEncodeOptions = (codec, quantizer) => {
21807
+ if (codec === "avc") {
21808
+ return { avc: { quantizer } };
21809
+ } else if (codec === "hevc") {
21810
+ return { hevc: { quantizer } };
21811
+ } else if (codec === "vp9") {
21812
+ return { vp9: { quantizer } };
21813
+ } else if (codec === "av1") {
21814
+ return { av1: { quantizer } };
21815
+ }
21816
+ assert(false);
21817
+ };
21818
+ var QUALITY_VERY_LOW = /* @__PURE__ */ new Quality("very-low");
21819
+ var QUALITY_LOW = /* @__PURE__ */ new Quality("low");
21820
+ var QUALITY_MEDIUM = /* @__PURE__ */ new Quality("medium");
21821
+ var QUALITY_HIGH = /* @__PURE__ */ new Quality("high");
21822
+ var QUALITY_VERY_HIGH = /* @__PURE__ */ new Quality("very-high");
21629
21823
  var canEncode = (codec) => {
21630
21824
  if (VIDEO_CODECS.includes(codec)) {
21631
21825
  return canEncodeVideo(codec);
@@ -21640,7 +21834,9 @@ var Mediabunny = (() => {
21640
21834
  const {
21641
21835
  width = 1280,
21642
21836
  height = 720,
21643
- bitrate = 1e6,
21837
+ quality,
21838
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
21839
+ bitrate,
21644
21840
  ...restOptions
21645
21841
  } = options;
21646
21842
  if (!VIDEO_CODECS.includes(codec)) {
@@ -21652,28 +21848,42 @@ var Mediabunny = (() => {
21652
21848
  if (!Number.isInteger(height) || height <= 0) {
21653
21849
  throw new TypeError("height must be a positive integer.");
21654
21850
  }
21655
- if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21851
+ if (quality !== void 0 && !(quality instanceof Quality)) {
21852
+ throw new TypeError("quality, when provided, must be a Quality.");
21853
+ }
21854
+ if (quality !== void 0 && bitrate !== void 0) {
21855
+ throw new TypeError("quality and bitrate cannot both be provided.");
21856
+ }
21857
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21656
21858
  throw new TypeError("bitrate must be a positive integer or a quality.");
21657
21859
  }
21658
21860
  validateVideoEncodingAdditionalOptions(codec, restOptions);
21659
- const encoderConfig = buildVideoEncoderConfig({
21660
- codec,
21661
- width,
21662
- height,
21663
- bitrate,
21664
- framerate: void 0,
21665
- ...restOptions,
21666
- alpha: "discard"
21667
- // Since we handle alpha ourselves
21668
- });
21669
- const key = JSON.stringify(encoderConfig);
21861
+ const resolvedQuality = resolveQuality(quality, bitrate) ?? new Quality({ bitrate: 1e6 });
21862
+ let candidates;
21863
+ try {
21864
+ candidates = buildVideoEncoderConfigs({
21865
+ codec,
21866
+ width,
21867
+ height,
21868
+ quality: resolvedQuality,
21869
+ framerate: void 0,
21870
+ ...restOptions,
21871
+ alpha: "discard"
21872
+ // Since we handle alpha ourselves
21873
+ });
21874
+ } catch {
21875
+ return false;
21876
+ }
21877
+ const key = JSON.stringify(candidates);
21670
21878
  const memoized = canEncodeVideoMemo.get(key);
21671
21879
  if (memoized) {
21672
21880
  return memoized;
21673
21881
  }
21674
21882
  const promise = (async () => {
21675
- if (customVideoEncoders.some((x) => x.supports(codec, encoderConfig))) {
21676
- return true;
21883
+ for (const { config } of candidates) {
21884
+ if (customVideoEncoders.some((x) => x.supports(codec, config))) {
21885
+ return true;
21886
+ }
21677
21887
  }
21678
21888
  if (typeof VideoEncoder === "undefined") {
21679
21889
  return false;
@@ -21682,19 +21892,22 @@ var Mediabunny = (() => {
21682
21892
  if (hasOddDimension && (codec === "avc" || codec === "hevc")) {
21683
21893
  return false;
21684
21894
  }
21685
- const support = await VideoEncoder.isConfigSupported(encoderConfig);
21686
- if (!support.supported) {
21687
- return false;
21688
- }
21689
- if (isFirefox()) {
21690
- return new Promise(async (resolve) => {
21895
+ for (const { config, quantizer } of candidates) {
21896
+ const support = await VideoEncoder.isConfigSupported(config);
21897
+ if (!support.supported) {
21898
+ continue;
21899
+ }
21900
+ if (!isFirefox()) {
21901
+ return true;
21902
+ }
21903
+ const success = await new Promise(async (resolve) => {
21691
21904
  try {
21692
21905
  const encoder = new VideoEncoder({
21693
21906
  output: () => {
21694
21907
  },
21695
21908
  error: () => resolve(false)
21696
21909
  });
21697
- encoder.configure(encoderConfig);
21910
+ encoder.configure(config);
21698
21911
  const frameData = new Uint8Array(width * height * 4);
21699
21912
  const frame = new VideoFrame(frameData, {
21700
21913
  format: "RGBA",
@@ -21702,7 +21915,10 @@ var Mediabunny = (() => {
21702
21915
  codedHeight: height,
21703
21916
  timestamp: 0
21704
21917
  });
21705
- encoder.encode(frame);
21918
+ encoder.encode(
21919
+ frame,
21920
+ quantizer !== null ? buildQuantizerEncodeOptions(codec, quantizer) : void 0
21921
+ );
21706
21922
  frame.close();
21707
21923
  await encoder.flush();
21708
21924
  resolve(true);
@@ -21710,8 +21926,11 @@ var Mediabunny = (() => {
21710
21926
  resolve(false);
21711
21927
  }
21712
21928
  });
21929
+ if (success) {
21930
+ return true;
21931
+ }
21713
21932
  }
21714
- return true;
21933
+ return false;
21715
21934
  })();
21716
21935
  canEncodeVideoMemo.set(key, promise);
21717
21936
  return promise;
@@ -21720,7 +21939,9 @@ var Mediabunny = (() => {
21720
21939
  const {
21721
21940
  numberOfChannels = 2,
21722
21941
  sampleRate = 48e3,
21723
- bitrate = 128e3,
21942
+ quality,
21943
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
21944
+ bitrate,
21724
21945
  ...restOptions
21725
21946
  } = options;
21726
21947
  if (!AUDIO_CODECS.includes(codec)) {
@@ -21732,15 +21953,22 @@ var Mediabunny = (() => {
21732
21953
  if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
21733
21954
  throw new TypeError("sampleRate must be a positive integer.");
21734
21955
  }
21735
- if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21956
+ if (quality !== void 0 && !(quality instanceof Quality)) {
21957
+ throw new TypeError("quality, when provided, must be a Quality.");
21958
+ }
21959
+ if (quality !== void 0 && bitrate !== void 0) {
21960
+ throw new TypeError("quality and bitrate cannot both be provided.");
21961
+ }
21962
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
21736
21963
  throw new TypeError("bitrate must be a positive integer.");
21737
21964
  }
21738
21965
  validateAudioEncodingAdditionalOptions(codec, restOptions);
21966
+ const resolvedQuality = resolveQuality(quality, bitrate) ?? new Quality({ bitrate: 128e3 });
21739
21967
  const encoderConfig = buildAudioEncoderConfig({
21740
21968
  codec,
21741
21969
  numberOfChannels,
21742
21970
  sampleRate,
21743
- bitrate,
21971
+ quality: resolvedQuality,
21744
21972
  ...restOptions
21745
21973
  });
21746
21974
  const key = JSON.stringify(encoderConfig);
@@ -21764,6 +21992,15 @@ var Mediabunny = (() => {
21764
21992
  canEncodeAudioMemo.set(key, promise);
21765
21993
  return promise;
21766
21994
  };
21995
+ var resolveQuality = (quality, bitrate) => {
21996
+ if (quality !== void 0) {
21997
+ return quality;
21998
+ }
21999
+ if (bitrate === void 0) {
22000
+ return void 0;
22001
+ }
22002
+ return bitrate instanceof Quality ? bitrate : new Quality({ bitrate });
22003
+ };
21767
22004
  var canEncodeSubtitles = async (codec) => {
21768
22005
  if (!SUBTITLE_CODECS.includes(codec)) {
21769
22006
  return false;
@@ -22241,6 +22478,7 @@ var Mediabunny = (() => {
22241
22478
  let decoderIsFlushed = false;
22242
22479
  let ended = false;
22243
22480
  let terminated = false;
22481
+ let decoder = null;
22244
22482
  let outOfBandError = null;
22245
22483
  let hasOutOfBandError = false;
22246
22484
  const packetRetrievalOptions = {
@@ -22249,7 +22487,7 @@ var Mediabunny = (() => {
22249
22487
  metadataOnly: false
22250
22488
  };
22251
22489
  (async () => {
22252
- const decoder = await this._createDecoder((sample) => {
22490
+ decoder = await this._createDecoder((sample) => {
22253
22491
  onQueueDequeue();
22254
22492
  if (sample.timestamp >= endTimestamp) {
22255
22493
  ended = true;
@@ -22306,7 +22544,6 @@ var Mediabunny = (() => {
22306
22544
  if (!terminated && !this._track.input._disposed) {
22307
22545
  await decoder.flush();
22308
22546
  }
22309
- decoder.close();
22310
22547
  if (!firstSampleQueued && lastSample) {
22311
22548
  sampleQueue.push(lastSample);
22312
22549
  }
@@ -22318,6 +22555,8 @@ var Mediabunny = (() => {
22318
22555
  hasOutOfBandError = true;
22319
22556
  onQueueNotEmpty();
22320
22557
  }
22558
+ }).finally(() => {
22559
+ decoder?.close();
22321
22560
  });
22322
22561
  const track = this._track;
22323
22562
  const closeSamples = () => {
@@ -22374,6 +22613,7 @@ var Mediabunny = (() => {
22374
22613
  let { promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers();
22375
22614
  let decoderIsFlushed = false;
22376
22615
  let terminated = false;
22616
+ let decoder = null;
22377
22617
  let outOfBandError = null;
22378
22618
  let hasOutOfBandError = false;
22379
22619
  const pushToQueue = (sample) => {
@@ -22387,7 +22627,7 @@ var Mediabunny = (() => {
22387
22627
  metadataOnly: false
22388
22628
  };
22389
22629
  (async () => {
22390
- const decoder = await this._createDecoder((sample) => {
22630
+ decoder = await this._createDecoder((sample) => {
22391
22631
  onQueueDequeue();
22392
22632
  if (terminated) {
22393
22633
  sample.close();
@@ -22418,6 +22658,7 @@ var Mediabunny = (() => {
22418
22658
  let maxSequenceNumber = -1;
22419
22659
  const decodePackets = async () => {
22420
22660
  assert(lastKeyPacket);
22661
+ assert(decoder);
22421
22662
  let currentPacket = lastKeyPacket;
22422
22663
  decoder.decode(currentPacket);
22423
22664
  while (currentPacket.sequenceNumber < maxSequenceNumber) {
@@ -22437,6 +22678,7 @@ var Mediabunny = (() => {
22437
22678
  maxSequenceNumber = -1;
22438
22679
  };
22439
22680
  const flushDecoder = async () => {
22681
+ assert(decoder);
22440
22682
  await decoder.flush();
22441
22683
  for (let i = 0; i < timestampsOfInterest.length; i++) {
22442
22684
  pushToQueue(null);
@@ -22474,7 +22716,6 @@ var Mediabunny = (() => {
22474
22716
  }
22475
22717
  await flushDecoder();
22476
22718
  }
22477
- decoder.close();
22478
22719
  decoderIsFlushed = true;
22479
22720
  onQueueNotEmpty();
22480
22721
  })().catch((error) => {
@@ -22483,6 +22724,8 @@ var Mediabunny = (() => {
22483
22724
  hasOutOfBandError = true;
22484
22725
  onQueueNotEmpty();
22485
22726
  }
22727
+ }).finally(() => {
22728
+ decoder?.close();
22486
22729
  });
22487
22730
  const track = this._track;
22488
22731
  const closeSamples = () => {
@@ -27156,9 +27399,10 @@ var Mediabunny = (() => {
27156
27399
  // Reserved
27157
27400
  u16(1),
27158
27401
  // Frame count
27159
- Array(32).fill(0),
27160
27402
  // Compressor name
27161
- u16(24),
27403
+ ascii("Mediabunny"),
27404
+ Array(32 - "Mediabunny".length).fill(0),
27405
+ u16(trackData.info.hasAlphaChannel ? 32 : 24),
27162
27406
  // Depth
27163
27407
  i16(65535)
27164
27408
  // Pre-defined
@@ -29114,6 +29358,7 @@ var Mediabunny = (() => {
29114
29358
  num: displayAspectWidth * decoderConfig.codedHeight,
29115
29359
  den: displayAspectHeight * decoderConfig.codedWidth
29116
29360
  });
29361
+ const hasAlphaChannel = decoderConfig.codec === "ap4h" || decoderConfig.codec === "ap4x";
29117
29362
  const newTrackData = {
29118
29363
  muxer: this,
29119
29364
  track,
@@ -29123,7 +29368,8 @@ var Mediabunny = (() => {
29123
29368
  height: decoderConfig.codedHeight,
29124
29369
  pixelAspectRatio,
29125
29370
  decoderConfig,
29126
- requiresAnnexBTransformation
29371
+ requiresAnnexBTransformation,
29372
+ hasAlphaChannel
29127
29373
  },
29128
29374
  timescale,
29129
29375
  samples: [],
@@ -32654,6 +32900,8 @@ ${cue.notes ?? ""}`;
32654
32900
  this.customEncoder = null;
32655
32901
  this.customEncoderCallSerializer = new CallSerializer();
32656
32902
  this.customEncoderQueueSize = 0;
32903
+ // Set when the encoder uses quantizer-based rate control; carries the quantizer value applied to each frame
32904
+ this.defaultEncodeOptions = {};
32657
32905
  // Alpha stuff
32658
32906
  this.alphaEncoder = null;
32659
32907
  this.splitter = null;
@@ -32783,18 +33031,36 @@ ${cue.notes ?? ""}`;
32783
33031
  if (!Array.isArray(processed)) {
32784
33032
  processed = [processed];
32785
33033
  }
32786
- samplesToEncode = processed.map((x) => {
32787
- if (x instanceof VideoSample) {
32788
- return x;
33034
+ const mappedSamples = [];
33035
+ try {
33036
+ for (const x of processed) {
33037
+ if (x instanceof VideoSample) {
33038
+ mappedSamples.push(x);
33039
+ } else if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
33040
+ mappedSamples.push(new VideoSample(x));
33041
+ } else {
33042
+ mappedSamples.push(new VideoSample(x, {
33043
+ timestamp: videoSample.timestamp,
33044
+ duration: videoSample.duration
33045
+ }));
33046
+ }
32789
33047
  }
32790
- if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
32791
- return new VideoSample(x);
33048
+ } catch (error) {
33049
+ for (const sample of mappedSamples) {
33050
+ if (sample !== videoSample) {
33051
+ sample.close();
33052
+ }
32792
33053
  }
32793
- return new VideoSample(x, {
32794
- timestamp: videoSample.timestamp,
32795
- duration: videoSample.duration
32796
- });
32797
- });
33054
+ for (const x of processed) {
33055
+ if (x instanceof VideoSample && x !== videoSample) {
33056
+ x.close();
33057
+ } else if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
33058
+ x.close();
33059
+ }
33060
+ }
33061
+ throw error;
33062
+ }
33063
+ samplesToEncode = mappedSamples;
32798
33064
  } else {
32799
33065
  samplesToEncode = [videoSample];
32800
33066
  }
@@ -32814,7 +33080,11 @@ ${cue.notes ?? ""}`;
32814
33080
  }
32815
33081
  const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
32816
33082
  const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
32817
- const mergedEncodeOptions = { ...sampleToEncode.encodeOptions, ...encodeOptions };
33083
+ const mergedEncodeOptions = {
33084
+ ...this.defaultEncodeOptions,
33085
+ ...sampleToEncode.encodeOptions,
33086
+ ...encodeOptions
33087
+ };
32818
33088
  const finalEncodeOptions = {
32819
33089
  ...mergedEncodeOptions,
32820
33090
  keyFrame: mergedEncodeOptions.keyFrame !== void 0 ? mergedEncodeOptions.keyFrame : keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval
@@ -32860,22 +33130,31 @@ ${cue.notes ?? ""}`;
32860
33130
  }
32861
33131
  }
32862
33132
  if (!this.alphaEncoder) {
32863
- this.encoder.encode(videoFrame, finalEncodeOptions);
32864
- videoFrame.close();
33133
+ try {
33134
+ this.encoder.encode(videoFrame, finalEncodeOptions);
33135
+ } finally {
33136
+ videoFrame.close();
33137
+ }
32865
33138
  } else {
32866
33139
  const frameDefinitelyHasNoAlpha = !!videoFrame.format && !videoFrame.format.includes("A");
32867
33140
  if (frameDefinitelyHasNoAlpha || this.splitterCreationFailed) {
32868
33141
  this.alphaFrameQueue.push(null);
32869
- this.encoder.encode(videoFrame, finalEncodeOptions);
32870
- videoFrame.close();
33142
+ try {
33143
+ this.encoder.encode(videoFrame, finalEncodeOptions);
33144
+ } finally {
33145
+ videoFrame.close();
33146
+ }
32871
33147
  } else {
32872
33148
  if (!this.splitter) {
32873
33149
  this.splitter = new ColorAlphaSplitter();
32874
33150
  }
32875
33151
  const { colorFrame, alphaFrame } = await this.splitter.split(videoFrame);
32876
33152
  this.alphaFrameQueue.push(alphaFrame);
32877
- this.encoder.encode(colorFrame, finalEncodeOptions);
32878
- colorFrame.close();
33153
+ try {
33154
+ this.encoder.encode(colorFrame, finalEncodeOptions);
33155
+ } finally {
33156
+ colorFrame.close();
33157
+ }
32879
33158
  }
32880
33159
  }
32881
33160
  if (this.encoder.encodeQueueSize >= 4) {
@@ -32900,28 +33179,83 @@ ${cue.notes ?? ""}`;
32900
33179
  assert(this.frameRateLastSample);
32901
33180
  const frameDifference = Math.round((until - this.frameRateLastTimestamp) * frameRate);
32902
33181
  for (let i = 1; i < frameDifference; i++) {
32903
- const sample = this.frameRateLastSample.clone();
32904
- sample.setTimestamp(this.frameRateLastTimestamp + i / frameRate);
32905
- sample.setDuration(1 / frameRate);
32906
- await this.processAndEncode(sample, encodeOptions);
32907
- sample.close();
33182
+ var _stack = [];
33183
+ try {
33184
+ const sample = __using(_stack, this.frameRateLastSample.clone());
33185
+ sample.setTimestamp(this.frameRateLastTimestamp + i / frameRate);
33186
+ sample.setDuration(1 / frameRate);
33187
+ await this.processAndEncode(sample, encodeOptions);
33188
+ } catch (_) {
33189
+ var _error = _, _hasError = true;
33190
+ } finally {
33191
+ __callDispose(_stack, _error, _hasError);
33192
+ }
32908
33193
  }
32909
33194
  }
32910
33195
  ensureEncoder(videoSample) {
32911
33196
  this.ensureEncoderPromise = (async () => {
32912
- const encoderConfig = buildVideoEncoderConfig({
33197
+ const quality = resolveQuality(this.encodingConfig.quality, this.encodingConfig.bitrate);
33198
+ assert(quality !== void 0);
33199
+ const candidates = buildVideoEncoderConfigs({
32913
33200
  ...this.encodingConfig,
33201
+ quality,
32914
33202
  width: videoSample.codedWidth,
32915
33203
  height: videoSample.codedHeight,
32916
33204
  squarePixelWidth: videoSample.squarePixelWidth,
32917
33205
  squarePixelHeight: videoSample.squarePixelHeight,
32918
33206
  framerate: this.source._connectedTrack?.metadata.frameRate
32919
33207
  });
32920
- this.encodingConfig.onEncoderConfig?.(encoderConfig);
32921
- const MatchingCustomEncoder = customVideoEncoders.find((x) => x.supports(
32922
- this.encodingConfig.codec,
32923
- encoderConfig
32924
- ));
33208
+ let selected = null;
33209
+ let MatchingCustomEncoder;
33210
+ for (const candidate of candidates) {
33211
+ const candidateConfig = candidate.config;
33212
+ this.encodingConfig.onEncoderConfig?.(candidateConfig);
33213
+ MatchingCustomEncoder = customVideoEncoders.find((x) => x.supports(
33214
+ this.encodingConfig.codec,
33215
+ candidateConfig
33216
+ ));
33217
+ if (MatchingCustomEncoder) {
33218
+ selected = candidate;
33219
+ break;
33220
+ }
33221
+ if (typeof VideoEncoder === "undefined") {
33222
+ continue;
33223
+ }
33224
+ candidateConfig.alpha = "discard";
33225
+ if (this.encodingConfig.alpha === "keep") {
33226
+ candidateConfig.latencyMode = "quality";
33227
+ }
33228
+ const hasOddDimension = candidateConfig.width % 2 === 1 || candidateConfig.height % 2 === 1;
33229
+ if (hasOddDimension && (this.encodingConfig.codec === "avc" || this.encodingConfig.codec === "hevc")) {
33230
+ throw new Error(
33231
+ `The dimensions ${candidateConfig.width}x${candidateConfig.height} are not supported for codec '${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to round your dimensions to the nearest even number.`
33232
+ );
33233
+ }
33234
+ const support = await VideoEncoder.isConfigSupported(candidateConfig);
33235
+ if (support.supported) {
33236
+ selected = candidate;
33237
+ break;
33238
+ }
33239
+ }
33240
+ if (!selected) {
33241
+ if (typeof VideoEncoder === "undefined") {
33242
+ throw new Error("VideoEncoder is not supported by this browser.");
33243
+ }
33244
+ const firstConfig = candidates[0].config;
33245
+ const rateControls = candidates.map(
33246
+ ({ config, quantizer }) => quantizer !== null ? `quantizer ${quantizer}` : `${config.bitrate} bps`
33247
+ );
33248
+ throw new Error(
33249
+ `This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(" / ")}, ${firstConfig.width}x${firstConfig.height}, hardware acceleration: ${firstConfig.hardwareAcceleration ?? "no-preference"}) is not supported by this browser. Consider using another codec or changing your video parameters.`
33250
+ );
33251
+ }
33252
+ const encoderConfig = selected.config;
33253
+ if (selected.quantizer !== null) {
33254
+ this.defaultEncodeOptions = buildQuantizerEncodeOptions(
33255
+ this.encodingConfig.codec,
33256
+ selected.quantizer
33257
+ );
33258
+ }
32925
33259
  if (MatchingCustomEncoder) {
32926
33260
  this.customEncoder = new MatchingCustomEncoder();
32927
33261
  this.customEncoder.codec = this.encodingConfig.codec;
@@ -32944,25 +33278,6 @@ ${cue.notes ?? ""}`;
32944
33278
  };
32945
33279
  await this.customEncoder.init();
32946
33280
  } else {
32947
- if (typeof VideoEncoder === "undefined") {
32948
- throw new Error("VideoEncoder is not supported by this browser.");
32949
- }
32950
- encoderConfig.alpha = "discard";
32951
- if (this.encodingConfig.alpha === "keep") {
32952
- encoderConfig.latencyMode = "quality";
32953
- }
32954
- const hasOddDimension = encoderConfig.width % 2 === 1 || encoderConfig.height % 2 === 1;
32955
- if (hasOddDimension && (this.encodingConfig.codec === "avc" || this.encodingConfig.codec === "hevc")) {
32956
- throw new Error(
32957
- `The dimensions ${encoderConfig.width}x${encoderConfig.height} are not supported for codec '${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to round your dimensions to the nearest even number.`
32958
- );
32959
- }
32960
- const support = await VideoEncoder.isConfigSupported(encoderConfig);
32961
- if (!support.supported) {
32962
- throw new Error(
32963
- `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.`
32964
- );
32965
- }
32966
33281
  const colorChunkQueue = [];
32967
33282
  const nullAlphaChunkQueue = [];
32968
33283
  let encodedAlphaChunkCount = 0;
@@ -33014,6 +33329,7 @@ ${cue.notes ?? ""}`;
33014
33329
  assert(alphaFrame !== void 0);
33015
33330
  if (alphaFrame) {
33016
33331
  this.alphaEncoder.encode(alphaFrame, {
33332
+ ...this.defaultEncodeOptions,
33017
33333
  // Crucial: The alpha frame is forced to be a key frame whenever the color frame
33018
33334
  // also is. Without this, playback can glitch and even crash in some browsers.
33019
33335
  // This is the reason why the two encoders are wired in series and not in parallel.
@@ -33069,36 +33385,42 @@ ${cue.notes ?? ""}`;
33069
33385
  })();
33070
33386
  }
33071
33387
  async flushAndClose(forceClose) {
33072
- if (!forceClose) {
33073
- this.checkForEncoderError();
33074
- }
33075
- if (!forceClose && this.frameRateLastSample) {
33076
- const frameRate = this.encodingConfig.transform.frameRate;
33077
- const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp, frameRate);
33078
- await this.padFrameRate(alignedEnd);
33079
- }
33080
- this.closed = true;
33081
- this.frameRateLastSample?.close();
33082
- this.frameRateLastSample = null;
33083
- if (this.customEncoder) {
33388
+ try {
33084
33389
  if (!forceClose) {
33085
- void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
33390
+ this.checkForEncoderError();
33391
+ if (this.frameRateLastSample) {
33392
+ const frameRate = this.encodingConfig.transform.frameRate;
33393
+ const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp, frameRate);
33394
+ await this.padFrameRate(alignedEnd);
33395
+ }
33086
33396
  }
33087
- await this.customEncoderCallSerializer.call(() => this.customEncoder.close());
33088
- } else if (this.encoder) {
33397
+ this.closed = true;
33089
33398
  if (!forceClose) {
33090
- await this.encoder.flush();
33091
- await this.alphaEncoder?.flush();
33092
- await wait(25);
33093
- }
33094
- if (this.encoder.state !== "closed") {
33095
- this.encoder.close();
33399
+ if (this.customEncoder) {
33400
+ void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
33401
+ } else if (this.encoder) {
33402
+ await this.encoder.flush();
33403
+ await this.alphaEncoder?.flush();
33404
+ await wait(25);
33405
+ }
33096
33406
  }
33097
- if (this.alphaEncoder && this.alphaEncoder.state !== "closed") {
33098
- this.alphaEncoder.close();
33407
+ } finally {
33408
+ this.closed = true;
33409
+ this.frameRateLastSample?.close();
33410
+ this.frameRateLastSample = null;
33411
+ if (this.customEncoder) {
33412
+ await this.customEncoderCallSerializer.call(() => this.customEncoder.close()).catch((error) => this.setError(error));
33413
+ } else if (this.encoder) {
33414
+ if (this.encoder.state !== "closed") {
33415
+ this.encoder.close();
33416
+ }
33417
+ if (this.alphaEncoder && this.alphaEncoder.state !== "closed") {
33418
+ this.alphaEncoder.close();
33419
+ }
33420
+ this.alphaFrameQueue.forEach((x) => x?.close());
33421
+ this.alphaFrameQueue.length = 0;
33422
+ this.splitter?.close();
33099
33423
  }
33100
- this.alphaFrameQueue.forEach((x) => x?.close());
33101
- this.splitter?.close();
33102
33424
  }
33103
33425
  if (!forceClose) {
33104
33426
  this.checkForEncoderError();
@@ -33762,26 +34084,39 @@ ${cue.notes ?? ""}`;
33762
34084
  shouldClose = true;
33763
34085
  }
33764
34086
  if (config.transform?.process) {
33765
- let processed = config.transform.process(audioSample);
33766
- if (processed instanceof Promise) {
33767
- processed = await processed;
33768
- }
33769
- if (processed === null) {
33770
- return;
33771
- }
33772
- if (!Array.isArray(processed)) {
33773
- processed = [processed];
33774
- }
33775
- for (const sample of processed) {
33776
- if (!(sample instanceof AudioSample)) {
33777
- throw new TypeError(
33778
- "The audio process function must return an AudioSample, null, or an array of AudioSamples."
33779
- );
34087
+ try {
34088
+ let processed = config.transform.process(audioSample);
34089
+ if (processed instanceof Promise) {
34090
+ processed = await processed;
34091
+ }
34092
+ if (processed === null) {
34093
+ return;
34094
+ }
34095
+ if (!Array.isArray(processed)) {
34096
+ processed = [processed];
34097
+ }
34098
+ try {
34099
+ for (const sample of processed) {
34100
+ if (!(sample instanceof AudioSample)) {
34101
+ throw new TypeError(
34102
+ "The audio process function must return an AudioSample, null, or an array of AudioSamples."
34103
+ );
34104
+ }
34105
+ }
34106
+ for (const sample of processed) {
34107
+ await this.encodeSample(sample, true);
34108
+ }
34109
+ } finally {
34110
+ for (const sample of processed) {
34111
+ if (sample instanceof AudioSample) {
34112
+ sample.close();
34113
+ }
34114
+ }
34115
+ }
34116
+ } finally {
34117
+ if (shouldClose) {
34118
+ audioSample.close();
33780
34119
  }
33781
- await this.encodeSample(sample, true);
33782
- }
33783
- if (shouldClose) {
33784
- audioSample.close();
33785
34120
  }
33786
34121
  } else {
33787
34122
  await this.encodeSample(audioSample, shouldClose);
@@ -33917,10 +34252,12 @@ ${cue.notes ?? ""}`;
33917
34252
  ensureEncoder(audioSample) {
33918
34253
  this.ensureEncoderPromise = (async () => {
33919
34254
  const { numberOfChannels, sampleRate } = audioSample;
34255
+ const quality = resolveQuality(this.encodingConfig.quality, this.encodingConfig.bitrate);
33920
34256
  const encoderConfig = buildAudioEncoderConfig({
33921
34257
  numberOfChannels,
33922
34258
  sampleRate,
33923
- ...this.encodingConfig
34259
+ ...this.encodingConfig,
34260
+ quality
33924
34261
  });
33925
34262
  this.encodingConfig.onEncoderConfig?.(encoderConfig);
33926
34263
  const MatchingCustomEncoder = customAudioEncoders.find((x) => x.supports(
@@ -34099,24 +34436,27 @@ ${cue.notes ?? ""}`;
34099
34436
  }
34100
34437
  }
34101
34438
  async flushAndClose(forceClose) {
34102
- if (!forceClose) {
34103
- this.checkForEncoderError();
34104
- }
34105
- if (!forceClose && this.resampler) {
34106
- await this.resampler.finalize();
34107
- }
34108
- this.resampler = null;
34109
- this.closed = true;
34110
- if (this.customEncoder) {
34439
+ try {
34111
34440
  if (!forceClose) {
34112
- void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
34441
+ this.checkForEncoderError();
34442
+ if (this.resampler) {
34443
+ await this.resampler.finalize();
34444
+ }
34113
34445
  }
34114
- await this.customEncoderCallSerializer.call(() => this.customEncoder.close());
34115
- } else if (this.encoder) {
34446
+ this.closed = true;
34116
34447
  if (!forceClose) {
34117
- await this.encoder.flush();
34448
+ if (this.customEncoder) {
34449
+ void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
34450
+ } else if (this.encoder) {
34451
+ await this.encoder.flush();
34452
+ }
34118
34453
  }
34119
- if (this.encoder.state !== "closed") {
34454
+ } finally {
34455
+ this.closed = true;
34456
+ this.resampler = null;
34457
+ if (this.customEncoder) {
34458
+ await this.customEncoderCallSerializer.call(() => this.customEncoder.close()).catch((error) => this.setError(error));
34459
+ } else if (this.encoder && this.encoder.state !== "closed") {
34120
34460
  this.encoder.close();
34121
34461
  }
34122
34462
  }
@@ -36801,6 +37141,9 @@ ${cue.notes ?? ""}`;
36801
37141
  }
36802
37142
  this.state = "finalized";
36803
37143
  } finally {
37144
+ await Promise.all([...this._unfinalizedTargets].map((target) => target._close().catch(() => {
37145
+ })));
37146
+ this._unfinalizedTargets.clear();
36804
37147
  release();
36805
37148
  }
36806
37149
  })();
@@ -36823,7 +37166,14 @@ ${cue.notes ?? ""}`;
36823
37166
  `options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(", ")}.`
36824
37167
  );
36825
37168
  }
36826
- if (videoOptions?.bitrate !== void 0 && !(videoOptions.bitrate instanceof Quality) && (!Number.isInteger(videoOptions.bitrate) || videoOptions.bitrate <= 0)) {
37169
+ const bitrate = videoOptions?.bitrate;
37170
+ if (videoOptions?.quality !== void 0 && !(videoOptions.quality instanceof Quality)) {
37171
+ throw new TypeError("options.video.quality, when provided, must be a Quality.");
37172
+ }
37173
+ if (videoOptions?.quality !== void 0 && bitrate !== void 0) {
37174
+ throw new TypeError("options.video.quality and options.video.bitrate cannot both be provided.");
37175
+ }
37176
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
36827
37177
  throw new TypeError("options.video.bitrate, when provided, must be a positive integer or a quality.");
36828
37178
  }
36829
37179
  if (videoOptions?.width !== void 0 && (!Number.isInteger(videoOptions.width) || videoOptions.width <= 0)) {
@@ -36893,7 +37243,14 @@ ${cue.notes ?? ""}`;
36893
37243
  `options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(", ")}.`
36894
37244
  );
36895
37245
  }
36896
- if (audioOptions?.bitrate !== void 0 && !(audioOptions.bitrate instanceof Quality) && (!Number.isInteger(audioOptions.bitrate) || audioOptions.bitrate <= 0)) {
37246
+ const bitrate = audioOptions?.bitrate;
37247
+ if (audioOptions?.quality !== void 0 && !(audioOptions.quality instanceof Quality)) {
37248
+ throw new TypeError("options.audio.quality, when provided, must be a Quality.");
37249
+ }
37250
+ if (audioOptions?.quality !== void 0 && bitrate !== void 0) {
37251
+ throw new TypeError("options.audio.quality and options.audio.bitrate cannot both be provided.");
37252
+ }
37253
+ if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
36897
37254
  throw new TypeError("options.audio.bitrate, when provided, must be a positive integer or a quality.");
36898
37255
  }
36899
37256
  if (audioOptions?.numberOfChannels !== void 0 && (!Number.isInteger(audioOptions.numberOfChannels) || audioOptions.numberOfChannels <= 0)) {
@@ -37277,24 +37634,26 @@ ${cue.notes ?? ""}`;
37277
37634
  if (encodabilityIsTheProblem) {
37278
37635
  const codecs = this.discardedTracks.flatMap((x) => {
37279
37636
  if (x.reason === "discarded_by_user") return [];
37637
+ let supportedCodecs;
37280
37638
  if (x.track.type === "video") {
37281
- return this.output.format.getSupportedVideoCodecs();
37639
+ supportedCodecs = this.output.format.getSupportedVideoCodecs();
37282
37640
  } else if (x.track.type === "audio") {
37283
- return this.output.format.getSupportedAudioCodecs();
37641
+ supportedCodecs = this.output.format.getSupportedAudioCodecs();
37284
37642
  } else {
37285
- return this.output.format.getSupportedSubtitleCodecs();
37643
+ supportedCodecs = this.output.format.getSupportedSubtitleCodecs();
37286
37644
  }
37645
+ return supportedCodecs.filter((codec) => !x.trackOptions.codec || codec === x.trackOptions.codec);
37287
37646
  });
37288
37647
  const uniqueCodecs = [...new Set(codecs)];
37289
37648
  if (uniqueCodecs.length === 1) {
37290
37649
  elements.push(
37291
37650
  `
37292
- Tracks were discarded because your environment is not able to encode '${uniqueCodecs[0]}'.`
37651
+ Tracks were discarded because your environment is not able to encode '${uniqueCodecs[0]}' with the provided parameters.`
37293
37652
  );
37294
37653
  } else {
37295
37654
  elements.push(
37296
37655
  `
37297
- Tracks were discarded because your environment is not able to encode any of the following codecs: ${uniqueCodecs.map((x) => `'${x}'`).join(", ")}.`
37656
+ Tracks were discarded because your environment is not able to encode any of the codecs ${uniqueCodecs.map((x) => `'${x}'`).join(", ")} with the provided parameters.`
37298
37657
  );
37299
37658
  }
37300
37659
  if (uniqueCodecs.includes("mp3")) {
@@ -37497,7 +37856,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37497
37856
  }
37498
37857
  const firstTimestamp = await track.getFirstTimestamp();
37499
37858
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
37500
- const needsTranscode = !!trackOptions.forceTranscode || firstTimestamp < this._startTimestamp || !!trackOptions.frameRate || trackOptions.keyFrameInterval !== void 0 || trackOptions.process !== void 0 || trackOptions.bitrate !== void 0 || !videoCodecs.includes(sourceCodec) || trackOptions.codec && trackOptions.codec !== sourceCodec || width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !canUseRotationMetadata || !!crop;
37859
+ const needsTranscode = !!trackOptions.forceTranscode || firstTimestamp < this._startTimestamp || !!trackOptions.frameRate || trackOptions.keyFrameInterval !== void 0 || trackOptions.process !== void 0 || trackOptions.quality !== void 0 || trackOptions.bitrate !== void 0 || !videoCodecs.includes(sourceCodec) || trackOptions.codec && trackOptions.codec !== sourceCodec || width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !canUseRotationMetadata || !!crop;
37501
37860
  const alpha = trackOptions.alpha ?? "discard";
37502
37861
  if (!needsTranscode) {
37503
37862
  const source = new EncodedVideoPacketSource(sourceCodec);
@@ -37541,11 +37900,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37541
37900
  if (trackOptions.codec) {
37542
37901
  videoCodecs = videoCodecs.filter((codec) => codec === trackOptions.codec);
37543
37902
  }
37544
- const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
37903
+ const quality = resolveQuality(trackOptions.quality, trackOptions.bitrate) ?? new Quality("high");
37545
37904
  const encodableCodec = await getFirstEncodableVideoCodec(videoCodecs, {
37546
37905
  width: trackOptions.process && trackOptions.processedWidth ? trackOptions.processedWidth : width,
37547
37906
  height: trackOptions.process && trackOptions.processedHeight ? trackOptions.processedHeight : height,
37548
- bitrate
37907
+ quality
37549
37908
  });
37550
37909
  if (!encodableCodec) {
37551
37910
  this.discardedTracks.push({
@@ -37557,7 +37916,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37557
37916
  }
37558
37917
  const encodingConfig = {
37559
37918
  codec: encodableCodec,
37560
- bitrate,
37919
+ quality,
37561
37920
  keyFrameInterval: trackOptions.keyFrameInterval,
37562
37921
  sizeChangeBehavior: trackOptions.fit ?? "passThrough",
37563
37922
  alpha,
@@ -37691,7 +38050,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37691
38050
  const needsTrimming = firstTimestamp < this._startTimestamp;
37692
38051
  let needsPadding = firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
37693
38052
  let audioCodecs = this.output.format.getSupportedAudioCodecs();
37694
- if (!trackOptions.forceTranscode && !trackOptions.bitrate && numberOfChannels === originalNumberOfChannels && sampleRate === originalSampleRate && !needsTrimming && !needsPadding && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
38053
+ if (!trackOptions.forceTranscode && !trackOptions.quality && !trackOptions.bitrate && numberOfChannels === originalNumberOfChannels && sampleRate === originalSampleRate && !needsTrimming && !needsPadding && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
37695
38054
  const source = new EncodedAudioPacketSource(sourceCodec);
37696
38055
  audioSource = source;
37697
38056
  this._registerTrackPump(async (pump) => {
@@ -37733,17 +38092,17 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37733
38092
  if (trackOptions.codec) {
37734
38093
  audioCodecs = audioCodecs.filter((codec) => codec === trackOptions.codec);
37735
38094
  }
37736
- const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
38095
+ const quality = resolveQuality(trackOptions.quality, trackOptions.bitrate) ?? new Quality("high");
37737
38096
  const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
37738
38097
  numberOfChannels: trackOptions.process && trackOptions.processedNumberOfChannels ? trackOptions.processedNumberOfChannels : numberOfChannels,
37739
38098
  sampleRate: trackOptions.process && trackOptions.processedSampleRate ? trackOptions.processedSampleRate : sampleRate,
37740
- bitrate
38099
+ quality
37741
38100
  });
37742
38101
  if (!encodableCodecs.some((codec) => NON_PCM_AUDIO_CODECS.includes(codec)) && audioCodecs.some((codec) => NON_PCM_AUDIO_CODECS.includes(codec)) && (numberOfChannels !== FALLBACK_NUMBER_OF_CHANNELS || sampleRate !== FALLBACK_SAMPLE_RATE)) {
37743
38102
  const encodableCodecsWithDefaultParams = await getEncodableAudioCodecs(audioCodecs, {
37744
38103
  numberOfChannels: FALLBACK_NUMBER_OF_CHANNELS,
37745
38104
  sampleRate: FALLBACK_SAMPLE_RATE,
37746
- bitrate
38105
+ quality
37747
38106
  });
37748
38107
  const nonPcmCodec = encodableCodecsWithDefaultParams.find((codec) => NON_PCM_AUDIO_CODECS.includes(codec));
37749
38108
  if (nonPcmCodec) {
@@ -37764,7 +38123,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37764
38123
  }
37765
38124
  const encodingConfig = {
37766
38125
  codec: codecOfChoice,
37767
- bitrate,
38126
+ quality,
37768
38127
  transform: {
37769
38128
  sampleFormat: trackOptions.sampleFormat,
37770
38129
  process: trackOptions.process