mediabunny 1.56.2 → 1.57.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 (93) hide show
  1. package/dist/bundles/mediabunny.cjs +698 -195
  2. package/dist/bundles/mediabunny.min.cjs +13 -13
  3. package/dist/bundles/mediabunny.min.mjs +13 -13
  4. package/dist/bundles/mediabunny.mjs +698 -195
  5. package/dist/bundles/mediabunny.node.cjs +698 -195
  6. package/dist/mediabunny.d.ts +212 -56
  7. package/dist/modules/src/conversion.d.ts +19 -6
  8. package/dist/modules/src/conversion.d.ts.map +1 -1
  9. package/dist/modules/src/conversion.js +40 -6
  10. package/dist/modules/src/encode.d.ts +63 -9
  11. package/dist/modules/src/encode.d.ts.map +1 -1
  12. package/dist/modules/src/encode.js +8 -2
  13. package/dist/modules/src/hls/hls-demuxer.js +2 -2
  14. package/dist/modules/src/hls/hls-muxer.d.ts.map +1 -1
  15. package/dist/modules/src/hls/hls-muxer.js +5 -3
  16. package/dist/modules/src/hls/hls-segmented-input.d.ts.map +1 -1
  17. package/dist/modules/src/hls/hls-segmented-input.js +3 -2
  18. package/dist/modules/src/id3.d.ts.map +1 -1
  19. package/dist/modules/src/id3.js +1 -1
  20. package/dist/modules/src/index.d.ts +1 -1
  21. package/dist/modules/src/index.d.ts.map +1 -1
  22. package/dist/modules/src/input-track.d.ts +21 -5
  23. package/dist/modules/src/input-track.d.ts.map +1 -1
  24. package/dist/modules/src/input-track.js +26 -6
  25. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -0
  26. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  27. package/dist/modules/src/isobmff/isobmff-boxes.js +26 -39
  28. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +6 -2
  29. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  30. package/dist/modules/src/isobmff/isobmff-demuxer.js +44 -30
  31. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +3 -0
  32. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  33. package/dist/modules/src/isobmff/isobmff-muxer.js +52 -0
  34. package/dist/modules/src/isobmff/isobmff-reader.js +1 -1
  35. package/dist/modules/src/matroska/ebml.d.ts +2 -0
  36. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  37. package/dist/modules/src/matroska/ebml.js +2 -0
  38. package/dist/modules/src/matroska/matroska-demuxer.d.ts +2 -0
  39. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  40. package/dist/modules/src/matroska/matroska-demuxer.js +40 -3
  41. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  42. package/dist/modules/src/matroska/matroska-muxer.js +49 -9
  43. package/dist/modules/src/media-sink.d.ts +10 -5
  44. package/dist/modules/src/media-sink.d.ts.map +1 -1
  45. package/dist/modules/src/media-sink.js +15 -4
  46. package/dist/modules/src/media-source.d.ts.map +1 -1
  47. package/dist/modules/src/media-source.js +62 -41
  48. package/dist/modules/src/misc.d.ts +70 -3
  49. package/dist/modules/src/misc.d.ts.map +1 -1
  50. package/dist/modules/src/misc.js +239 -0
  51. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts.map +1 -1
  52. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +3 -3
  53. package/dist/modules/src/output-format.d.ts +16 -11
  54. package/dist/modules/src/output-format.d.ts.map +1 -1
  55. package/dist/modules/src/output-format.js +18 -11
  56. package/dist/modules/src/output.d.ts +23 -2
  57. package/dist/modules/src/output.d.ts.map +1 -1
  58. package/dist/modules/src/output.js +21 -5
  59. package/dist/modules/src/sample.d.ts +48 -23
  60. package/dist/modules/src/sample.d.ts.map +1 -1
  61. package/dist/modules/src/sample.js +72 -16
  62. package/dist/modules/src/segmented-input.js +2 -2
  63. package/dist/modules/src/source.d.ts.map +1 -1
  64. package/dist/modules/src/source.js +34 -17
  65. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  66. package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
  67. package/dist/modules/src/wave/riff-writer.js +2 -1
  68. package/package.json +1 -1
  69. package/src/conversion.ts +68 -12
  70. package/src/encode.ts +71 -10
  71. package/src/hls/hls-demuxer.ts +3 -3
  72. package/src/hls/hls-muxer.ts +5 -3
  73. package/src/hls/hls-segmented-input.ts +3 -2
  74. package/src/id3.ts +1 -0
  75. package/src/index.ts +1 -0
  76. package/src/input-track.ts +37 -7
  77. package/src/isobmff/isobmff-boxes.ts +36 -45
  78. package/src/isobmff/isobmff-demuxer.ts +51 -38
  79. package/src/isobmff/isobmff-muxer.ts +66 -0
  80. package/src/isobmff/isobmff-reader.ts +1 -1
  81. package/src/matroska/ebml.ts +2 -0
  82. package/src/matroska/matroska-demuxer.ts +47 -2
  83. package/src/matroska/matroska-muxer.ts +56 -8
  84. package/src/media-sink.ts +22 -6
  85. package/src/media-source.ts +60 -44
  86. package/src/misc.ts +285 -1
  87. package/src/mpeg-ts/mpeg-ts-demuxer.ts +4 -3
  88. package/src/output-format.ts +20 -13
  89. package/src/output.ts +49 -6
  90. package/src/sample.ts +125 -35
  91. package/src/segmented-input.ts +2 -2
  92. package/src/source.ts +43 -22
  93. package/src/wave/riff-writer.ts +2 -1
@@ -245,6 +245,8 @@ var Mediabunny = (() => {
245
245
  throw new Error("Assertion failed.");
246
246
  }
247
247
  }
248
+ var DEG_TO_RAD = Math.PI / 180;
249
+ var RAD_TO_DEG = 180 / Math.PI;
248
250
  var normalizeRotation = (rotation) => {
249
251
  const mappedRotation = (rotation % 360 + 360) % 360;
250
252
  if (mappedRotation === 0 || mappedRotation === 90 || mappedRotation === 180 || mappedRotation === 270) {
@@ -253,6 +255,86 @@ var Mediabunny = (() => {
253
255
  throw new Error(`Invalid rotation ${rotation}.`);
254
256
  }
255
257
  };
258
+ var IDENTITY_MATRIX = [1, 0, 0, 0, 1, 0, 0, 0, 1];
259
+ var centeredTransformationMatrix = (linear, width, height) => {
260
+ const [a, b, , c, d] = linear;
261
+ const transformedWidth = Math.abs(a) * width + Math.abs(c) * height;
262
+ const transformedHeight = Math.abs(b) * width + Math.abs(d) * height;
263
+ return multiplyMatrices(
264
+ multiplyMatrices(
265
+ translationMatrix(-width / 2, -height / 2),
266
+ linear
267
+ ),
268
+ translationMatrix(transformedWidth / 2, transformedHeight / 2)
269
+ );
270
+ };
271
+ var extractRotationFromMatrix = (matrix) => {
272
+ const [a, b] = matrix;
273
+ const radians = Math.atan2(b, matrixIsFlipped(matrix) ? -a : a);
274
+ return normalizeRotation(roundToMultiple(radians * RAD_TO_DEG, 90));
275
+ };
276
+ var matrixIsFlipped = (matrix) => {
277
+ const [a, b, , c, d] = matrix;
278
+ const det = a * d - b * c;
279
+ return det < 0;
280
+ };
281
+ var rotationMatrix = (rotationInDegrees) => {
282
+ const theta = rotationInDegrees * DEG_TO_RAD;
283
+ const cosTheta = Math.round(Math.cos(theta));
284
+ const sinTheta = Math.round(Math.sin(theta));
285
+ return [
286
+ cosTheta,
287
+ sinTheta,
288
+ 0,
289
+ -sinTheta,
290
+ cosTheta,
291
+ 0,
292
+ 0,
293
+ 0,
294
+ 1
295
+ ];
296
+ };
297
+ var translationMatrix = (x, y) => {
298
+ return [
299
+ 1,
300
+ 0,
301
+ 0,
302
+ 0,
303
+ 1,
304
+ 0,
305
+ x,
306
+ y,
307
+ 1
308
+ ];
309
+ };
310
+ var scaleMatrix = (x, y) => {
311
+ return [
312
+ x,
313
+ 0,
314
+ 0,
315
+ 0,
316
+ y,
317
+ 0,
318
+ 0,
319
+ 0,
320
+ 1
321
+ ];
322
+ };
323
+ var multiplyMatrices = (a, b) => {
324
+ const result = new Array(9);
325
+ for (let i = 0; i < 3; i++) {
326
+ for (let j = 0; j < 3; j++) {
327
+ result[3 * i + j] = a[3 * i] * b[j] + a[3 * i + 1] * b[3 + j] + a[3 * i + 2] * b[6 + j];
328
+ }
329
+ }
330
+ return result;
331
+ };
332
+ var composeRotationAndFlip = (rotation1, flip1, rotation2, flip2) => {
333
+ return {
334
+ rotation: normalizeRotation(rotation1 + (flip1 ? -rotation2 : rotation2)),
335
+ flip: flip1 !== flip2
336
+ };
337
+ };
256
338
  var last = (arr) => {
257
339
  return arr && arr[arr.length - 1];
258
340
  };
@@ -312,6 +394,139 @@ var Mediabunny = (() => {
312
394
  return new DataView(source);
313
395
  }
314
396
  };
397
+ var TextEncoder = typeof globalThis.TextEncoder !== "undefined" ? globalThis.TextEncoder : class TextEncoder2 {
398
+ constructor() {
399
+ this.encoding = "utf-8";
400
+ }
401
+ encode(input = "") {
402
+ const bytes2 = new Uint8Array(3 * input.length);
403
+ let n = 0;
404
+ for (let i = 0; i < input.length; i++) {
405
+ let c = input.charCodeAt(i);
406
+ if (c < 128) {
407
+ bytes2[n++] = c;
408
+ } else if (c < 2048) {
409
+ bytes2[n++] = 192 | c >> 6;
410
+ bytes2[n++] = 128 | c & 63;
411
+ } else if (c < 55296 || c > 57343) {
412
+ bytes2[n++] = 224 | c >> 12;
413
+ bytes2[n++] = 128 | c >> 6 & 63;
414
+ bytes2[n++] = 128 | c & 63;
415
+ } else {
416
+ const next = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
417
+ if (c < 56320 && next >= 56320 && next <= 57343) {
418
+ c = 65536 + (c - 55296 << 10) + (next - 56320);
419
+ i++;
420
+ bytes2[n++] = 240 | c >> 18;
421
+ bytes2[n++] = 128 | c >> 12 & 63;
422
+ bytes2[n++] = 128 | c >> 6 & 63;
423
+ bytes2[n++] = 128 | c & 63;
424
+ } else {
425
+ bytes2[n++] = 239;
426
+ bytes2[n++] = 191;
427
+ bytes2[n++] = 189;
428
+ }
429
+ }
430
+ }
431
+ return bytes2.slice(0, n);
432
+ }
433
+ };
434
+ var TextDecoder = typeof globalThis.TextDecoder !== "undefined" ? globalThis.TextDecoder : class TextDecoder2 {
435
+ constructor(label = "utf-8") {
436
+ const normalized = label.trim().toLowerCase();
437
+ if (normalized === "utf-8" || normalized === "utf8" || normalized === "unicode-1-1-utf-8") {
438
+ this.encoding = "utf-8";
439
+ } else if (normalized === "utf-16le" || normalized === "utf-16") {
440
+ this.encoding = "utf-16le";
441
+ } else if (normalized === "utf-16be") {
442
+ this.encoding = "utf-16be";
443
+ } else {
444
+ throw new RangeError(`The encoding label provided ('${label}') is invalid.`);
445
+ }
446
+ }
447
+ decode(input) {
448
+ const bytes2 = input ? toUint8Array(input) : new Uint8Array(0);
449
+ const units = [];
450
+ if (this.encoding === "utf-8") {
451
+ let i = bytes2.length >= 3 && bytes2[0] === 239 && bytes2[1] === 187 && bytes2[2] === 191 ? 3 : 0;
452
+ while (i < bytes2.length) {
453
+ const lead = bytes2[i];
454
+ if (lead < 128) {
455
+ units.push(lead);
456
+ i++;
457
+ continue;
458
+ }
459
+ let continuationCount;
460
+ let codePoint;
461
+ if (lead >= 194 && lead < 224) {
462
+ continuationCount = 1;
463
+ codePoint = lead & 31;
464
+ } else if (lead >= 224 && lead < 240) {
465
+ continuationCount = 2;
466
+ codePoint = lead & 15;
467
+ } else if (lead >= 240 && lead < 245) {
468
+ continuationCount = 3;
469
+ codePoint = lead & 7;
470
+ } else {
471
+ units.push(65533);
472
+ i++;
473
+ continue;
474
+ }
475
+ let lowerBound = lead === 224 ? 160 : lead === 240 ? 144 : 128;
476
+ let upperBound = lead === 237 ? 159 : lead === 244 ? 143 : 191;
477
+ let j = 1;
478
+ for (; j <= continuationCount; j++) {
479
+ const byte = i + j < bytes2.length ? bytes2[i + j] : 0;
480
+ if (byte < lowerBound || byte > upperBound) {
481
+ break;
482
+ }
483
+ codePoint = codePoint << 6 | byte & 63;
484
+ lowerBound = 128;
485
+ upperBound = 191;
486
+ }
487
+ i += j;
488
+ if (j <= continuationCount) {
489
+ units.push(65533);
490
+ } else if (codePoint >= 65536) {
491
+ codePoint -= 65536;
492
+ units.push(55296 | codePoint >> 10, 56320 | codePoint & 1023);
493
+ } else {
494
+ units.push(codePoint);
495
+ }
496
+ }
497
+ } else {
498
+ const littleEndian = this.encoding === "utf-16le";
499
+ const bomLow = littleEndian ? 255 : 254;
500
+ const bomHigh = littleEndian ? 254 : 255;
501
+ let i = bytes2.length >= 2 && bytes2[0] === bomLow && bytes2[1] === bomHigh ? 2 : 0;
502
+ while (i + 1 < bytes2.length) {
503
+ const unit = littleEndian ? bytes2[i] | bytes2[i + 1] << 8 : bytes2[i] << 8 | bytes2[i + 1];
504
+ i += 2;
505
+ if (unit >= 55296 && unit <= 56319) {
506
+ const next = i + 1 < bytes2.length ? littleEndian ? bytes2[i] | bytes2[i + 1] << 8 : bytes2[i] << 8 | bytes2[i + 1] : -1;
507
+ if (next >= 56320 && next <= 57343) {
508
+ units.push(unit, next);
509
+ i += 2;
510
+ } else {
511
+ units.push(65533);
512
+ }
513
+ } else if (unit >= 56320 && unit <= 57343) {
514
+ units.push(65533);
515
+ } else {
516
+ units.push(unit);
517
+ }
518
+ }
519
+ if (i < bytes2.length) {
520
+ units.push(65533);
521
+ }
522
+ }
523
+ let result = "";
524
+ for (let i = 0; i < units.length; i += 8192) {
525
+ result += String.fromCharCode(...units.slice(i, i + 8192));
526
+ }
527
+ return result;
528
+ }
529
+ };
315
530
  var textDecoder = /* @__PURE__ */ new TextDecoder();
316
531
  var textEncoder = /* @__PURE__ */ new TextEncoder();
317
532
  var isIso88591Compatible = (text) => {
@@ -6258,6 +6473,7 @@ var Mediabunny = (() => {
6258
6473
  this.metadataPromise = null;
6259
6474
  this.movieTimescale = -1;
6260
6475
  this.movieDurationInTimescale = -1;
6476
+ this.movieMatrix = IDENTITY_MATRIX;
6261
6477
  this.isQuickTime = false;
6262
6478
  this.metadataTags = {};
6263
6479
  this.currentMetadataKeys = null;
@@ -6376,6 +6592,7 @@ var Mediabunny = (() => {
6376
6592
  await initDemuxer.readMetadata();
6377
6593
  this.movieTimescale = initDemuxer.movieTimescale;
6378
6594
  this.movieDurationInTimescale = initDemuxer.movieDurationInTimescale;
6595
+ this.movieMatrix = initDemuxer.movieMatrix;
6379
6596
  this.metadataTags = initDemuxer.metadataTags;
6380
6597
  this.isFragmented = true;
6381
6598
  this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
@@ -6389,7 +6606,7 @@ var Mediabunny = (() => {
6389
6606
  timescale: foreignTrack.timescale,
6390
6607
  durationInMediaTimescale: foreignTrack.durationInMediaTimescale,
6391
6608
  durationInMovieTimescale: foreignTrack.durationInMovieTimescale,
6392
- rotation: foreignTrack.rotation,
6609
+ matrix: foreignTrack.matrix,
6393
6610
  internalCodecId: foreignTrack.internalCodecId,
6394
6611
  name: foreignTrack.name,
6395
6612
  languageCode: foreignTrack.languageCode,
@@ -6403,6 +6620,8 @@ var Mediabunny = (() => {
6403
6620
  encryptionInfo: foreignTrack.encryptionInfo,
6404
6621
  encryptionAuxInfo: null,
6405
6622
  frmaCodecString: null,
6623
+ maxBitrate: foreignTrack.maxBitrate,
6624
+ avgBitrate: foreignTrack.avgBitrate,
6406
6625
  info: foreignTrack.info
6407
6626
  };
6408
6627
  if (foreignTrack.trackBacking) {
@@ -6638,6 +6857,8 @@ var Mediabunny = (() => {
6638
6857
  this.movieTimescale = readU32Be(slice);
6639
6858
  this.movieDurationInTimescale = readU32Be(slice);
6640
6859
  }
6860
+ slice.skip(4 + 2 + 2 + 2 * 4);
6861
+ this.movieMatrix = readMatrix(slice);
6641
6862
  }
6642
6863
  ;
6643
6864
  break;
@@ -6655,7 +6876,7 @@ var Mediabunny = (() => {
6655
6876
  timescale: -1,
6656
6877
  durationInMovieTimescale: -1,
6657
6878
  durationInMediaTimescale: -1,
6658
- rotation: 0,
6879
+ matrix: IDENTITY_MATRIX,
6659
6880
  internalCodecId: null,
6660
6881
  name: null,
6661
6882
  languageCode: UNDETERMINED_LANGUAGE,
@@ -6668,7 +6889,9 @@ var Mediabunny = (() => {
6668
6889
  editListOffset: 0,
6669
6890
  encryptionInfo: null,
6670
6891
  encryptionAuxInfo: null,
6671
- frmaCodecString: null
6892
+ frmaCodecString: null,
6893
+ maxBitrate: null,
6894
+ avgBitrate: null
6672
6895
  };
6673
6896
  this.currentTrack = track;
6674
6897
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
@@ -6711,20 +6934,8 @@ var Mediabunny = (() => {
6711
6934
  throw new Error(`Incorrect track header version ${version}.`);
6712
6935
  }
6713
6936
  slice.skip(2 * 4 + 2 + 2 + 2 + 2);
6714
- const matrix = [
6715
- readFixed_16_16(slice),
6716
- readFixed_16_16(slice),
6717
- readFixed_2_30(slice),
6718
- readFixed_16_16(slice),
6719
- readFixed_16_16(slice),
6720
- readFixed_2_30(slice),
6721
- readFixed_16_16(slice),
6722
- readFixed_16_16(slice),
6723
- readFixed_2_30(slice)
6724
- ];
6725
- const rotation = normalizeRotation(roundToMultiple(extractRotationFromMatrix(matrix), 90));
6726
- assert(rotation === 0 || rotation === 90 || rotation === 180 || rotation === 270);
6727
- track.rotation = rotation;
6937
+ const trackMatrix = readMatrix(slice);
6938
+ track.matrix = multiplyMatrices(trackMatrix, this.movieMatrix);
6728
6939
  }
6729
6940
  ;
6730
6941
  break;
@@ -7280,6 +7491,20 @@ var Mediabunny = (() => {
7280
7491
  }
7281
7492
  ;
7282
7493
  break;
7494
+ case "btrt":
7495
+ {
7496
+ const track = this.currentTrack;
7497
+ if (!track) {
7498
+ break;
7499
+ }
7500
+ slice.skip(4);
7501
+ const maxBitrate = readU32Be(slice);
7502
+ const avgBitrate = readU32Be(slice);
7503
+ track.maxBitrate = maxBitrate > 0 ? maxBitrate : null;
7504
+ track.avgBitrate = avgBitrate > 0 ? avgBitrate : null;
7505
+ }
7506
+ ;
7507
+ break;
7283
7508
  case "wave":
7284
7509
  {
7285
7510
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
@@ -8498,10 +8723,10 @@ var Mediabunny = (() => {
8498
8723
  return 1n;
8499
8724
  }
8500
8725
  getBitrate() {
8501
- return null;
8726
+ return this.internalTrack.maxBitrate;
8502
8727
  }
8503
8728
  getAverageBitrate() {
8504
- return null;
8729
+ return this.internalTrack.avgBitrate;
8505
8730
  }
8506
8731
  async getDurationFromMetadata() {
8507
8732
  const track = this.internalTrack;
@@ -8894,8 +9119,8 @@ var Mediabunny = (() => {
8894
9119
  getSquarePixelHeight() {
8895
9120
  return this.internalTrack.info.squarePixelHeight;
8896
9121
  }
8897
- getRotation() {
8898
- return this.internalTrack.rotation;
9122
+ getTransformationMatrix() {
9123
+ return [...this.internalTrack.matrix];
8899
9124
  }
8900
9125
  async getColorSpace() {
8901
9126
  const decoderConfig = await this.getDecoderConfig();
@@ -9134,13 +9359,18 @@ var Mediabunny = (() => {
9134
9359
  entry.presentationTimestamp += timestamp;
9135
9360
  }
9136
9361
  };
9137
- var extractRotationFromMatrix = (matrix) => {
9138
- const [a, b] = matrix;
9139
- const radians = Math.atan2(b, a);
9140
- if (!Number.isFinite(radians)) {
9141
- return 0;
9142
- }
9143
- return radians * (180 / Math.PI);
9362
+ var readMatrix = (slice) => {
9363
+ return [
9364
+ readFixed_16_16(slice),
9365
+ readFixed_16_16(slice),
9366
+ readFixed_2_30(slice),
9367
+ readFixed_16_16(slice),
9368
+ readFixed_16_16(slice),
9369
+ readFixed_2_30(slice),
9370
+ readFixed_16_16(slice),
9371
+ readFixed_16_16(slice),
9372
+ readFixed_2_30(slice)
9373
+ ];
9144
9374
  };
9145
9375
  var sampleTableIsEmpty = (sampleTable) => {
9146
9376
  return sampleTable.sampleSizes.length === 0;
@@ -10617,6 +10847,8 @@ var Mediabunny = (() => {
10617
10847
  displayUnit: null,
10618
10848
  squarePixelWidth: -1,
10619
10849
  squarePixelHeight: -1,
10850
+ horizontalScale: 1,
10851
+ verticalScale: 1,
10620
10852
  rotation: 0,
10621
10853
  codec: null,
10622
10854
  codecDescription: null,
@@ -10846,6 +11078,32 @@ var Mediabunny = (() => {
10846
11078
  }
10847
11079
  ;
10848
11080
  break;
11081
+ // Yaw and pitch rotate the frame in 3D space about the vertical and horizontal axis respectively. When
11082
+ // snapped to multiples of 90 degrees, that amounts to scaling each axis by the cosine, which models flips.
11083
+ case 30323 /* ProjectionPoseYaw */:
11084
+ {
11085
+ if (this.currentTrack?.info?.type !== "video") break;
11086
+ const yaw = readFloat(slice, size);
11087
+ try {
11088
+ const normalized = normalizeRotation(yaw);
11089
+ this.currentTrack.info.horizontalScale = Math.round(Math.cos(normalized * DEG_TO_RAD));
11090
+ } catch {
11091
+ }
11092
+ }
11093
+ ;
11094
+ break;
11095
+ case 30324 /* ProjectionPosePitch */:
11096
+ {
11097
+ if (this.currentTrack?.info?.type !== "video") break;
11098
+ const pitch = readFloat(slice, size);
11099
+ try {
11100
+ const normalized = normalizeRotation(pitch);
11101
+ this.currentTrack.info.verticalScale = Math.round(Math.cos(normalized * DEG_TO_RAD));
11102
+ } catch {
11103
+ }
11104
+ }
11105
+ ;
11106
+ break;
10849
11107
  case 30325 /* ProjectionPoseRoll */:
10850
11108
  {
10851
11109
  if (this.currentTrack?.info?.type !== "video") break;
@@ -11822,8 +12080,13 @@ var Mediabunny = (() => {
11822
12080
  getSquarePixelHeight() {
11823
12081
  return this.internalTrack.info.squarePixelHeight;
11824
12082
  }
11825
- getRotation() {
11826
- return this.internalTrack.info.rotation;
12083
+ getTransformationMatrix() {
12084
+ const info = this.internalTrack.info;
12085
+ const linear = multiplyMatrices(
12086
+ scaleMatrix(info.horizontalScale, info.verticalScale),
12087
+ rotationMatrix(info.rotation)
12088
+ );
12089
+ return centeredTransformationMatrix(linear, info.width, info.height);
11827
12090
  }
11828
12091
  async getColorSpace() {
11829
12092
  const decoderConfig = await this.getDecoderConfig();
@@ -15801,8 +16064,8 @@ var Mediabunny = (() => {
15801
16064
  getSquarePixelHeight() {
15802
16065
  return this.elementaryStream.info.squarePixelHeight;
15803
16066
  }
15804
- getRotation() {
15805
- return 0;
16067
+ getTransformationMatrix() {
16068
+ return [...IDENTITY_MATRIX];
15806
16069
  }
15807
16070
  async getColorSpace() {
15808
16071
  return this.elementaryStream.info.colorSpace;
@@ -16855,8 +17118,8 @@ var Mediabunny = (() => {
16855
17118
  getSquarePixelHeight() {
16856
17119
  return this.delegate(() => this.firstInputTrack._backing.getSquarePixelHeight());
16857
17120
  }
16858
- getRotation() {
16859
- return this.delegate(() => this.firstInputTrack._backing.getRotation());
17121
+ getTransformationMatrix() {
17122
+ return this.delegate(() => this.firstInputTrack._backing.getTransformationMatrix());
16860
17123
  }
16861
17124
  async getColorSpace() {
16862
17125
  return this.delegate(() => this.firstInputTrack._backing.getColorSpace());
@@ -17752,6 +18015,8 @@ var Mediabunny = (() => {
17752
18015
  );
17753
18016
  }
17754
18017
  super();
18018
+ /** @internal */
18019
+ this._readers = /* @__PURE__ */ new Set();
17755
18020
  this._options = options;
17756
18021
  this._orchestrator = new ReadOrchestrator({
17757
18022
  maxCacheSize: options.maxCacheSize ?? 8 * 2 ** 20,
@@ -17796,6 +18061,9 @@ var Mediabunny = (() => {
17796
18061
  let data = this._options.read(worker.currentPos, originalTargetPos);
17797
18062
  if (isThenable(data)) data = await data;
17798
18063
  if (worker.aborted) {
18064
+ if (data instanceof ReadableStream) {
18065
+ await data.cancel();
18066
+ }
17799
18067
  break;
17800
18068
  }
17801
18069
  if (data instanceof Uint8Array) {
@@ -17809,25 +18077,33 @@ var Mediabunny = (() => {
17809
18077
  this._orchestrator.supplyWorkerData(worker, data);
17810
18078
  } else if (data instanceof ReadableStream) {
17811
18079
  const reader = data.getReader();
17812
- while (worker.currentPos < originalTargetPos && !worker.aborted) {
17813
- const { done, value } = await reader.read();
17814
- if (done) {
17815
- if (worker.currentPos < originalTargetPos) {
17816
- throw new Error(
17817
- `ReadableStream returned by options.read ended before supplying enough data. Requested ${originalTargetPos - originalCurrentPos} bytes, but got ${worker.currentPos - originalCurrentPos}`
18080
+ this._readers.add(reader);
18081
+ try {
18082
+ while (worker.currentPos < originalTargetPos && !worker.aborted) {
18083
+ const { done, value } = await reader.read();
18084
+ if (done) {
18085
+ if (worker.currentPos < originalTargetPos) {
18086
+ throw new Error(
18087
+ `ReadableStream returned by options.read ended before supplying enough data. Requested ${originalTargetPos - originalCurrentPos} bytes, but got ${worker.currentPos - originalCurrentPos}`
18088
+ );
18089
+ }
18090
+ break;
18091
+ }
18092
+ if (!(value instanceof Uint8Array)) {
18093
+ throw new TypeError(
18094
+ "ReadableStream returned by options.read must yield Uint8Array chunks."
17818
18095
  );
17819
18096
  }
17820
- break;
17821
- }
17822
- if (!(value instanceof Uint8Array)) {
17823
- throw new TypeError("ReadableStream returned by options.read must yield Uint8Array chunks.");
17824
- }
17825
- if (worker.aborted) {
17826
- break;
18097
+ if (worker.aborted) {
18098
+ break;
18099
+ }
18100
+ const data2 = toUint8Array(value);
18101
+ this._dispatchRead(worker.currentPos, worker.currentPos + data2.length);
18102
+ this._orchestrator.supplyWorkerData(worker, data2);
17827
18103
  }
17828
- const data2 = toUint8Array(value);
17829
- this._dispatchRead(worker.currentPos, worker.currentPos + data2.length);
17830
- this._orchestrator.supplyWorkerData(worker, data2);
18104
+ } finally {
18105
+ this._readers.delete(reader);
18106
+ reader.releaseLock();
17831
18107
  }
17832
18108
  } else {
17833
18109
  throw new TypeError("options.read must return or resolve to a Uint8Array or a ReadableStream.");
@@ -17838,6 +18114,11 @@ var Mediabunny = (() => {
17838
18114
  /** @internal */
17839
18115
  _dispose() {
17840
18116
  this._orchestrator.dispose();
18117
+ for (const reader of this._readers) {
18118
+ void reader.cancel().catch(() => {
18119
+ });
18120
+ }
18121
+ this._readers.clear();
17841
18122
  this._options.dispose?.();
17842
18123
  }
17843
18124
  };
@@ -18871,10 +19152,11 @@ var Mediabunny = (() => {
18871
19152
  throw new Error(`Unsupported IV format '${ivString}'.`);
18872
19153
  }
18873
19154
  let hex = ivString.slice(2);
18874
- hex = hex.padStart(AES_128_BLOCK_SIZE * 2, "0");
19155
+ hex = hex.padStart(2 * AES_128_BLOCK_SIZE, "0");
19156
+ hex = hex.slice(-2 * AES_128_BLOCK_SIZE);
18875
19157
  iv = new Uint8Array(AES_128_BLOCK_SIZE);
18876
19158
  for (let i2 = 0; i2 < AES_128_BLOCK_SIZE; i2++) {
18877
- const startIndex = -AES_128_BLOCK_SIZE * 2 + i2;
19159
+ const startIndex = 2 * i2;
18878
19160
  iv[i2] = parseInt(hex.slice(startIndex, startIndex + 2), 16);
18879
19161
  }
18880
19162
  }
@@ -19780,8 +20062,8 @@ var Mediabunny = (() => {
19780
20062
  }
19781
20063
  return this.internalTrack.info.height;
19782
20064
  }
19783
- getRotation() {
19784
- return this.delegate(() => this.backingVideoTrack.getRotation());
20065
+ getTransformationMatrix() {
20066
+ return this.delegate(() => this.backingVideoTrack.getTransformationMatrix());
19785
20067
  }
19786
20068
  async getColorSpace() {
19787
20069
  await this.hydrate();
@@ -20581,6 +20863,9 @@ var Mediabunny = (() => {
20581
20863
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20582
20864
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20583
20865
  }
20866
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
20867
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20868
+ }
20584
20869
  if (!Number.isFinite(init.timestamp)) {
20585
20870
  throw new TypeError("init.timestamp must be a number.");
20586
20871
  }
@@ -20619,6 +20904,7 @@ var Mediabunny = (() => {
20619
20904
  }
20620
20905
  this.format = init.format;
20621
20906
  this.rotation = init.rotation ?? 0;
20907
+ this.flip = init.flip ?? false;
20622
20908
  this.timestamp = init.timestamp;
20623
20909
  this.duration = init.duration ?? 0;
20624
20910
  const layout = init.layout ?? createDefaultPlaneLayout(init.format, init.codedWidth, init.codedHeight);
@@ -20660,6 +20946,9 @@ var Mediabunny = (() => {
20660
20946
  if (init?.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20661
20947
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20662
20948
  }
20949
+ if (init?.flip !== void 0 && typeof init.flip !== "boolean") {
20950
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20951
+ }
20663
20952
  if (init?.timestamp !== void 0 && !Number.isFinite(init?.timestamp)) {
20664
20953
  throw new TypeError("init.timestamp, when provided, must be a number.");
20665
20954
  }
@@ -20679,6 +20968,7 @@ var Mediabunny = (() => {
20679
20968
  height: data.visibleRect?.height ?? data.codedHeight
20680
20969
  };
20681
20970
  this.rotation = init?.rotation ?? 0;
20971
+ this.flip = init?.flip ?? false;
20682
20972
  this.squarePixelWidth = data.displayWidth;
20683
20973
  this.squarePixelHeight = data.displayHeight;
20684
20974
  this.timestamp = init?.timestamp ?? data.timestamp / 1e6;
@@ -20691,6 +20981,9 @@ var Mediabunny = (() => {
20691
20981
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20692
20982
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20693
20983
  }
20984
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
20985
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20986
+ }
20694
20987
  if (!Number.isFinite(init.timestamp)) {
20695
20988
  throw new TypeError("init.timestamp must be a number.");
20696
20989
  }
@@ -20752,6 +21045,7 @@ var Mediabunny = (() => {
20752
21045
  this.squarePixelWidth = visibleRect.width;
20753
21046
  this.squarePixelHeight = visibleRect.height;
20754
21047
  this.rotation = init.rotation ?? 0;
21048
+ this.flip = init.flip ?? false;
20755
21049
  this.timestamp = init.timestamp;
20756
21050
  this.duration = init.duration ?? 0;
20757
21051
  this.colorSpace = new VideoSampleColorSpace({
@@ -20767,6 +21061,9 @@ var Mediabunny = (() => {
20767
21061
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20768
21062
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20769
21063
  }
21064
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
21065
+ throw new TypeError("init.flip, when provided, must be a boolean.");
21066
+ }
20770
21067
  if (!Number.isFinite(init.timestamp)) {
20771
21068
  throw new TypeError("init.timestamp must be a number.");
20772
21069
  }
@@ -20800,6 +21097,7 @@ var Mediabunny = (() => {
20800
21097
  throw new TypeError("getSquarePixelHeight() must return a positive integer.");
20801
21098
  }
20802
21099
  this.rotation = init.rotation ?? 0;
21100
+ this.flip = init.flip ?? false;
20803
21101
  this.timestamp = init.timestamp;
20804
21102
  this.duration = init.duration ?? 0;
20805
21103
  this.colorSpace = data.getColorSpace();
@@ -20823,11 +21121,11 @@ var Mediabunny = (() => {
20823
21121
  get codedHeight() {
20824
21122
  return this.visibleRect.height;
20825
21123
  }
20826
- /** The display width of the frame in pixels, after aspect ratio adjustment and rotation. */
21124
+ /** The display width of the frame in pixels, after aspect ratio adjustment, rotation and flip. */
20827
21125
  get displayWidth() {
20828
21126
  return this.rotation % 180 === 0 ? this.squarePixelWidth : this.squarePixelHeight;
20829
21127
  }
20830
- /** The display height of the frame in pixels, after aspect ratio adjustment and rotation. */
21128
+ /** The display height of the frame in pixels, after aspect ratio adjustment, rotation and flip. */
20831
21129
  get displayHeight() {
20832
21130
  return this.rotation % 180 === 0 ? this.squarePixelHeight : this.squarePixelWidth;
20833
21131
  }
@@ -20857,6 +21155,7 @@ var Mediabunny = (() => {
20857
21155
  timestamp: this.timestamp,
20858
21156
  duration: this.duration,
20859
21157
  rotation: this.rotation,
21158
+ flip: this.flip,
20860
21159
  encodeOptions: this.encodeOptions
20861
21160
  });
20862
21161
  } else if (isVideoFrame(this._data)) {
@@ -20864,6 +21163,7 @@ var Mediabunny = (() => {
20864
21163
  timestamp: this.timestamp,
20865
21164
  duration: this.duration,
20866
21165
  rotation: this.rotation,
21166
+ flip: this.flip,
20867
21167
  encodeOptions: this.encodeOptions
20868
21168
  });
20869
21169
  } else if (this._data instanceof Uint8Array) {
@@ -20877,6 +21177,7 @@ var Mediabunny = (() => {
20877
21177
  duration: this.duration,
20878
21178
  colorSpace: this.colorSpace,
20879
21179
  rotation: this.rotation,
21180
+ flip: this.flip,
20880
21181
  visibleRect: this.visibleRect,
20881
21182
  displayWidth: this.displayWidth,
20882
21183
  displayHeight: this.displayHeight,
@@ -20893,6 +21194,7 @@ var Mediabunny = (() => {
20893
21194
  duration: this.duration,
20894
21195
  colorSpace: this.colorSpace,
20895
21196
  rotation: this.rotation,
21197
+ flip: this.flip,
20896
21198
  visibleRect: this.visibleRect,
20897
21199
  displayWidth: this.displayWidth,
20898
21200
  displayHeight: this.displayHeight,
@@ -20965,7 +21267,8 @@ var Mediabunny = (() => {
20965
21267
  {
20966
21268
  timestamp: this.timestamp,
20967
21269
  duration: this.duration,
20968
- rotation: this.rotation
21270
+ rotation: this.rotation,
21271
+ flip: this.flip
20969
21272
  },
20970
21273
  options.colorSpace ?? "srgb"
20971
21274
  ));
@@ -21221,12 +21524,15 @@ var Mediabunny = (() => {
21221
21524
  if (this._closed) {
21222
21525
  throw new Error("VideoSample is closed.");
21223
21526
  }
21224
- ({ sx, sy, sWidth, sHeight } = this._rotateSourceRegion(sx, sy, sWidth, sHeight, this.rotation));
21527
+ ({ sx, sy, sWidth, sHeight } = this._unmapSourceRegion(sx, sy, sWidth, sHeight, this.rotation, this.flip));
21225
21528
  const source = this.toCanvasImageSource();
21226
21529
  context.save();
21227
21530
  const centerX = dx + dWidth / 2;
21228
21531
  const centerY = dy + dHeight / 2;
21229
21532
  context.translate(centerX, centerY);
21533
+ if (this.flip) {
21534
+ context.scale(-1, 1);
21535
+ }
21230
21536
  context.rotate(this.rotation * Math.PI / 180);
21231
21537
  const aspectRatioChange = this.rotation % 180 === 0 ? 1 : dWidth / dHeight;
21232
21538
  context.scale(1 / aspectRatioChange, aspectRatioChange);
@@ -21259,12 +21565,16 @@ var Mediabunny = (() => {
21259
21565
  if (options.rotation !== void 0 && ![0, 90, 180, 270].includes(options.rotation)) {
21260
21566
  throw new TypeError("options.rotation, when provided, must be 0, 90, 180, or 270.");
21261
21567
  }
21568
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
21569
+ throw new TypeError("options.flip, when provided, must be a boolean.");
21570
+ }
21262
21571
  if (options.crop !== void 0) {
21263
21572
  validateCropRectangle(options.crop, "options.");
21264
21573
  }
21265
21574
  const canvasWidth = context.canvas.width;
21266
21575
  const canvasHeight = context.canvas.height;
21267
21576
  const rotation = options.rotation ?? this.rotation;
21577
+ const flip = options.flip ?? this.flip;
21268
21578
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
21269
21579
  let finalCrop = options.crop;
21270
21580
  if (finalCrop) {
@@ -21274,12 +21584,13 @@ var Mediabunny = (() => {
21274
21584
  let dy;
21275
21585
  let newWidth;
21276
21586
  let newHeight;
21277
- const { sx, sy, sWidth, sHeight } = this._rotateSourceRegion(
21587
+ const { sx, sy, sWidth, sHeight } = this._unmapSourceRegion(
21278
21588
  options.crop?.left ?? 0,
21279
21589
  options.crop?.top ?? 0,
21280
21590
  options.crop?.width ?? rotatedWidth,
21281
21591
  options.crop?.height ?? rotatedHeight,
21282
- rotation
21592
+ rotation,
21593
+ flip
21283
21594
  );
21284
21595
  if (options.fit === "fill") {
21285
21596
  dx = 0;
@@ -21297,6 +21608,9 @@ var Mediabunny = (() => {
21297
21608
  context.save();
21298
21609
  const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight;
21299
21610
  context.translate(canvasWidth / 2, canvasHeight / 2);
21611
+ if (flip) {
21612
+ context.scale(-1, 1);
21613
+ }
21300
21614
  context.rotate(rotation * Math.PI / 180);
21301
21615
  context.scale(1 / aspectRatioChange, aspectRatioChange);
21302
21616
  context.translate(-canvasWidth / 2, -canvasHeight / 2);
@@ -21304,7 +21618,11 @@ var Mediabunny = (() => {
21304
21618
  context.restore();
21305
21619
  }
21306
21620
  /** @internal */
21307
- _rotateSourceRegion(sx, sy, sWidth, sHeight, rotation) {
21621
+ _unmapSourceRegion(sx, sy, sWidth, sHeight, rotation, flip) {
21622
+ if (flip) {
21623
+ const rotatedWidth = rotation % 180 === 0 ? this.squarePixelWidth : this.squarePixelHeight;
21624
+ sx = rotatedWidth - sx - sWidth;
21625
+ }
21308
21626
  if (rotation === 90) {
21309
21627
  [sx, sy, sWidth, sHeight] = [
21310
21628
  sy,
@@ -21356,6 +21674,7 @@ var Mediabunny = (() => {
21356
21674
  this.drawWithFit(context, {
21357
21675
  fit: options.fit,
21358
21676
  rotation: options.rotation,
21677
+ flip: options.flip,
21359
21678
  crop: options.crop
21360
21679
  });
21361
21680
  context.globalCompositeOperation = "copy";
@@ -21403,8 +21722,8 @@ var Mediabunny = (() => {
21403
21722
  }
21404
21723
  }
21405
21724
  /**
21406
- * Transform this video sample to a new video sample given the options. Can be used to resize, rotate, and crop
21407
- * the sample.
21725
+ * Transform this video sample to a new video sample given the options. Can be used to resize, rotate, flip, and
21726
+ * crop the sample.
21408
21727
  *
21409
21728
  * In non-browser environments, this method will not work by default. To make it work, register a custom
21410
21729
  * transformer function via {@link registerVideoSampleTransformer}.
@@ -21433,13 +21752,21 @@ var Mediabunny = (() => {
21433
21752
  if (options.rotate !== void 0 && ![0, 90, 180, 270].includes(options.rotate)) {
21434
21753
  throw new TypeError("options.rotate, when provided, must be 0, 90, 180 or 270.");
21435
21754
  }
21755
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
21756
+ throw new TypeError("options.flip, when provided, must be a boolean.");
21757
+ }
21436
21758
  if (options.crop !== void 0) {
21437
21759
  validateCropRectangle(options.crop, "options.");
21438
21760
  }
21439
21761
  if (options.alpha !== void 0 && !["keep", "discard"].includes(options.alpha)) {
21440
21762
  throw new TypeError("options.alpha, when provided, must be 'keep' or 'discard'.");
21441
21763
  }
21442
- const rotation = normalizeRotation(this.rotation + (options.rotate ?? 0));
21764
+ const { rotation, flip } = composeRotationAndFlip(
21765
+ this.rotation,
21766
+ this.flip,
21767
+ options.rotate ?? 0,
21768
+ options.flip ?? false
21769
+ );
21443
21770
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
21444
21771
  let finalCrop = options.crop;
21445
21772
  if (finalCrop) {
@@ -21470,6 +21797,7 @@ var Mediabunny = (() => {
21470
21797
  height: targetHeight,
21471
21798
  fit: options.fit ?? "fill",
21472
21799
  rotation,
21800
+ flip,
21473
21801
  crop: finalCrop ?? {
21474
21802
  left: 0,
21475
21803
  top: 0,
@@ -21489,6 +21817,7 @@ var Mediabunny = (() => {
21489
21817
  this._drawWithFitAndMipmapping(canvas, context, {
21490
21818
  fit: description.fit,
21491
21819
  rotation: description.rotation,
21820
+ flip: description.flip,
21492
21821
  crop: description.crop,
21493
21822
  targetIsFresh: isNew,
21494
21823
  fillBlack: description.alpha === "discard"
@@ -21496,8 +21825,9 @@ var Mediabunny = (() => {
21496
21825
  return new _VideoSample(canvas, {
21497
21826
  timestamp: this.timestamp,
21498
21827
  duration: this.duration,
21499
- rotation: 0
21500
- // Any previous rotation is now baked in
21828
+ // Any previous rotation and flip are now baked in
21829
+ rotation: 0,
21830
+ flip: false
21501
21831
  });
21502
21832
  }
21503
21833
  /** Sets the rotation metadata of this video sample. */
@@ -21507,6 +21837,13 @@ var Mediabunny = (() => {
21507
21837
  }
21508
21838
  this.rotation = newRotation;
21509
21839
  }
21840
+ /** Sets the flip metadata of this video sample. */
21841
+ setFlip(newFlip) {
21842
+ if (typeof newFlip !== "boolean") {
21843
+ throw new TypeError("newFlip must be a boolean.");
21844
+ }
21845
+ this.flip = newFlip;
21846
+ }
21510
21847
  /** Sets the presentation timestamp of this video sample, in seconds. */
21511
21848
  setTimestamp(newTimestamp) {
21512
21849
  if (!Number.isFinite(newTimestamp)) {
@@ -22702,6 +23039,9 @@ var Mediabunny = (() => {
22702
23039
  if (config.transform.rotate !== void 0 && ![0, 90, 180, 270].includes(config.transform.rotate)) {
22703
23040
  throw new TypeError("config.transform.rotate, when provided, must be 0, 90, 180 or 270.");
22704
23041
  }
23042
+ if (config.transform.flip !== void 0 && typeof config.transform.flip !== "boolean") {
23043
+ throw new TypeError("config.transform.flip, when provided, must be a boolean.");
23044
+ }
22705
23045
  if (config.transform.crop !== void 0) {
22706
23046
  validateCropRectangle(config.transform.crop, "config.transform.");
22707
23047
  }
@@ -23133,6 +23473,7 @@ var Mediabunny = (() => {
23133
23473
  quality,
23134
23474
  // eslint-disable-next-line @typescript-eslint/no-deprecated
23135
23475
  bitrate,
23476
+ frameRate,
23136
23477
  ...restOptions
23137
23478
  } = options;
23138
23479
  if (!VIDEO_CODECS.includes(codec)) {
@@ -23153,6 +23494,9 @@ var Mediabunny = (() => {
23153
23494
  if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
23154
23495
  throw new TypeError("bitrate must be a positive integer or a quality.");
23155
23496
  }
23497
+ if (frameRate !== void 0 && (!Number.isFinite(frameRate) || frameRate <= 0)) {
23498
+ throw new TypeError("frameRate, when provided, must be a finite positive number.");
23499
+ }
23156
23500
  validateVideoEncodingAdditionalOptions(codec, restOptions);
23157
23501
  const resolvedQuality = resolveQuality(quality, bitrate) ?? new Quality("medium");
23158
23502
  let candidates;
@@ -23162,7 +23506,7 @@ var Mediabunny = (() => {
23162
23506
  width,
23163
23507
  height,
23164
23508
  quality: resolvedQuality,
23165
- framerate: void 0,
23509
+ framerate: frameRate,
23166
23510
  ...restOptions,
23167
23511
  alpha: "discard"
23168
23512
  // Since we handle alpha ourselves
@@ -24086,11 +24430,12 @@ var Mediabunny = (() => {
24086
24430
  return decodedSampleQueueSize === 0 ? 40 : 8;
24087
24431
  };
24088
24432
  var VideoDecoderWrapper = class extends DecoderWrapper {
24089
- constructor(onSample, onError, codec, decoderConfig, rotation, timeResolution) {
24433
+ constructor(onSample, onError, codec, decoderConfig, rotation, flip, timeResolution) {
24090
24434
  super(onSample, onError);
24091
24435
  this.codec = codec;
24092
24436
  this.decoderConfig = decoderConfig;
24093
24437
  this.rotation = rotation;
24438
+ this.flip = flip;
24094
24439
  this.timeResolution = timeResolution;
24095
24440
  this.decoder = null;
24096
24441
  this.customDecoder = null;
@@ -24375,6 +24720,7 @@ var Mediabunny = (() => {
24375
24720
  sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution);
24376
24721
  sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution);
24377
24722
  sample.setRotation(this.rotation);
24723
+ sample.setFlip(this.flip);
24378
24724
  this.onSample(sample);
24379
24725
  }
24380
24726
  async mergeAlpha(color, alpha) {
@@ -24709,6 +25055,7 @@ var Mediabunny = (() => {
24709
25055
  }
24710
25056
  const codec = await this._track.getCodec();
24711
25057
  const rotation = await this._track.getRotation();
25058
+ const flip = await this._track.getFlip();
24712
25059
  let decoderConfig = await this._track.getDecoderConfig();
24713
25060
  const timeResolution = await this._track.getTimeResolution();
24714
25061
  assert(codec && decoderConfig);
@@ -24717,7 +25064,7 @@ var Mediabunny = (() => {
24717
25064
  hardwareAcceleration: this._decoderOptions.hardwareAcceleration,
24718
25065
  optimizeForLatency: this._decoderOptions.optimizeForLatency
24719
25066
  };
24720
- return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution);
25067
+ return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, flip, timeResolution);
24721
25068
  }
24722
25069
  /** @internal */
24723
25070
  _createPacketSink() {
@@ -24771,6 +25118,8 @@ var Mediabunny = (() => {
24771
25118
  /** @internal */
24772
25119
  this._rotation = 0;
24773
25120
  /** @internal */
25121
+ this._flip = false;
25122
+ /** @internal */
24774
25123
  this._initPromise = null;
24775
25124
  /** @internal */
24776
25125
  this._nextCanvasIndex = 0;
@@ -24800,6 +25149,9 @@ var Mediabunny = (() => {
24800
25149
  if (options.rotation !== void 0 && ![0, 90, 180, 270].includes(options.rotation)) {
24801
25150
  throw new TypeError("options.rotation, when provided, must be 0, 90, 180 or 270.");
24802
25151
  }
25152
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
25153
+ throw new TypeError("options.flip, when provided, must be a boolean.");
25154
+ }
24803
25155
  if (options.crop !== void 0) {
24804
25156
  validateCropRectangle(options.crop, "options.");
24805
25157
  }
@@ -24822,6 +25174,7 @@ var Mediabunny = (() => {
24822
25174
  const options = this._options;
24823
25175
  const videoTrack = this._videoTrack;
24824
25176
  const rotation = options.rotation ?? await videoTrack.getRotation();
25177
+ const flip = options.flip ?? await videoTrack.getFlip();
24825
25178
  const squarePixelWidth = await videoTrack.getSquarePixelWidth();
24826
25179
  const squarePixelHeight = await videoTrack.getSquarePixelHeight();
24827
25180
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [squarePixelWidth, squarePixelHeight] : [squarePixelHeight, squarePixelWidth];
@@ -24844,6 +25197,7 @@ var Mediabunny = (() => {
24844
25197
  this._width = width;
24845
25198
  this._height = height;
24846
25199
  this._rotation = rotation;
25200
+ this._flip = flip;
24847
25201
  this._crop = crop;
24848
25202
  })();
24849
25203
  }
@@ -24877,6 +25231,7 @@ var Mediabunny = (() => {
24877
25231
  sample._drawWithFitAndMipmapping(canvas, context, {
24878
25232
  fit: this._fit,
24879
25233
  rotation: this._rotation,
25234
+ flip: this._flip,
24880
25235
  crop: this._crop,
24881
25236
  targetIsFresh: canvasIsNew,
24882
25237
  fillBlack: !this._alpha && isFirefox()
@@ -25742,16 +26097,38 @@ var Mediabunny = (() => {
25742
26097
  get codedHeight() {
25743
26098
  return requireSync(this._backing.getCodedHeight(), "codedHeight", "getCodedHeight");
25744
26099
  }
25745
- /** Returns the angle in degrees by which the track's frames should be rotated (clockwise). */
26100
+ /**
26101
+ * Returns the row-major 3x3 affine transformation matrix that is used to transform the raw video frames before they
26102
+ * are displayed. The raw frames are assumed to be placed with their top-left corner at the origin (0,0).
26103
+ *
26104
+ * Usually, this matrix models a rotation and a flip. Depending on the file, it may also contain a translation
26105
+ * component. When displaying video, you should typically ignore the translation component and display the center of
26106
+ * the transformed frame in the center of the viewport.
26107
+ */
26108
+ async getTransformationMatrix() {
26109
+ return this._backing.getTransformationMatrix();
26110
+ }
26111
+ /**
26112
+ * Returns the angle in degrees by which the track's frames should be rotated (clockwise). The rotation is
26113
+ * applied before flipping.
26114
+ */
25746
26115
  async getRotation() {
25747
- return this._backing.getRotation();
26116
+ return extractRotationFromMatrix(await this._backing.getTransformationMatrix());
25748
26117
  }
25749
26118
  /**
25750
26119
  * The angle in degrees by which the track's frames should be rotated (clockwise).
25751
26120
  * @deprecated Use {@link InputVideoTrack.getRotation} instead.
25752
26121
  */
25753
26122
  get rotation() {
25754
- return requireSync(this._backing.getRotation(), "rotation", "getRotation");
26123
+ return extractRotationFromMatrix(
26124
+ requireSync(this._backing.getTransformationMatrix(), "rotation", "getRotation")
26125
+ );
26126
+ }
26127
+ /**
26128
+ * Returns whether the track's frames should be flipped horizontally (about the vertical axis), after rotation.
26129
+ */
26130
+ async getFlip() {
26131
+ return matrixIsFlipped(await this._backing.getTransformationMatrix());
25755
26132
  }
25756
26133
  /**
25757
26134
  * Returns the width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation.
@@ -25821,7 +26198,9 @@ var Mediabunny = (() => {
25821
26198
  return metadata;
25822
26199
  }
25823
26200
  }
25824
- const rotation = requireSync(this._backing.getRotation(), "displayWidth", "getDisplayWidth");
26201
+ const rotation = extractRotationFromMatrix(
26202
+ requireSync(this._backing.getTransformationMatrix(), "displayWidth", "getDisplayWidth")
26203
+ );
25825
26204
  const value = rotation % 180 === 0 ? this._backing.getSquarePixelWidth() : this._backing.getSquarePixelHeight();
25826
26205
  return requireSync(value, "displayWidth", "getDisplayWidth");
25827
26206
  }
@@ -25846,7 +26225,9 @@ var Mediabunny = (() => {
25846
26225
  return metadata;
25847
26226
  }
25848
26227
  }
25849
- const rotation = requireSync(this._backing.getRotation(), "displayHeight", "getDisplayHeight");
26228
+ const rotation = extractRotationFromMatrix(
26229
+ requireSync(this._backing.getTransformationMatrix(), "displayHeight", "getDisplayHeight")
26230
+ );
25850
26231
  const value = rotation % 180 === 0 ? this._backing.getSquarePixelHeight() : this._backing.getSquarePixelWidth();
25851
26232
  return requireSync(value, "displayHeight", "getDisplayHeight");
25852
26233
  }
@@ -28617,23 +28998,6 @@ var Mediabunny = (() => {
28617
28998
  if (nullTerminated) bytes2.push(0);
28618
28999
  return bytes2;
28619
29000
  };
28620
- var rotationMatrix = (rotationInDegrees) => {
28621
- const theta = rotationInDegrees * (Math.PI / 180);
28622
- const cosTheta = Math.round(Math.cos(theta));
28623
- const sinTheta = Math.round(Math.sin(theta));
28624
- return [
28625
- cosTheta,
28626
- sinTheta,
28627
- 0,
28628
- -sinTheta,
28629
- cosTheta,
28630
- 0,
28631
- 0,
28632
- 0,
28633
- 1
28634
- ];
28635
- };
28636
- var IDENTITY_MATRIX = /* @__PURE__ */ rotationMatrix(0);
28637
29001
  var matrixToBytes = (matrix) => {
28638
29002
  return [
28639
29003
  fixed_16_16(matrix[0]),
@@ -28793,26 +29157,6 @@ var Mediabunny = (() => {
28793
29157
  // Next track ID
28794
29158
  ]);
28795
29159
  };
28796
- var presentationSpan = (trackData) => {
28797
- if (trackData.samples.length === 0) {
28798
- return 0;
28799
- }
28800
- let minTimestamp = Infinity;
28801
- let maxEndTimestamp = -Infinity;
28802
- for (let i = 0; i < trackData.samples.length; i++) {
28803
- const sample = trackData.samples[i];
28804
- if (sample.timestamp < minTimestamp) {
28805
- minTimestamp = sample.timestamp;
28806
- }
28807
- if (sample.timestamp + sample.duration > maxEndTimestamp) {
28808
- maxEndTimestamp = sample.timestamp + sample.duration;
28809
- }
28810
- }
28811
- if (minTimestamp === Infinity) {
28812
- return 0;
28813
- }
28814
- return maxEndTimestamp - minTimestamp;
28815
- };
28816
29160
  var trak = (trackData, creationTime) => {
28817
29161
  const trackMetadata = getTrackMetadata(trackData);
28818
29162
  const needsEditList = trackData.startTimestampOffset !== null && trackData.startTimestampOffset !== 0;
@@ -28836,9 +29180,15 @@ var Mediabunny = (() => {
28836
29180
  const needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
28837
29181
  const u32OrU64 = needsU64 ? u64 : u32;
28838
29182
  let matrix;
28839
- if (trackData.type === "video") {
28840
- const rotation = trackData.track.metadata.rotation;
28841
- matrix = rotationMatrix(rotation ?? 0);
29183
+ if (trackData.type === "video" && trackData.track.metadata.transformationMatrix) {
29184
+ matrix = trackData.track.metadata.transformationMatrix;
29185
+ } else if (trackData.type === "video") {
29186
+ const { rotation, flip } = trackData.track.metadata;
29187
+ const linear = multiplyMatrices(
29188
+ rotationMatrix(rotation ?? 0),
29189
+ scaleMatrix(flip ? -1 : 1, 1)
29190
+ );
29191
+ matrix = centeredTransformationMatrix(linear, trackData.info.width, trackData.info.height);
28842
29192
  } else {
28843
29193
  matrix = IDENTITY_MATRIX;
28844
29194
  }
@@ -29097,8 +29447,22 @@ var Mediabunny = (() => {
29097
29447
  ], [
29098
29448
  VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec]?.(trackData) ?? null,
29099
29449
  pasp(trackData),
29100
- colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace) ? null : colr(trackData)
29450
+ colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace) ? null : colr(trackData),
29451
+ btrt(trackData)
29101
29452
  ]);
29453
+ var btrt = (trackData) => {
29454
+ if (trackData.avgBitrate === 0 && trackData.maxBitrate === 0) {
29455
+ return null;
29456
+ }
29457
+ return box("btrt", [
29458
+ u32(0),
29459
+ // Decoding buffer size (unknown)
29460
+ u32(trackData.maxBitrate),
29461
+ // Max bitrate
29462
+ u32(trackData.avgBitrate)
29463
+ // Average bitrate
29464
+ ]);
29465
+ };
29102
29466
  var pasp = (trackData) => {
29103
29467
  if (trackData.info.pixelAspectRatio.num === trackData.info.pixelAspectRatio.den) {
29104
29468
  return null;
@@ -29252,7 +29616,8 @@ var Mediabunny = (() => {
29252
29616
  ];
29253
29617
  }
29254
29618
  return box(compressionType, contents, [
29255
- audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null
29619
+ audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null,
29620
+ btrt(trackData)
29256
29621
  ]);
29257
29622
  };
29258
29623
  var esds = (trackData) => {
@@ -29286,9 +29651,9 @@ var Mediabunny = (() => {
29286
29651
  // stream type(6bits)=5 audio, flags(2bits)=1
29287
29652
  ...u24(0),
29288
29653
  // 24bit buffer size
29289
- ...u32(0),
29654
+ ...u32(trackData.maxBitrate),
29290
29655
  // max bitrate
29291
- ...u32(0)
29656
+ ...u32(trackData.avgBitrate)
29292
29657
  // avg bitrate
29293
29658
  ];
29294
29659
  if (trackData.info.decoderConfig.description) {
@@ -29476,7 +29841,8 @@ var Mediabunny = (() => {
29476
29841
  u16(1)
29477
29842
  // Data reference index
29478
29843
  ], [
29479
- SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData)
29844
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
29845
+ btrt(trackData)
29480
29846
  ]);
29481
29847
  var vttC = (trackData) => box("vttC", [
29482
29848
  ...textEncoder.encode(trackData.info.config.description)
@@ -30911,6 +31277,26 @@ var Mediabunny = (() => {
30911
31277
  const value = timeInSeconds * timescale;
30912
31278
  return round ? Math.round(value) : value;
30913
31279
  };
31280
+ var presentationSpan = (trackData) => {
31281
+ if (trackData.samples.length === 0) {
31282
+ return 0;
31283
+ }
31284
+ let minTimestamp = Infinity;
31285
+ let maxEndTimestamp = -Infinity;
31286
+ for (let i = 0; i < trackData.samples.length; i++) {
31287
+ const sample = trackData.samples[i];
31288
+ if (sample.timestamp < minTimestamp) {
31289
+ minTimestamp = sample.timestamp;
31290
+ }
31291
+ if (sample.timestamp + sample.duration > maxEndTimestamp) {
31292
+ maxEndTimestamp = sample.timestamp + sample.duration;
31293
+ }
31294
+ }
31295
+ if (minTimestamp === Infinity) {
31296
+ return 0;
31297
+ }
31298
+ return maxEndTimestamp - minTimestamp;
31299
+ };
30914
31300
  var IsobmffMuxer2 = class extends Muxer {
30915
31301
  constructor(output, format) {
30916
31302
  super(output);
@@ -31124,7 +31510,9 @@ var Mediabunny = (() => {
31124
31510
  finalizedChunks: [],
31125
31511
  currentChunk: null,
31126
31512
  compactlyCodedChunkTable: [],
31127
- closed: false
31513
+ closed: false,
31514
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31515
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0
31128
31516
  };
31129
31517
  this.trackDatas.push(newTrackData);
31130
31518
  this.trackDatas.sort((a, b) => a.track.id - b.track.id);
@@ -31198,7 +31586,9 @@ var Mediabunny = (() => {
31198
31586
  finalizedChunks: [],
31199
31587
  currentChunk: null,
31200
31588
  compactlyCodedChunkTable: [],
31201
- closed: false
31589
+ closed: false,
31590
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31591
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0
31202
31592
  };
31203
31593
  this.trackDatas.push(newTrackData);
31204
31594
  this.trackDatas.sort((a, b) => a.track.id - b.track.id);
@@ -31236,6 +31626,8 @@ var Mediabunny = (() => {
31236
31626
  currentChunk: null,
31237
31627
  compactlyCodedChunkTable: [],
31238
31628
  closed: false,
31629
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31630
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0,
31239
31631
  lastCueEndTimestamp: null,
31240
31632
  cueQueue: [],
31241
31633
  nextSourceId: 0,
@@ -31901,6 +32293,29 @@ var Mediabunny = (() => {
31901
32293
  } else {
31902
32294
  for (const trackData of this.trackDatas) {
31903
32295
  await this.finalizeCurrentChunk(trackData);
32296
+ const span = presentationSpan(trackData);
32297
+ if (span > 0) {
32298
+ let totalBytes = 0;
32299
+ for (const sample of trackData.samples) {
32300
+ totalBytes += sample.size;
32301
+ }
32302
+ trackData.avgBitrate = Math.round(8 * totalBytes / span);
32303
+ } else {
32304
+ trackData.avgBitrate = 0;
32305
+ }
32306
+ let windowStart = 0;
32307
+ let windowBytes = 0;
32308
+ let maxWindowBytes = 0;
32309
+ for (let i = 0; i < trackData.samples.length; i++) {
32310
+ const sample = trackData.samples[i];
32311
+ windowBytes += sample.size;
32312
+ while (sample.decodeTimestamp - trackData.samples[windowStart].decodeTimestamp >= 1) {
32313
+ windowBytes -= trackData.samples[windowStart].size;
32314
+ windowStart++;
32315
+ }
32316
+ maxWindowBytes = Math.max(maxWindowBytes, windowBytes);
32317
+ }
32318
+ trackData.maxBitrate = 8 * maxWindowBytes;
31904
32319
  if (trackData.startTimestampOffset !== null) {
31905
32320
  for (let i = 0; i < trackData.samples.length; i++) {
31906
32321
  const sample = trackData.samples[i];
@@ -32199,14 +32614,34 @@ var Mediabunny = (() => {
32199
32614
  }
32200
32615
  }
32201
32616
  videoSpecificTrackInfo(trackData) {
32202
- const { frameRate, rotation } = trackData.track.metadata;
32617
+ const { frameRate, transformationMatrix } = trackData.track.metadata;
32203
32618
  const elements = [
32204
32619
  frameRate ? {
32205
32620
  id: 2352003 /* DefaultDuration */,
32206
32621
  data: 1e9 / frameRate
32207
32622
  } : null
32208
32623
  ];
32209
- const flippedRotation = rotation ? normalizeRotation(-rotation) : 0;
32624
+ let rotation;
32625
+ let horizontalScale;
32626
+ let verticalScale;
32627
+ if (transformationMatrix) {
32628
+ rotation = extractRotationFromMatrix(transformationMatrix);
32629
+ const unrotated = multiplyMatrices(rotationMatrix(-rotation), transformationMatrix);
32630
+ horizontalScale = Math.sign(unrotated[0]);
32631
+ verticalScale = Math.sign(unrotated[4]);
32632
+ if (verticalScale === -1) {
32633
+ verticalScale = 1;
32634
+ horizontalScale = -horizontalScale;
32635
+ rotation = normalizeRotation(rotation + 180);
32636
+ }
32637
+ } else {
32638
+ rotation = trackData.track.metadata.rotation ?? 0;
32639
+ horizontalScale = trackData.track.metadata.flip ? -1 : 1;
32640
+ verticalScale = 1;
32641
+ }
32642
+ const roll = rotation ? normalizeRotation(horizontalScale === -1 ? rotation : -rotation) : 0;
32643
+ const yaw = Math.round(Math.acos(horizontalScale) * RAD_TO_DEG);
32644
+ const pitch = Math.round(Math.acos(verticalScale) * RAD_TO_DEG);
32210
32645
  const hasNonSquarePixelAspectRatio = !!trackData.info.aspectRatio && trackData.info.aspectRatio.num * trackData.info.height !== trackData.info.aspectRatio.den * trackData.info.width;
32211
32646
  const colorSpace = trackData.info.decoderConfig.colorSpace;
32212
32647
  const videoElement = { id: 224 /* Video */, data: [
@@ -32238,7 +32673,7 @@ var Mediabunny = (() => {
32238
32673
  }
32239
32674
  ]
32240
32675
  },
32241
- flippedRotation ? {
32676
+ roll || yaw || pitch ? {
32242
32677
  id: 30320 /* Projection */,
32243
32678
  data: [
32244
32679
  {
@@ -32246,11 +32681,19 @@ var Mediabunny = (() => {
32246
32681
  data: 0
32247
32682
  // rectangular
32248
32683
  },
32249
- {
32684
+ yaw ? {
32685
+ id: 30323 /* ProjectionPoseYaw */,
32686
+ data: new EBMLFloat32(yaw)
32687
+ } : null,
32688
+ pitch ? {
32689
+ id: 30324 /* ProjectionPosePitch */,
32690
+ data: new EBMLFloat32(pitch)
32691
+ } : null,
32692
+ roll ? {
32250
32693
  id: 30325 /* ProjectionPoseRoll */,
32251
- data: new EBMLFloat32((flippedRotation + 180) % 360 - 180)
32694
+ data: new EBMLFloat32((roll + 180) % 360 - 180)
32252
32695
  // [0, 270] -> [-180, 90]
32253
- }
32696
+ } : null
32254
32697
  ]
32255
32698
  } : null
32256
32699
  ] };
@@ -34213,7 +34656,7 @@ ${cue.notes ?? ""}`;
34213
34656
  this.writer.write(this.helper);
34214
34657
  }
34215
34658
  writeAscii(text) {
34216
- this.writer.write(new TextEncoder().encode(text));
34659
+ this.writer.write(textEncoder.encode(text));
34217
34660
  }
34218
34661
  };
34219
34662
 
@@ -34697,6 +35140,11 @@ ${cue.notes ?? ""}`;
34697
35140
  this._closingPromise = null;
34698
35141
  /** @internal */
34699
35142
  this._closed = false;
35143
+ /**
35144
+ * Set when the encoder is configured with a bitrate.
35145
+ * @internal
35146
+ */
35147
+ this._nominalBitrate = null;
34700
35148
  }
34701
35149
  /** @internal */
34702
35150
  _ensureValidAdd() {
@@ -34873,7 +35321,7 @@ ${cue.notes ?? ""}`;
34873
35321
  this.codedWidth = videoSample.codedWidth;
34874
35322
  this.codedHeight = videoSample.codedHeight;
34875
35323
  }
34876
- const hasTransformConfig = config.transform?.width !== void 0 || config.transform?.height !== void 0 || config.transform?.rotate !== void 0 || config.transform?.crop !== void 0 || config.transform?.force === true;
35324
+ const hasTransformConfig = config.transform?.width !== void 0 || config.transform?.height !== void 0 || config.transform?.rotate !== void 0 || config.transform?.flip !== void 0 || config.transform?.crop !== void 0 || config.transform?.force === true;
34877
35325
  const needsTransform = hasTransformConfig || isSizeChange && sizeChangeBehavior !== "passThrough";
34878
35326
  if (needsTransform) {
34879
35327
  let targetWidth = config.transform?.width;
@@ -34893,6 +35341,7 @@ ${cue.notes ?? ""}`;
34893
35341
  roundDimensionsTo: 2,
34894
35342
  crop: config.transform?.crop,
34895
35343
  rotate: config.transform?.rotate,
35344
+ flip: config.transform?.flip,
34896
35345
  fit: appliedFit,
34897
35346
  alpha: config.alpha
34898
35347
  });
@@ -35187,6 +35636,8 @@ ${cue.notes ?? ""}`;
35187
35636
  this.encodingConfig.codec,
35188
35637
  selected.quantizer
35189
35638
  );
35639
+ } else {
35640
+ this.source._nominalBitrate = encoderConfig.bitrate ?? null;
35190
35641
  }
35191
35642
  if (MatchingCustomEncoder) {
35192
35643
  this.customEncoder = new MatchingCustomEncoder();
@@ -35857,39 +36308,42 @@ ${cue.notes ?? ""}`;
35857
36308
  }
35858
36309
  /** @internal */
35859
36310
  async _flushAndClose(forceClose) {
35860
- if (this._abortController) {
35861
- this._abortController.abort();
35862
- this._abortController = null;
35863
- }
35864
- if (this._timerHandle) {
35865
- clearIntervalUnthrottled(this._timerHandle);
35866
- }
35867
- this._lastVideoFrame?.close();
35868
- if (this._videoElement) {
35869
- this._videoElement.srcObject = null;
35870
- this._videoElement.remove();
35871
- this._videoElement = null;
35872
- }
35873
- if (this._workerTrackId !== null) {
35874
- assert(this._workerListener);
35875
- sendMessageToMediaStreamTrackProcessorWorker({
35876
- type: "stopTrack",
35877
- trackId: this._workerTrackId
35878
- });
35879
- await new Promise((resolve) => {
35880
- const listener = (event) => {
35881
- const message = event.data;
35882
- if (message.type === "trackStopped" && message.trackId === this._workerTrackId) {
35883
- assert(this._workerListener);
35884
- mediaStreamTrackProcessorWorker.removeEventListener("message", this._workerListener);
35885
- mediaStreamTrackProcessorWorker.removeEventListener("message", listener);
35886
- resolve();
35887
- }
35888
- };
35889
- mediaStreamTrackProcessorWorker.addEventListener("message", listener);
35890
- });
36311
+ try {
36312
+ if (this._abortController) {
36313
+ this._abortController.abort();
36314
+ this._abortController = null;
36315
+ }
36316
+ if (this._timerHandle) {
36317
+ clearIntervalUnthrottled(this._timerHandle);
36318
+ }
36319
+ this._lastVideoFrame?.close();
36320
+ if (this._videoElement) {
36321
+ this._videoElement.srcObject = null;
36322
+ this._videoElement.remove();
36323
+ this._videoElement = null;
36324
+ }
36325
+ if (this._workerTrackId !== null) {
36326
+ assert(this._workerListener);
36327
+ sendMessageToMediaStreamTrackProcessorWorker({
36328
+ type: "stopTrack",
36329
+ trackId: this._workerTrackId
36330
+ });
36331
+ await new Promise((resolve) => {
36332
+ const listener = (event) => {
36333
+ const message = event.data;
36334
+ if (message.type === "trackStopped" && message.trackId === this._workerTrackId) {
36335
+ assert(this._workerListener);
36336
+ mediaStreamTrackProcessorWorker.removeEventListener("message", this._workerListener);
36337
+ mediaStreamTrackProcessorWorker.removeEventListener("message", listener);
36338
+ resolve();
36339
+ }
36340
+ };
36341
+ mediaStreamTrackProcessorWorker.addEventListener("message", listener);
36342
+ });
36343
+ }
36344
+ } finally {
36345
+ await this._encoder.flushAndClose(forceClose);
35891
36346
  }
35892
- await this._encoder.flushAndClose(forceClose);
35893
36347
  }
35894
36348
  };
35895
36349
  var AudioSource = class extends MediaSource {
@@ -36192,6 +36646,7 @@ ${cue.notes ?? ""}`;
36192
36646
  quality
36193
36647
  });
36194
36648
  this.encodingConfig.onEncoderConfig?.(encoderConfig);
36649
+ this.source._nominalBitrate = encoderConfig.bitrate ?? null;
36195
36650
  const MatchingCustomEncoder = customAudioEncoders.find((x) => x.supports(
36196
36651
  this.encodingConfig.codec,
36197
36652
  encoderConfig
@@ -36636,16 +37091,19 @@ ${cue.notes ?? ""}`;
36636
37091
  }
36637
37092
  /** @internal */
36638
37093
  async _flushAndClose(forceClose) {
36639
- if (this._abortController) {
36640
- this._abortController.abort();
36641
- this._abortController = null;
36642
- }
36643
- if (this._audioContext) {
36644
- assert(this._scriptProcessorNode);
36645
- this._scriptProcessorNode.disconnect();
36646
- await this._audioContext.suspend();
37094
+ try {
37095
+ if (this._abortController) {
37096
+ this._abortController.abort();
37097
+ this._abortController = null;
37098
+ }
37099
+ if (this._audioContext) {
37100
+ assert(this._scriptProcessorNode);
37101
+ this._scriptProcessorNode.disconnect();
37102
+ await this._audioContext.suspend();
37103
+ }
37104
+ } finally {
37105
+ await this._encoder.flushAndClose(forceClose);
36647
37106
  }
36648
- await this._encoder.flushAndClose(forceClose);
36649
37107
  }
36650
37108
  };
36651
37109
  var mediaStreamTrackProcessorWorkerCode = () => {
@@ -37022,13 +37480,13 @@ ${cue.notes ?? ""}`;
37022
37480
  const codecs = [];
37023
37481
  let videoCount = 0;
37024
37482
  let audioCount = 0;
37025
- let requiresRotationMetadata = false;
37483
+ let requiresTransformationMetadata = false;
37026
37484
  let candidate = null;
37027
37485
  let candidateScore = -Infinity;
37028
37486
  for (const track of tracks) {
37029
37487
  if (track.isVideoTrack()) {
37030
37488
  videoCount++;
37031
- requiresRotationMetadata ||= (track.metadata.rotation ?? 0) !== 0;
37489
+ requiresTransformationMetadata ||= (track.metadata.rotation ?? 0) !== 0 || !!track.metadata.flip || !!track.metadata.transformationMatrix;
37032
37490
  } else if (track.isAudioTrack()) {
37033
37491
  audioCount++;
37034
37492
  }
@@ -37047,7 +37505,7 @@ ${cue.notes ?? ""}`;
37047
37505
  continue;
37048
37506
  }
37049
37507
  let score = 0;
37050
- if (requiresRotationMetadata && format.supportsVideoRotationMetadata) {
37508
+ if (requiresTransformationMetadata && format.supportsVideoTransformationMetadata) {
37051
37509
  score++;
37052
37510
  }
37053
37511
  if (score > candidateScore) {
@@ -37975,6 +38433,13 @@ ${cue.notes ?? ""}`;
37975
38433
 
37976
38434
  // src/output-format.ts
37977
38435
  var OutputFormat = class {
38436
+ /**
38437
+ * Whether this output format supports video rotation metadata.
38438
+ * @deprecated Use {@link OutputFormat.supportsVideoTransformationMetadata} instead.
38439
+ */
38440
+ get supportsVideoRotationMetadata() {
38441
+ return this.supportsVideoTransformationMetadata;
38442
+ }
37978
38443
  /** Returns a list of video codecs that this output format can contain. */
37979
38444
  getSupportedVideoCodecs() {
37980
38445
  return this.getSupportedCodecs().filter((codec) => VIDEO_CODECS.includes(codec));
@@ -38040,7 +38505,7 @@ ${cue.notes ?? ""}`;
38040
38505
  total: { min: 0, max }
38041
38506
  };
38042
38507
  }
38043
- get supportsVideoRotationMetadata() {
38508
+ get supportsVideoTransformationMetadata() {
38044
38509
  return true;
38045
38510
  }
38046
38511
  get supportsTimestampedMediaData() {
@@ -38217,7 +38682,7 @@ ${cue.notes ?? ""}`;
38217
38682
  ...SUBTITLE_CODECS
38218
38683
  ];
38219
38684
  }
38220
- get supportsVideoRotationMetadata() {
38685
+ get supportsVideoTransformationMetadata() {
38221
38686
  return false;
38222
38687
  }
38223
38688
  get supportsTimestampedMediaData() {
@@ -38297,7 +38762,7 @@ ${cue.notes ?? ""}`;
38297
38762
  getSupportedCodecs() {
38298
38763
  return ["mp3"];
38299
38764
  }
38300
- get supportsVideoRotationMetadata() {
38765
+ get supportsVideoTransformationMetadata() {
38301
38766
  return false;
38302
38767
  }
38303
38768
  get supportsTimestampedMediaData() {
@@ -38354,7 +38819,7 @@ ${cue.notes ?? ""}`;
38354
38819
  )
38355
38820
  ];
38356
38821
  }
38357
- get supportsVideoRotationMetadata() {
38822
+ get supportsVideoTransformationMetadata() {
38358
38823
  return false;
38359
38824
  }
38360
38825
  get supportsTimestampedMediaData() {
@@ -38407,7 +38872,7 @@ ${cue.notes ?? ""}`;
38407
38872
  ...AUDIO_CODECS.filter((codec) => ["vorbis", "opus"].includes(codec))
38408
38873
  ];
38409
38874
  }
38410
- get supportsVideoRotationMetadata() {
38875
+ get supportsVideoTransformationMetadata() {
38411
38876
  return false;
38412
38877
  }
38413
38878
  get supportsTimestampedMediaData() {
@@ -38454,7 +38919,7 @@ ${cue.notes ?? ""}`;
38454
38919
  getSupportedCodecs() {
38455
38920
  return ["aac"];
38456
38921
  }
38457
- get supportsVideoRotationMetadata() {
38922
+ get supportsVideoTransformationMetadata() {
38458
38923
  return false;
38459
38924
  }
38460
38925
  get supportsTimestampedMediaData() {
@@ -38501,7 +38966,7 @@ ${cue.notes ?? ""}`;
38501
38966
  getSupportedCodecs() {
38502
38967
  return ["flac"];
38503
38968
  }
38504
- get supportsVideoRotationMetadata() {
38969
+ get supportsVideoTransformationMetadata() {
38505
38970
  return false;
38506
38971
  }
38507
38972
  get supportsTimestampedMediaData() {
@@ -38554,7 +39019,7 @@ ${cue.notes ?? ""}`;
38554
39019
  ...AUDIO_CODECS.filter((codec) => ["aac", "mp3", "ac3", "eac3", "dts"].includes(codec))
38555
39020
  ];
38556
39021
  }
38557
- get supportsVideoRotationMetadata() {
39022
+ get supportsVideoTransformationMetadata() {
38558
39023
  return false;
38559
39024
  }
38560
39025
  get supportsTimestampedMediaData() {
@@ -38650,8 +39115,8 @@ ${cue.notes ?? ""}`;
38650
39115
  total: { min: 0, max: Infinity }
38651
39116
  };
38652
39117
  }
38653
- get supportsVideoRotationMetadata() {
38654
- return toArray(this._options.segmentFormat).some((format) => format.supportsVideoRotationMetadata);
39118
+ get supportsVideoTransformationMetadata() {
39119
+ return toArray(this._options.segmentFormat).some((format) => format.supportsVideoTransformationMetadata);
38655
39120
  }
38656
39121
  get supportsTimestampedMediaData() {
38657
39122
  return true;
@@ -38784,6 +39249,12 @@ ${cue.notes ?? ""}`;
38784
39249
  if (metadata.maximumPacketCount !== void 0 && (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)) {
38785
39250
  throw new TypeError("metadata.maximumPacketCount, when provided, must be a non-negative integer.");
38786
39251
  }
39252
+ if (metadata.bitrate !== void 0 && (!Number.isFinite(metadata.bitrate) || metadata.bitrate < 0)) {
39253
+ throw new TypeError("metadata.bitrate, when provided, must be a non-negative number.");
39254
+ }
39255
+ if (metadata.averageBitrate !== void 0 && (!Number.isFinite(metadata.averageBitrate) || metadata.averageBitrate < 0)) {
39256
+ throw new TypeError("metadata.averageBitrate, when provided, must be a non-negative number.");
39257
+ }
38787
39258
  if (metadata.group !== void 0 && !(metadata.group instanceof OutputTrackGroup) && (!Array.isArray(metadata.group) || metadata.group.some((group) => !(group instanceof OutputTrackGroup)))) {
38788
39259
  throw new TypeError(
38789
39260
  "metadata.group, when provided, must be an OutputTrackGroup instance or an array of OutputTrackGroup instances."
@@ -38983,8 +39454,11 @@ ${cue.notes ?? ""}`;
38983
39454
  if (metadata.rotation !== void 0 && ![0, 90, 180, 270].includes(metadata.rotation)) {
38984
39455
  throw new TypeError(`Invalid video rotation: ${metadata.rotation}. Has to be 0, 90, 180 or 270.`);
38985
39456
  }
38986
- if (!this.format.supportsVideoRotationMetadata && metadata.rotation) {
38987
- throw new Error(`${this.format._name} does not support video rotation metadata.`);
39457
+ if (metadata.flip !== void 0 && typeof metadata.flip !== "boolean") {
39458
+ throw new TypeError("metadata.flip, when provided, must be a boolean.");
39459
+ }
39460
+ if (metadata.transformationMatrix !== void 0 && (!Array.isArray(metadata.transformationMatrix) || metadata.transformationMatrix.length !== 9 || !metadata.transformationMatrix.every((x) => Number.isFinite(x)))) {
39461
+ throw new TypeError("metadata.transformationMatrix, when provided, must be an array of 9 finite numbers.");
38988
39462
  }
38989
39463
  if (metadata.frameRate !== void 0 && (!Number.isFinite(metadata.frameRate) || metadata.frameRate <= 0)) {
38990
39464
  throw new TypeError(
@@ -39210,9 +39684,8 @@ ${cue.notes ?? ""}`;
39210
39684
  * @returns A promise that resolves once all internal resources have been released.
39211
39685
  */
39212
39686
  async cancel() {
39213
- if (this._cancelPromise) {
39214
- Logging._warn("Output has already been canceled.");
39215
- return this._cancelPromise;
39687
+ if (this.state === "canceled") {
39688
+ return this._cancelPromise ?? void 0;
39216
39689
  } else if (this.state === "finalizing" || this.state === "finalized") {
39217
39690
  if (this.state === "finalized") {
39218
39691
  Logging._warn("Output has already been finalized.");
@@ -39265,6 +39738,9 @@ ${cue.notes ?? ""}`;
39265
39738
  await this._onFinalize();
39266
39739
  }
39267
39740
  this.state = "finalized";
39741
+ } catch (error) {
39742
+ this.state = "canceled";
39743
+ throw error;
39268
39744
  } finally {
39269
39745
  await Promise.all([...this._unfinalizedTargets].map((target) => target._close().catch(() => {
39270
39746
  })));
@@ -39318,6 +39794,12 @@ ${cue.notes ?? ""}`;
39318
39794
  if (videoOptions?.rotate !== void 0 && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
39319
39795
  throw new TypeError("options.video.rotate, when provided, must be 0, 90, 180 or 270.");
39320
39796
  }
39797
+ if (videoOptions?.flip !== void 0 && typeof videoOptions.flip !== "boolean") {
39798
+ throw new TypeError("options.video.flip, when provided, must be a boolean.");
39799
+ }
39800
+ if (videoOptions?.allowTransformationMetadata !== void 0 && typeof videoOptions.allowTransformationMetadata !== "boolean") {
39801
+ throw new TypeError("options.video.allowTransformationMetadata, when provided, must be a boolean.");
39802
+ }
39321
39803
  if (videoOptions?.allowRotationMetadata !== void 0 && typeof videoOptions.allowRotationMetadata !== "boolean") {
39322
39804
  throw new TypeError("options.video.allowRotationMetadata, when provided, must be a boolean.");
39323
39805
  }
@@ -39987,9 +40469,17 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39987
40469
  }
39988
40470
  let videoSource;
39989
40471
  const innateRotation = await track.getRotation();
39990
- const totalRotation = normalizeRotation(innateRotation + (trackOptions.rotate ?? 0));
40472
+ const innateFlip = await track.getFlip();
40473
+ const { rotation: totalRotation, flip: totalFlip } = composeRotationAndFlip(
40474
+ innateRotation,
40475
+ innateFlip,
40476
+ trackOptions.rotate ?? 0,
40477
+ trackOptions.flip ?? false
40478
+ );
39991
40479
  let outputTrackRotation = totalRotation;
39992
- const canUseRotationMetadata = this.output.format.supportsVideoRotationMetadata && (trackOptions.allowRotationMetadata ?? true);
40480
+ let outputTrackFlip = totalFlip;
40481
+ let outputTrackMatrix = trackOptions.rotate || trackOptions.flip ? null : await track.getTransformationMatrix();
40482
+ const canUseRotationMetadata = this.output.format.supportsVideoTransformationMetadata && (trackOptions.allowTransformationMetadata ?? trackOptions.allowRotationMetadata ?? true);
39993
40483
  const squarePixelWidth = await track.getSquarePixelWidth();
39994
40484
  const squarePixelHeight = await track.getSquarePixelHeight();
39995
40485
  const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0 ? [squarePixelWidth, squarePixelHeight] : [squarePixelHeight, squarePixelWidth];
@@ -40013,7 +40503,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40013
40503
  }
40014
40504
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
40015
40505
  const alpha = trackOptions.alpha ?? "discard";
40016
- let needsTranscode = !this._copyMode || !!trackOptions.forceTranscode || !!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;
40506
+ let needsTranscode = !this._copyMode || !!trackOptions.forceTranscode || !!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 || totalFlip && !canUseRotationMetadata || !!crop;
40017
40507
  let copyStartPacket = null;
40018
40508
  if (!needsTranscode) {
40019
40509
  const sink = new EncodedPacketSink(track);
@@ -40167,7 +40657,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40167
40657
  transform: {}
40168
40658
  };
40169
40659
  assert(encodingConfig.transform);
40170
- let needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== void 0) || !!crop || squarePixelWidth !== await track.getCodedWidth() || squarePixelHeight !== await track.getCodedHeight();
40660
+ let needsRerender = width !== originalWidth || height !== originalHeight || totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== void 0) || totalFlip && (!canUseRotationMetadata || trackOptions.process !== void 0) || !!crop || squarePixelWidth !== await track.getCodedWidth() || squarePixelHeight !== await track.getCodedHeight();
40171
40661
  if (!needsRerender) {
40172
40662
  var _stack = [];
40173
40663
  try {
@@ -40212,10 +40702,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40212
40702
  }
40213
40703
  if (needsRerender) {
40214
40704
  outputTrackRotation = 0;
40705
+ outputTrackFlip = false;
40706
+ outputTrackMatrix = null;
40215
40707
  encodingConfig.transform.width = width;
40216
40708
  encodingConfig.transform.height = height;
40217
40709
  encodingConfig.transform.fit = trackOptions.fit ?? "fill";
40218
- encodingConfig.transform.rotate = normalizeRotation(totalRotation - innateRotation);
40710
+ encodingConfig.transform.rotate = trackOptions.rotate;
40711
+ encodingConfig.transform.flip = trackOptions.flip;
40219
40712
  encodingConfig.transform.crop = crop;
40220
40713
  encodingConfig.transform.alpha = alpha;
40221
40714
  }
@@ -40267,6 +40760,8 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40267
40760
  const videoTrackLanguageCode = await track.getLanguageCode();
40268
40761
  const trackName = await track.getName();
40269
40762
  const trackDisposition = await track.getDisposition();
40763
+ const bitrate = needsTranscode ? null : await track.getBitrate();
40764
+ const averageBitrate = needsTranscode ? null : await track.getAverageBitrate();
40270
40765
  this.output.addVideoTrack(videoSource, {
40271
40766
  frameRate: trackOptions.frameRate,
40272
40767
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
@@ -40274,7 +40769,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40274
40769
  name: trackName ?? void 0,
40275
40770
  disposition: trackDisposition,
40276
40771
  rotation: outputTrackRotation,
40277
- group: ownGroup ?? trackOptions.group
40772
+ flip: outputTrackFlip,
40773
+ transformationMatrix: outputTrackMatrix ?? void 0,
40774
+ group: ownGroup ?? trackOptions.group,
40775
+ bitrate: bitrate ?? void 0,
40776
+ averageBitrate: averageBitrate ?? void 0
40278
40777
  });
40279
40778
  this.utilizedTracks.push(track);
40280
40779
  this._outputTrackIds.push(outputTrackId);
@@ -40558,12 +41057,16 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40558
41057
  const audioTrackLanguageCode = await track.getLanguageCode();
40559
41058
  const trackName = await track.getName();
40560
41059
  const trackDisposition = await track.getDisposition();
41060
+ const bitrate = needsTranscode ? null : await track.getBitrate();
41061
+ const averageBitrate = needsTranscode ? null : await track.getAverageBitrate();
40561
41062
  this.output.addAudioTrack(audioSource, {
40562
41063
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
40563
41064
  languageCode: isIso639Dash2LanguageCode(audioTrackLanguageCode) ? audioTrackLanguageCode : void 0,
40564
41065
  name: trackName ?? void 0,
40565
41066
  disposition: trackDisposition,
40566
- group: ownGroup ?? trackOptions.group
41067
+ group: ownGroup ?? trackOptions.group,
41068
+ bitrate: bitrate ?? void 0,
41069
+ averageBitrate: averageBitrate ?? void 0
40567
41070
  });
40568
41071
  this.utilizedTracks.push(track);
40569
41072
  this._outputTrackIds.push(outputTrackId);