mediabunny 1.56.3 → 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 (82) hide show
  1. package/dist/bundles/mediabunny.cjs +530 -178
  2. package/dist/bundles/mediabunny.min.cjs +13 -13
  3. package/dist/bundles/mediabunny.min.mjs +13 -13
  4. package/dist/bundles/mediabunny.mjs +530 -178
  5. package/dist/bundles/mediabunny.node.cjs +530 -178
  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/index.d.ts +1 -1
  19. package/dist/modules/src/index.d.ts.map +1 -1
  20. package/dist/modules/src/input-track.d.ts +21 -5
  21. package/dist/modules/src/input-track.d.ts.map +1 -1
  22. package/dist/modules/src/input-track.js +26 -6
  23. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -0
  24. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  25. package/dist/modules/src/isobmff/isobmff-boxes.js +26 -39
  26. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +6 -2
  27. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  28. package/dist/modules/src/isobmff/isobmff-demuxer.js +44 -30
  29. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +3 -0
  30. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  31. package/dist/modules/src/isobmff/isobmff-muxer.js +52 -0
  32. package/dist/modules/src/matroska/ebml.d.ts +2 -0
  33. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  34. package/dist/modules/src/matroska/ebml.js +2 -0
  35. package/dist/modules/src/matroska/matroska-demuxer.d.ts +2 -0
  36. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  37. package/dist/modules/src/matroska/matroska-demuxer.js +40 -3
  38. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  39. package/dist/modules/src/matroska/matroska-muxer.js +49 -9
  40. package/dist/modules/src/media-sink.d.ts +10 -5
  41. package/dist/modules/src/media-sink.d.ts.map +1 -1
  42. package/dist/modules/src/media-sink.js +15 -4
  43. package/dist/modules/src/media-source.d.ts.map +1 -1
  44. package/dist/modules/src/media-source.js +62 -41
  45. package/dist/modules/src/misc.d.ts +44 -1
  46. package/dist/modules/src/misc.d.ts.map +1 -1
  47. package/dist/modules/src/misc.js +79 -0
  48. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts.map +1 -1
  49. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +3 -3
  50. package/dist/modules/src/output-format.d.ts +16 -11
  51. package/dist/modules/src/output-format.d.ts.map +1 -1
  52. package/dist/modules/src/output-format.js +18 -11
  53. package/dist/modules/src/output.d.ts +23 -2
  54. package/dist/modules/src/output.d.ts.map +1 -1
  55. package/dist/modules/src/output.js +21 -5
  56. package/dist/modules/src/sample.d.ts +48 -23
  57. package/dist/modules/src/sample.d.ts.map +1 -1
  58. package/dist/modules/src/sample.js +72 -16
  59. package/dist/modules/src/segmented-input.js +2 -2
  60. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +1 -1
  62. package/src/conversion.ts +68 -12
  63. package/src/encode.ts +71 -10
  64. package/src/hls/hls-demuxer.ts +3 -3
  65. package/src/hls/hls-muxer.ts +5 -3
  66. package/src/hls/hls-segmented-input.ts +3 -2
  67. package/src/index.ts +1 -0
  68. package/src/input-track.ts +37 -7
  69. package/src/isobmff/isobmff-boxes.ts +36 -45
  70. package/src/isobmff/isobmff-demuxer.ts +51 -38
  71. package/src/isobmff/isobmff-muxer.ts +66 -0
  72. package/src/matroska/ebml.ts +2 -0
  73. package/src/matroska/matroska-demuxer.ts +47 -2
  74. package/src/matroska/matroska-muxer.ts +56 -8
  75. package/src/media-sink.ts +22 -6
  76. package/src/media-source.ts +60 -44
  77. package/src/misc.ts +123 -1
  78. package/src/mpeg-ts/mpeg-ts-demuxer.ts +4 -3
  79. package/src/output-format.ts +20 -13
  80. package/src/output.ts +49 -6
  81. package/src/sample.ts +125 -35
  82. package/src/segmented-input.ts +2 -2
@@ -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
  };
@@ -6391,6 +6473,7 @@ var Mediabunny = (() => {
6391
6473
  this.metadataPromise = null;
6392
6474
  this.movieTimescale = -1;
6393
6475
  this.movieDurationInTimescale = -1;
6476
+ this.movieMatrix = IDENTITY_MATRIX;
6394
6477
  this.isQuickTime = false;
6395
6478
  this.metadataTags = {};
6396
6479
  this.currentMetadataKeys = null;
@@ -6509,6 +6592,7 @@ var Mediabunny = (() => {
6509
6592
  await initDemuxer.readMetadata();
6510
6593
  this.movieTimescale = initDemuxer.movieTimescale;
6511
6594
  this.movieDurationInTimescale = initDemuxer.movieDurationInTimescale;
6595
+ this.movieMatrix = initDemuxer.movieMatrix;
6512
6596
  this.metadataTags = initDemuxer.metadataTags;
6513
6597
  this.isFragmented = true;
6514
6598
  this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults;
@@ -6522,7 +6606,7 @@ var Mediabunny = (() => {
6522
6606
  timescale: foreignTrack.timescale,
6523
6607
  durationInMediaTimescale: foreignTrack.durationInMediaTimescale,
6524
6608
  durationInMovieTimescale: foreignTrack.durationInMovieTimescale,
6525
- rotation: foreignTrack.rotation,
6609
+ matrix: foreignTrack.matrix,
6526
6610
  internalCodecId: foreignTrack.internalCodecId,
6527
6611
  name: foreignTrack.name,
6528
6612
  languageCode: foreignTrack.languageCode,
@@ -6536,6 +6620,8 @@ var Mediabunny = (() => {
6536
6620
  encryptionInfo: foreignTrack.encryptionInfo,
6537
6621
  encryptionAuxInfo: null,
6538
6622
  frmaCodecString: null,
6623
+ maxBitrate: foreignTrack.maxBitrate,
6624
+ avgBitrate: foreignTrack.avgBitrate,
6539
6625
  info: foreignTrack.info
6540
6626
  };
6541
6627
  if (foreignTrack.trackBacking) {
@@ -6771,6 +6857,8 @@ var Mediabunny = (() => {
6771
6857
  this.movieTimescale = readU32Be(slice);
6772
6858
  this.movieDurationInTimescale = readU32Be(slice);
6773
6859
  }
6860
+ slice.skip(4 + 2 + 2 + 2 * 4);
6861
+ this.movieMatrix = readMatrix(slice);
6774
6862
  }
6775
6863
  ;
6776
6864
  break;
@@ -6788,7 +6876,7 @@ var Mediabunny = (() => {
6788
6876
  timescale: -1,
6789
6877
  durationInMovieTimescale: -1,
6790
6878
  durationInMediaTimescale: -1,
6791
- rotation: 0,
6879
+ matrix: IDENTITY_MATRIX,
6792
6880
  internalCodecId: null,
6793
6881
  name: null,
6794
6882
  languageCode: UNDETERMINED_LANGUAGE,
@@ -6801,7 +6889,9 @@ var Mediabunny = (() => {
6801
6889
  editListOffset: 0,
6802
6890
  encryptionInfo: null,
6803
6891
  encryptionAuxInfo: null,
6804
- frmaCodecString: null
6892
+ frmaCodecString: null,
6893
+ maxBitrate: null,
6894
+ avgBitrate: null
6805
6895
  };
6806
6896
  this.currentTrack = track;
6807
6897
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
@@ -6844,20 +6934,8 @@ var Mediabunny = (() => {
6844
6934
  throw new Error(`Incorrect track header version ${version}.`);
6845
6935
  }
6846
6936
  slice.skip(2 * 4 + 2 + 2 + 2 + 2);
6847
- const matrix = [
6848
- readFixed_16_16(slice),
6849
- readFixed_16_16(slice),
6850
- readFixed_2_30(slice),
6851
- readFixed_16_16(slice),
6852
- readFixed_16_16(slice),
6853
- readFixed_2_30(slice),
6854
- readFixed_16_16(slice),
6855
- readFixed_16_16(slice),
6856
- readFixed_2_30(slice)
6857
- ];
6858
- const rotation = normalizeRotation(roundToMultiple(extractRotationFromMatrix(matrix), 90));
6859
- assert(rotation === 0 || rotation === 90 || rotation === 180 || rotation === 270);
6860
- track.rotation = rotation;
6937
+ const trackMatrix = readMatrix(slice);
6938
+ track.matrix = multiplyMatrices(trackMatrix, this.movieMatrix);
6861
6939
  }
6862
6940
  ;
6863
6941
  break;
@@ -7413,6 +7491,20 @@ var Mediabunny = (() => {
7413
7491
  }
7414
7492
  ;
7415
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;
7416
7508
  case "wave":
7417
7509
  {
7418
7510
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
@@ -8631,10 +8723,10 @@ var Mediabunny = (() => {
8631
8723
  return 1n;
8632
8724
  }
8633
8725
  getBitrate() {
8634
- return null;
8726
+ return this.internalTrack.maxBitrate;
8635
8727
  }
8636
8728
  getAverageBitrate() {
8637
- return null;
8729
+ return this.internalTrack.avgBitrate;
8638
8730
  }
8639
8731
  async getDurationFromMetadata() {
8640
8732
  const track = this.internalTrack;
@@ -9027,8 +9119,8 @@ var Mediabunny = (() => {
9027
9119
  getSquarePixelHeight() {
9028
9120
  return this.internalTrack.info.squarePixelHeight;
9029
9121
  }
9030
- getRotation() {
9031
- return this.internalTrack.rotation;
9122
+ getTransformationMatrix() {
9123
+ return [...this.internalTrack.matrix];
9032
9124
  }
9033
9125
  async getColorSpace() {
9034
9126
  const decoderConfig = await this.getDecoderConfig();
@@ -9267,13 +9359,18 @@ var Mediabunny = (() => {
9267
9359
  entry.presentationTimestamp += timestamp;
9268
9360
  }
9269
9361
  };
9270
- var extractRotationFromMatrix = (matrix) => {
9271
- const [a, b] = matrix;
9272
- const radians = Math.atan2(b, a);
9273
- if (!Number.isFinite(radians)) {
9274
- return 0;
9275
- }
9276
- 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
+ ];
9277
9374
  };
9278
9375
  var sampleTableIsEmpty = (sampleTable) => {
9279
9376
  return sampleTable.sampleSizes.length === 0;
@@ -10750,6 +10847,8 @@ var Mediabunny = (() => {
10750
10847
  displayUnit: null,
10751
10848
  squarePixelWidth: -1,
10752
10849
  squarePixelHeight: -1,
10850
+ horizontalScale: 1,
10851
+ verticalScale: 1,
10753
10852
  rotation: 0,
10754
10853
  codec: null,
10755
10854
  codecDescription: null,
@@ -10979,6 +11078,32 @@ var Mediabunny = (() => {
10979
11078
  }
10980
11079
  ;
10981
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;
10982
11107
  case 30325 /* ProjectionPoseRoll */:
10983
11108
  {
10984
11109
  if (this.currentTrack?.info?.type !== "video") break;
@@ -11955,8 +12080,13 @@ var Mediabunny = (() => {
11955
12080
  getSquarePixelHeight() {
11956
12081
  return this.internalTrack.info.squarePixelHeight;
11957
12082
  }
11958
- getRotation() {
11959
- 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);
11960
12090
  }
11961
12091
  async getColorSpace() {
11962
12092
  const decoderConfig = await this.getDecoderConfig();
@@ -15934,8 +16064,8 @@ var Mediabunny = (() => {
15934
16064
  getSquarePixelHeight() {
15935
16065
  return this.elementaryStream.info.squarePixelHeight;
15936
16066
  }
15937
- getRotation() {
15938
- return 0;
16067
+ getTransformationMatrix() {
16068
+ return [...IDENTITY_MATRIX];
15939
16069
  }
15940
16070
  async getColorSpace() {
15941
16071
  return this.elementaryStream.info.colorSpace;
@@ -16988,8 +17118,8 @@ var Mediabunny = (() => {
16988
17118
  getSquarePixelHeight() {
16989
17119
  return this.delegate(() => this.firstInputTrack._backing.getSquarePixelHeight());
16990
17120
  }
16991
- getRotation() {
16992
- return this.delegate(() => this.firstInputTrack._backing.getRotation());
17121
+ getTransformationMatrix() {
17122
+ return this.delegate(() => this.firstInputTrack._backing.getTransformationMatrix());
16993
17123
  }
16994
17124
  async getColorSpace() {
16995
17125
  return this.delegate(() => this.firstInputTrack._backing.getColorSpace());
@@ -19022,10 +19152,11 @@ var Mediabunny = (() => {
19022
19152
  throw new Error(`Unsupported IV format '${ivString}'.`);
19023
19153
  }
19024
19154
  let hex = ivString.slice(2);
19025
- 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);
19026
19157
  iv = new Uint8Array(AES_128_BLOCK_SIZE);
19027
19158
  for (let i2 = 0; i2 < AES_128_BLOCK_SIZE; i2++) {
19028
- const startIndex = -AES_128_BLOCK_SIZE * 2 + i2;
19159
+ const startIndex = 2 * i2;
19029
19160
  iv[i2] = parseInt(hex.slice(startIndex, startIndex + 2), 16);
19030
19161
  }
19031
19162
  }
@@ -19931,8 +20062,8 @@ var Mediabunny = (() => {
19931
20062
  }
19932
20063
  return this.internalTrack.info.height;
19933
20064
  }
19934
- getRotation() {
19935
- return this.delegate(() => this.backingVideoTrack.getRotation());
20065
+ getTransformationMatrix() {
20066
+ return this.delegate(() => this.backingVideoTrack.getTransformationMatrix());
19936
20067
  }
19937
20068
  async getColorSpace() {
19938
20069
  await this.hydrate();
@@ -20732,6 +20863,9 @@ var Mediabunny = (() => {
20732
20863
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20733
20864
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20734
20865
  }
20866
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
20867
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20868
+ }
20735
20869
  if (!Number.isFinite(init.timestamp)) {
20736
20870
  throw new TypeError("init.timestamp must be a number.");
20737
20871
  }
@@ -20770,6 +20904,7 @@ var Mediabunny = (() => {
20770
20904
  }
20771
20905
  this.format = init.format;
20772
20906
  this.rotation = init.rotation ?? 0;
20907
+ this.flip = init.flip ?? false;
20773
20908
  this.timestamp = init.timestamp;
20774
20909
  this.duration = init.duration ?? 0;
20775
20910
  const layout = init.layout ?? createDefaultPlaneLayout(init.format, init.codedWidth, init.codedHeight);
@@ -20811,6 +20946,9 @@ var Mediabunny = (() => {
20811
20946
  if (init?.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20812
20947
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20813
20948
  }
20949
+ if (init?.flip !== void 0 && typeof init.flip !== "boolean") {
20950
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20951
+ }
20814
20952
  if (init?.timestamp !== void 0 && !Number.isFinite(init?.timestamp)) {
20815
20953
  throw new TypeError("init.timestamp, when provided, must be a number.");
20816
20954
  }
@@ -20830,6 +20968,7 @@ var Mediabunny = (() => {
20830
20968
  height: data.visibleRect?.height ?? data.codedHeight
20831
20969
  };
20832
20970
  this.rotation = init?.rotation ?? 0;
20971
+ this.flip = init?.flip ?? false;
20833
20972
  this.squarePixelWidth = data.displayWidth;
20834
20973
  this.squarePixelHeight = data.displayHeight;
20835
20974
  this.timestamp = init?.timestamp ?? data.timestamp / 1e6;
@@ -20842,6 +20981,9 @@ var Mediabunny = (() => {
20842
20981
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20843
20982
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20844
20983
  }
20984
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
20985
+ throw new TypeError("init.flip, when provided, must be a boolean.");
20986
+ }
20845
20987
  if (!Number.isFinite(init.timestamp)) {
20846
20988
  throw new TypeError("init.timestamp must be a number.");
20847
20989
  }
@@ -20903,6 +21045,7 @@ var Mediabunny = (() => {
20903
21045
  this.squarePixelWidth = visibleRect.width;
20904
21046
  this.squarePixelHeight = visibleRect.height;
20905
21047
  this.rotation = init.rotation ?? 0;
21048
+ this.flip = init.flip ?? false;
20906
21049
  this.timestamp = init.timestamp;
20907
21050
  this.duration = init.duration ?? 0;
20908
21051
  this.colorSpace = new VideoSampleColorSpace({
@@ -20918,6 +21061,9 @@ var Mediabunny = (() => {
20918
21061
  if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
20919
21062
  throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
20920
21063
  }
21064
+ if (init.flip !== void 0 && typeof init.flip !== "boolean") {
21065
+ throw new TypeError("init.flip, when provided, must be a boolean.");
21066
+ }
20921
21067
  if (!Number.isFinite(init.timestamp)) {
20922
21068
  throw new TypeError("init.timestamp must be a number.");
20923
21069
  }
@@ -20951,6 +21097,7 @@ var Mediabunny = (() => {
20951
21097
  throw new TypeError("getSquarePixelHeight() must return a positive integer.");
20952
21098
  }
20953
21099
  this.rotation = init.rotation ?? 0;
21100
+ this.flip = init.flip ?? false;
20954
21101
  this.timestamp = init.timestamp;
20955
21102
  this.duration = init.duration ?? 0;
20956
21103
  this.colorSpace = data.getColorSpace();
@@ -20974,11 +21121,11 @@ var Mediabunny = (() => {
20974
21121
  get codedHeight() {
20975
21122
  return this.visibleRect.height;
20976
21123
  }
20977
- /** 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. */
20978
21125
  get displayWidth() {
20979
21126
  return this.rotation % 180 === 0 ? this.squarePixelWidth : this.squarePixelHeight;
20980
21127
  }
20981
- /** 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. */
20982
21129
  get displayHeight() {
20983
21130
  return this.rotation % 180 === 0 ? this.squarePixelHeight : this.squarePixelWidth;
20984
21131
  }
@@ -21008,6 +21155,7 @@ var Mediabunny = (() => {
21008
21155
  timestamp: this.timestamp,
21009
21156
  duration: this.duration,
21010
21157
  rotation: this.rotation,
21158
+ flip: this.flip,
21011
21159
  encodeOptions: this.encodeOptions
21012
21160
  });
21013
21161
  } else if (isVideoFrame(this._data)) {
@@ -21015,6 +21163,7 @@ var Mediabunny = (() => {
21015
21163
  timestamp: this.timestamp,
21016
21164
  duration: this.duration,
21017
21165
  rotation: this.rotation,
21166
+ flip: this.flip,
21018
21167
  encodeOptions: this.encodeOptions
21019
21168
  });
21020
21169
  } else if (this._data instanceof Uint8Array) {
@@ -21028,6 +21177,7 @@ var Mediabunny = (() => {
21028
21177
  duration: this.duration,
21029
21178
  colorSpace: this.colorSpace,
21030
21179
  rotation: this.rotation,
21180
+ flip: this.flip,
21031
21181
  visibleRect: this.visibleRect,
21032
21182
  displayWidth: this.displayWidth,
21033
21183
  displayHeight: this.displayHeight,
@@ -21044,6 +21194,7 @@ var Mediabunny = (() => {
21044
21194
  duration: this.duration,
21045
21195
  colorSpace: this.colorSpace,
21046
21196
  rotation: this.rotation,
21197
+ flip: this.flip,
21047
21198
  visibleRect: this.visibleRect,
21048
21199
  displayWidth: this.displayWidth,
21049
21200
  displayHeight: this.displayHeight,
@@ -21116,7 +21267,8 @@ var Mediabunny = (() => {
21116
21267
  {
21117
21268
  timestamp: this.timestamp,
21118
21269
  duration: this.duration,
21119
- rotation: this.rotation
21270
+ rotation: this.rotation,
21271
+ flip: this.flip
21120
21272
  },
21121
21273
  options.colorSpace ?? "srgb"
21122
21274
  ));
@@ -21372,12 +21524,15 @@ var Mediabunny = (() => {
21372
21524
  if (this._closed) {
21373
21525
  throw new Error("VideoSample is closed.");
21374
21526
  }
21375
- ({ 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));
21376
21528
  const source = this.toCanvasImageSource();
21377
21529
  context.save();
21378
21530
  const centerX = dx + dWidth / 2;
21379
21531
  const centerY = dy + dHeight / 2;
21380
21532
  context.translate(centerX, centerY);
21533
+ if (this.flip) {
21534
+ context.scale(-1, 1);
21535
+ }
21381
21536
  context.rotate(this.rotation * Math.PI / 180);
21382
21537
  const aspectRatioChange = this.rotation % 180 === 0 ? 1 : dWidth / dHeight;
21383
21538
  context.scale(1 / aspectRatioChange, aspectRatioChange);
@@ -21410,12 +21565,16 @@ var Mediabunny = (() => {
21410
21565
  if (options.rotation !== void 0 && ![0, 90, 180, 270].includes(options.rotation)) {
21411
21566
  throw new TypeError("options.rotation, when provided, must be 0, 90, 180, or 270.");
21412
21567
  }
21568
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
21569
+ throw new TypeError("options.flip, when provided, must be a boolean.");
21570
+ }
21413
21571
  if (options.crop !== void 0) {
21414
21572
  validateCropRectangle(options.crop, "options.");
21415
21573
  }
21416
21574
  const canvasWidth = context.canvas.width;
21417
21575
  const canvasHeight = context.canvas.height;
21418
21576
  const rotation = options.rotation ?? this.rotation;
21577
+ const flip = options.flip ?? this.flip;
21419
21578
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
21420
21579
  let finalCrop = options.crop;
21421
21580
  if (finalCrop) {
@@ -21425,12 +21584,13 @@ var Mediabunny = (() => {
21425
21584
  let dy;
21426
21585
  let newWidth;
21427
21586
  let newHeight;
21428
- const { sx, sy, sWidth, sHeight } = this._rotateSourceRegion(
21587
+ const { sx, sy, sWidth, sHeight } = this._unmapSourceRegion(
21429
21588
  options.crop?.left ?? 0,
21430
21589
  options.crop?.top ?? 0,
21431
21590
  options.crop?.width ?? rotatedWidth,
21432
21591
  options.crop?.height ?? rotatedHeight,
21433
- rotation
21592
+ rotation,
21593
+ flip
21434
21594
  );
21435
21595
  if (options.fit === "fill") {
21436
21596
  dx = 0;
@@ -21448,6 +21608,9 @@ var Mediabunny = (() => {
21448
21608
  context.save();
21449
21609
  const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight;
21450
21610
  context.translate(canvasWidth / 2, canvasHeight / 2);
21611
+ if (flip) {
21612
+ context.scale(-1, 1);
21613
+ }
21451
21614
  context.rotate(rotation * Math.PI / 180);
21452
21615
  context.scale(1 / aspectRatioChange, aspectRatioChange);
21453
21616
  context.translate(-canvasWidth / 2, -canvasHeight / 2);
@@ -21455,7 +21618,11 @@ var Mediabunny = (() => {
21455
21618
  context.restore();
21456
21619
  }
21457
21620
  /** @internal */
21458
- _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
+ }
21459
21626
  if (rotation === 90) {
21460
21627
  [sx, sy, sWidth, sHeight] = [
21461
21628
  sy,
@@ -21507,6 +21674,7 @@ var Mediabunny = (() => {
21507
21674
  this.drawWithFit(context, {
21508
21675
  fit: options.fit,
21509
21676
  rotation: options.rotation,
21677
+ flip: options.flip,
21510
21678
  crop: options.crop
21511
21679
  });
21512
21680
  context.globalCompositeOperation = "copy";
@@ -21554,8 +21722,8 @@ var Mediabunny = (() => {
21554
21722
  }
21555
21723
  }
21556
21724
  /**
21557
- * Transform this video sample to a new video sample given the options. Can be used to resize, rotate, and crop
21558
- * 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.
21559
21727
  *
21560
21728
  * In non-browser environments, this method will not work by default. To make it work, register a custom
21561
21729
  * transformer function via {@link registerVideoSampleTransformer}.
@@ -21584,13 +21752,21 @@ var Mediabunny = (() => {
21584
21752
  if (options.rotate !== void 0 && ![0, 90, 180, 270].includes(options.rotate)) {
21585
21753
  throw new TypeError("options.rotate, when provided, must be 0, 90, 180 or 270.");
21586
21754
  }
21755
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
21756
+ throw new TypeError("options.flip, when provided, must be a boolean.");
21757
+ }
21587
21758
  if (options.crop !== void 0) {
21588
21759
  validateCropRectangle(options.crop, "options.");
21589
21760
  }
21590
21761
  if (options.alpha !== void 0 && !["keep", "discard"].includes(options.alpha)) {
21591
21762
  throw new TypeError("options.alpha, when provided, must be 'keep' or 'discard'.");
21592
21763
  }
21593
- 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
+ );
21594
21770
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
21595
21771
  let finalCrop = options.crop;
21596
21772
  if (finalCrop) {
@@ -21621,6 +21797,7 @@ var Mediabunny = (() => {
21621
21797
  height: targetHeight,
21622
21798
  fit: options.fit ?? "fill",
21623
21799
  rotation,
21800
+ flip,
21624
21801
  crop: finalCrop ?? {
21625
21802
  left: 0,
21626
21803
  top: 0,
@@ -21640,6 +21817,7 @@ var Mediabunny = (() => {
21640
21817
  this._drawWithFitAndMipmapping(canvas, context, {
21641
21818
  fit: description.fit,
21642
21819
  rotation: description.rotation,
21820
+ flip: description.flip,
21643
21821
  crop: description.crop,
21644
21822
  targetIsFresh: isNew,
21645
21823
  fillBlack: description.alpha === "discard"
@@ -21647,8 +21825,9 @@ var Mediabunny = (() => {
21647
21825
  return new _VideoSample(canvas, {
21648
21826
  timestamp: this.timestamp,
21649
21827
  duration: this.duration,
21650
- rotation: 0
21651
- // Any previous rotation is now baked in
21828
+ // Any previous rotation and flip are now baked in
21829
+ rotation: 0,
21830
+ flip: false
21652
21831
  });
21653
21832
  }
21654
21833
  /** Sets the rotation metadata of this video sample. */
@@ -21658,6 +21837,13 @@ var Mediabunny = (() => {
21658
21837
  }
21659
21838
  this.rotation = newRotation;
21660
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
+ }
21661
21847
  /** Sets the presentation timestamp of this video sample, in seconds. */
21662
21848
  setTimestamp(newTimestamp) {
21663
21849
  if (!Number.isFinite(newTimestamp)) {
@@ -22853,6 +23039,9 @@ var Mediabunny = (() => {
22853
23039
  if (config.transform.rotate !== void 0 && ![0, 90, 180, 270].includes(config.transform.rotate)) {
22854
23040
  throw new TypeError("config.transform.rotate, when provided, must be 0, 90, 180 or 270.");
22855
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
+ }
22856
23045
  if (config.transform.crop !== void 0) {
22857
23046
  validateCropRectangle(config.transform.crop, "config.transform.");
22858
23047
  }
@@ -23284,6 +23473,7 @@ var Mediabunny = (() => {
23284
23473
  quality,
23285
23474
  // eslint-disable-next-line @typescript-eslint/no-deprecated
23286
23475
  bitrate,
23476
+ frameRate,
23287
23477
  ...restOptions
23288
23478
  } = options;
23289
23479
  if (!VIDEO_CODECS.includes(codec)) {
@@ -23304,6 +23494,9 @@ var Mediabunny = (() => {
23304
23494
  if (bitrate !== void 0 && !(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
23305
23495
  throw new TypeError("bitrate must be a positive integer or a quality.");
23306
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
+ }
23307
23500
  validateVideoEncodingAdditionalOptions(codec, restOptions);
23308
23501
  const resolvedQuality = resolveQuality(quality, bitrate) ?? new Quality("medium");
23309
23502
  let candidates;
@@ -23313,7 +23506,7 @@ var Mediabunny = (() => {
23313
23506
  width,
23314
23507
  height,
23315
23508
  quality: resolvedQuality,
23316
- framerate: void 0,
23509
+ framerate: frameRate,
23317
23510
  ...restOptions,
23318
23511
  alpha: "discard"
23319
23512
  // Since we handle alpha ourselves
@@ -24237,11 +24430,12 @@ var Mediabunny = (() => {
24237
24430
  return decodedSampleQueueSize === 0 ? 40 : 8;
24238
24431
  };
24239
24432
  var VideoDecoderWrapper = class extends DecoderWrapper {
24240
- constructor(onSample, onError, codec, decoderConfig, rotation, timeResolution) {
24433
+ constructor(onSample, onError, codec, decoderConfig, rotation, flip, timeResolution) {
24241
24434
  super(onSample, onError);
24242
24435
  this.codec = codec;
24243
24436
  this.decoderConfig = decoderConfig;
24244
24437
  this.rotation = rotation;
24438
+ this.flip = flip;
24245
24439
  this.timeResolution = timeResolution;
24246
24440
  this.decoder = null;
24247
24441
  this.customDecoder = null;
@@ -24526,6 +24720,7 @@ var Mediabunny = (() => {
24526
24720
  sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution);
24527
24721
  sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution);
24528
24722
  sample.setRotation(this.rotation);
24723
+ sample.setFlip(this.flip);
24529
24724
  this.onSample(sample);
24530
24725
  }
24531
24726
  async mergeAlpha(color, alpha) {
@@ -24860,6 +25055,7 @@ var Mediabunny = (() => {
24860
25055
  }
24861
25056
  const codec = await this._track.getCodec();
24862
25057
  const rotation = await this._track.getRotation();
25058
+ const flip = await this._track.getFlip();
24863
25059
  let decoderConfig = await this._track.getDecoderConfig();
24864
25060
  const timeResolution = await this._track.getTimeResolution();
24865
25061
  assert(codec && decoderConfig);
@@ -24868,7 +25064,7 @@ var Mediabunny = (() => {
24868
25064
  hardwareAcceleration: this._decoderOptions.hardwareAcceleration,
24869
25065
  optimizeForLatency: this._decoderOptions.optimizeForLatency
24870
25066
  };
24871
- return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution);
25067
+ return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, flip, timeResolution);
24872
25068
  }
24873
25069
  /** @internal */
24874
25070
  _createPacketSink() {
@@ -24922,6 +25118,8 @@ var Mediabunny = (() => {
24922
25118
  /** @internal */
24923
25119
  this._rotation = 0;
24924
25120
  /** @internal */
25121
+ this._flip = false;
25122
+ /** @internal */
24925
25123
  this._initPromise = null;
24926
25124
  /** @internal */
24927
25125
  this._nextCanvasIndex = 0;
@@ -24951,6 +25149,9 @@ var Mediabunny = (() => {
24951
25149
  if (options.rotation !== void 0 && ![0, 90, 180, 270].includes(options.rotation)) {
24952
25150
  throw new TypeError("options.rotation, when provided, must be 0, 90, 180 or 270.");
24953
25151
  }
25152
+ if (options.flip !== void 0 && typeof options.flip !== "boolean") {
25153
+ throw new TypeError("options.flip, when provided, must be a boolean.");
25154
+ }
24954
25155
  if (options.crop !== void 0) {
24955
25156
  validateCropRectangle(options.crop, "options.");
24956
25157
  }
@@ -24973,6 +25174,7 @@ var Mediabunny = (() => {
24973
25174
  const options = this._options;
24974
25175
  const videoTrack = this._videoTrack;
24975
25176
  const rotation = options.rotation ?? await videoTrack.getRotation();
25177
+ const flip = options.flip ?? await videoTrack.getFlip();
24976
25178
  const squarePixelWidth = await videoTrack.getSquarePixelWidth();
24977
25179
  const squarePixelHeight = await videoTrack.getSquarePixelHeight();
24978
25180
  const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [squarePixelWidth, squarePixelHeight] : [squarePixelHeight, squarePixelWidth];
@@ -24995,6 +25197,7 @@ var Mediabunny = (() => {
24995
25197
  this._width = width;
24996
25198
  this._height = height;
24997
25199
  this._rotation = rotation;
25200
+ this._flip = flip;
24998
25201
  this._crop = crop;
24999
25202
  })();
25000
25203
  }
@@ -25028,6 +25231,7 @@ var Mediabunny = (() => {
25028
25231
  sample._drawWithFitAndMipmapping(canvas, context, {
25029
25232
  fit: this._fit,
25030
25233
  rotation: this._rotation,
25234
+ flip: this._flip,
25031
25235
  crop: this._crop,
25032
25236
  targetIsFresh: canvasIsNew,
25033
25237
  fillBlack: !this._alpha && isFirefox()
@@ -25893,16 +26097,38 @@ var Mediabunny = (() => {
25893
26097
  get codedHeight() {
25894
26098
  return requireSync(this._backing.getCodedHeight(), "codedHeight", "getCodedHeight");
25895
26099
  }
25896
- /** 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
+ */
25897
26115
  async getRotation() {
25898
- return this._backing.getRotation();
26116
+ return extractRotationFromMatrix(await this._backing.getTransformationMatrix());
25899
26117
  }
25900
26118
  /**
25901
26119
  * The angle in degrees by which the track's frames should be rotated (clockwise).
25902
26120
  * @deprecated Use {@link InputVideoTrack.getRotation} instead.
25903
26121
  */
25904
26122
  get rotation() {
25905
- 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());
25906
26132
  }
25907
26133
  /**
25908
26134
  * Returns the width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation.
@@ -25972,7 +26198,9 @@ var Mediabunny = (() => {
25972
26198
  return metadata;
25973
26199
  }
25974
26200
  }
25975
- const rotation = requireSync(this._backing.getRotation(), "displayWidth", "getDisplayWidth");
26201
+ const rotation = extractRotationFromMatrix(
26202
+ requireSync(this._backing.getTransformationMatrix(), "displayWidth", "getDisplayWidth")
26203
+ );
25976
26204
  const value = rotation % 180 === 0 ? this._backing.getSquarePixelWidth() : this._backing.getSquarePixelHeight();
25977
26205
  return requireSync(value, "displayWidth", "getDisplayWidth");
25978
26206
  }
@@ -25997,7 +26225,9 @@ var Mediabunny = (() => {
25997
26225
  return metadata;
25998
26226
  }
25999
26227
  }
26000
- const rotation = requireSync(this._backing.getRotation(), "displayHeight", "getDisplayHeight");
26228
+ const rotation = extractRotationFromMatrix(
26229
+ requireSync(this._backing.getTransformationMatrix(), "displayHeight", "getDisplayHeight")
26230
+ );
26001
26231
  const value = rotation % 180 === 0 ? this._backing.getSquarePixelHeight() : this._backing.getSquarePixelWidth();
26002
26232
  return requireSync(value, "displayHeight", "getDisplayHeight");
26003
26233
  }
@@ -28768,23 +28998,6 @@ var Mediabunny = (() => {
28768
28998
  if (nullTerminated) bytes2.push(0);
28769
28999
  return bytes2;
28770
29000
  };
28771
- var rotationMatrix = (rotationInDegrees) => {
28772
- const theta = rotationInDegrees * (Math.PI / 180);
28773
- const cosTheta = Math.round(Math.cos(theta));
28774
- const sinTheta = Math.round(Math.sin(theta));
28775
- return [
28776
- cosTheta,
28777
- sinTheta,
28778
- 0,
28779
- -sinTheta,
28780
- cosTheta,
28781
- 0,
28782
- 0,
28783
- 0,
28784
- 1
28785
- ];
28786
- };
28787
- var IDENTITY_MATRIX = /* @__PURE__ */ rotationMatrix(0);
28788
29001
  var matrixToBytes = (matrix) => {
28789
29002
  return [
28790
29003
  fixed_16_16(matrix[0]),
@@ -28944,26 +29157,6 @@ var Mediabunny = (() => {
28944
29157
  // Next track ID
28945
29158
  ]);
28946
29159
  };
28947
- var presentationSpan = (trackData) => {
28948
- if (trackData.samples.length === 0) {
28949
- return 0;
28950
- }
28951
- let minTimestamp = Infinity;
28952
- let maxEndTimestamp = -Infinity;
28953
- for (let i = 0; i < trackData.samples.length; i++) {
28954
- const sample = trackData.samples[i];
28955
- if (sample.timestamp < minTimestamp) {
28956
- minTimestamp = sample.timestamp;
28957
- }
28958
- if (sample.timestamp + sample.duration > maxEndTimestamp) {
28959
- maxEndTimestamp = sample.timestamp + sample.duration;
28960
- }
28961
- }
28962
- if (minTimestamp === Infinity) {
28963
- return 0;
28964
- }
28965
- return maxEndTimestamp - minTimestamp;
28966
- };
28967
29160
  var trak = (trackData, creationTime) => {
28968
29161
  const trackMetadata = getTrackMetadata(trackData);
28969
29162
  const needsEditList = trackData.startTimestampOffset !== null && trackData.startTimestampOffset !== 0;
@@ -28987,9 +29180,15 @@ var Mediabunny = (() => {
28987
29180
  const needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
28988
29181
  const u32OrU64 = needsU64 ? u64 : u32;
28989
29182
  let matrix;
28990
- if (trackData.type === "video") {
28991
- const rotation = trackData.track.metadata.rotation;
28992
- 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);
28993
29192
  } else {
28994
29193
  matrix = IDENTITY_MATRIX;
28995
29194
  }
@@ -29248,8 +29447,22 @@ var Mediabunny = (() => {
29248
29447
  ], [
29249
29448
  VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec]?.(trackData) ?? null,
29250
29449
  pasp(trackData),
29251
- colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace) ? null : colr(trackData)
29450
+ colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace) ? null : colr(trackData),
29451
+ btrt(trackData)
29252
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
+ };
29253
29466
  var pasp = (trackData) => {
29254
29467
  if (trackData.info.pixelAspectRatio.num === trackData.info.pixelAspectRatio.den) {
29255
29468
  return null;
@@ -29403,7 +29616,8 @@ var Mediabunny = (() => {
29403
29616
  ];
29404
29617
  }
29405
29618
  return box(compressionType, contents, [
29406
- audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null
29619
+ audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null,
29620
+ btrt(trackData)
29407
29621
  ]);
29408
29622
  };
29409
29623
  var esds = (trackData) => {
@@ -29437,9 +29651,9 @@ var Mediabunny = (() => {
29437
29651
  // stream type(6bits)=5 audio, flags(2bits)=1
29438
29652
  ...u24(0),
29439
29653
  // 24bit buffer size
29440
- ...u32(0),
29654
+ ...u32(trackData.maxBitrate),
29441
29655
  // max bitrate
29442
- ...u32(0)
29656
+ ...u32(trackData.avgBitrate)
29443
29657
  // avg bitrate
29444
29658
  ];
29445
29659
  if (trackData.info.decoderConfig.description) {
@@ -29627,7 +29841,8 @@ var Mediabunny = (() => {
29627
29841
  u16(1)
29628
29842
  // Data reference index
29629
29843
  ], [
29630
- SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData)
29844
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
29845
+ btrt(trackData)
29631
29846
  ]);
29632
29847
  var vttC = (trackData) => box("vttC", [
29633
29848
  ...textEncoder.encode(trackData.info.config.description)
@@ -31062,6 +31277,26 @@ var Mediabunny = (() => {
31062
31277
  const value = timeInSeconds * timescale;
31063
31278
  return round ? Math.round(value) : value;
31064
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
+ };
31065
31300
  var IsobmffMuxer2 = class extends Muxer {
31066
31301
  constructor(output, format) {
31067
31302
  super(output);
@@ -31275,7 +31510,9 @@ var Mediabunny = (() => {
31275
31510
  finalizedChunks: [],
31276
31511
  currentChunk: null,
31277
31512
  compactlyCodedChunkTable: [],
31278
- closed: false
31513
+ closed: false,
31514
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31515
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0
31279
31516
  };
31280
31517
  this.trackDatas.push(newTrackData);
31281
31518
  this.trackDatas.sort((a, b) => a.track.id - b.track.id);
@@ -31349,7 +31586,9 @@ var Mediabunny = (() => {
31349
31586
  finalizedChunks: [],
31350
31587
  currentChunk: null,
31351
31588
  compactlyCodedChunkTable: [],
31352
- closed: false
31589
+ closed: false,
31590
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31591
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0
31353
31592
  };
31354
31593
  this.trackDatas.push(newTrackData);
31355
31594
  this.trackDatas.sort((a, b) => a.track.id - b.track.id);
@@ -31387,6 +31626,8 @@ var Mediabunny = (() => {
31387
31626
  currentChunk: null,
31388
31627
  compactlyCodedChunkTable: [],
31389
31628
  closed: false,
31629
+ avgBitrate: track.source._nominalBitrate ?? track.metadata.averageBitrate ?? 0,
31630
+ maxBitrate: track.source._nominalBitrate ?? track.metadata.bitrate ?? 0,
31390
31631
  lastCueEndTimestamp: null,
31391
31632
  cueQueue: [],
31392
31633
  nextSourceId: 0,
@@ -32052,6 +32293,29 @@ var Mediabunny = (() => {
32052
32293
  } else {
32053
32294
  for (const trackData of this.trackDatas) {
32054
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;
32055
32319
  if (trackData.startTimestampOffset !== null) {
32056
32320
  for (let i = 0; i < trackData.samples.length; i++) {
32057
32321
  const sample = trackData.samples[i];
@@ -32350,14 +32614,34 @@ var Mediabunny = (() => {
32350
32614
  }
32351
32615
  }
32352
32616
  videoSpecificTrackInfo(trackData) {
32353
- const { frameRate, rotation } = trackData.track.metadata;
32617
+ const { frameRate, transformationMatrix } = trackData.track.metadata;
32354
32618
  const elements = [
32355
32619
  frameRate ? {
32356
32620
  id: 2352003 /* DefaultDuration */,
32357
32621
  data: 1e9 / frameRate
32358
32622
  } : null
32359
32623
  ];
32360
- 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);
32361
32645
  const hasNonSquarePixelAspectRatio = !!trackData.info.aspectRatio && trackData.info.aspectRatio.num * trackData.info.height !== trackData.info.aspectRatio.den * trackData.info.width;
32362
32646
  const colorSpace = trackData.info.decoderConfig.colorSpace;
32363
32647
  const videoElement = { id: 224 /* Video */, data: [
@@ -32389,7 +32673,7 @@ var Mediabunny = (() => {
32389
32673
  }
32390
32674
  ]
32391
32675
  },
32392
- flippedRotation ? {
32676
+ roll || yaw || pitch ? {
32393
32677
  id: 30320 /* Projection */,
32394
32678
  data: [
32395
32679
  {
@@ -32397,11 +32681,19 @@ var Mediabunny = (() => {
32397
32681
  data: 0
32398
32682
  // rectangular
32399
32683
  },
32400
- {
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 ? {
32401
32693
  id: 30325 /* ProjectionPoseRoll */,
32402
- data: new EBMLFloat32((flippedRotation + 180) % 360 - 180)
32694
+ data: new EBMLFloat32((roll + 180) % 360 - 180)
32403
32695
  // [0, 270] -> [-180, 90]
32404
- }
32696
+ } : null
32405
32697
  ]
32406
32698
  } : null
32407
32699
  ] };
@@ -34848,6 +35140,11 @@ ${cue.notes ?? ""}`;
34848
35140
  this._closingPromise = null;
34849
35141
  /** @internal */
34850
35142
  this._closed = false;
35143
+ /**
35144
+ * Set when the encoder is configured with a bitrate.
35145
+ * @internal
35146
+ */
35147
+ this._nominalBitrate = null;
34851
35148
  }
34852
35149
  /** @internal */
34853
35150
  _ensureValidAdd() {
@@ -35024,7 +35321,7 @@ ${cue.notes ?? ""}`;
35024
35321
  this.codedWidth = videoSample.codedWidth;
35025
35322
  this.codedHeight = videoSample.codedHeight;
35026
35323
  }
35027
- 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;
35028
35325
  const needsTransform = hasTransformConfig || isSizeChange && sizeChangeBehavior !== "passThrough";
35029
35326
  if (needsTransform) {
35030
35327
  let targetWidth = config.transform?.width;
@@ -35044,6 +35341,7 @@ ${cue.notes ?? ""}`;
35044
35341
  roundDimensionsTo: 2,
35045
35342
  crop: config.transform?.crop,
35046
35343
  rotate: config.transform?.rotate,
35344
+ flip: config.transform?.flip,
35047
35345
  fit: appliedFit,
35048
35346
  alpha: config.alpha
35049
35347
  });
@@ -35338,6 +35636,8 @@ ${cue.notes ?? ""}`;
35338
35636
  this.encodingConfig.codec,
35339
35637
  selected.quantizer
35340
35638
  );
35639
+ } else {
35640
+ this.source._nominalBitrate = encoderConfig.bitrate ?? null;
35341
35641
  }
35342
35642
  if (MatchingCustomEncoder) {
35343
35643
  this.customEncoder = new MatchingCustomEncoder();
@@ -36008,39 +36308,42 @@ ${cue.notes ?? ""}`;
36008
36308
  }
36009
36309
  /** @internal */
36010
36310
  async _flushAndClose(forceClose) {
36011
- if (this._abortController) {
36012
- this._abortController.abort();
36013
- this._abortController = null;
36014
- }
36015
- if (this._timerHandle) {
36016
- clearIntervalUnthrottled(this._timerHandle);
36017
- }
36018
- this._lastVideoFrame?.close();
36019
- if (this._videoElement) {
36020
- this._videoElement.srcObject = null;
36021
- this._videoElement.remove();
36022
- this._videoElement = null;
36023
- }
36024
- if (this._workerTrackId !== null) {
36025
- assert(this._workerListener);
36026
- sendMessageToMediaStreamTrackProcessorWorker({
36027
- type: "stopTrack",
36028
- trackId: this._workerTrackId
36029
- });
36030
- await new Promise((resolve) => {
36031
- const listener = (event) => {
36032
- const message = event.data;
36033
- if (message.type === "trackStopped" && message.trackId === this._workerTrackId) {
36034
- assert(this._workerListener);
36035
- mediaStreamTrackProcessorWorker.removeEventListener("message", this._workerListener);
36036
- mediaStreamTrackProcessorWorker.removeEventListener("message", listener);
36037
- resolve();
36038
- }
36039
- };
36040
- mediaStreamTrackProcessorWorker.addEventListener("message", listener);
36041
- });
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);
36042
36346
  }
36043
- await this._encoder.flushAndClose(forceClose);
36044
36347
  }
36045
36348
  };
36046
36349
  var AudioSource = class extends MediaSource {
@@ -36343,6 +36646,7 @@ ${cue.notes ?? ""}`;
36343
36646
  quality
36344
36647
  });
36345
36648
  this.encodingConfig.onEncoderConfig?.(encoderConfig);
36649
+ this.source._nominalBitrate = encoderConfig.bitrate ?? null;
36346
36650
  const MatchingCustomEncoder = customAudioEncoders.find((x) => x.supports(
36347
36651
  this.encodingConfig.codec,
36348
36652
  encoderConfig
@@ -36787,16 +37091,19 @@ ${cue.notes ?? ""}`;
36787
37091
  }
36788
37092
  /** @internal */
36789
37093
  async _flushAndClose(forceClose) {
36790
- if (this._abortController) {
36791
- this._abortController.abort();
36792
- this._abortController = null;
36793
- }
36794
- if (this._audioContext) {
36795
- assert(this._scriptProcessorNode);
36796
- this._scriptProcessorNode.disconnect();
36797
- 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);
36798
37106
  }
36799
- await this._encoder.flushAndClose(forceClose);
36800
37107
  }
36801
37108
  };
36802
37109
  var mediaStreamTrackProcessorWorkerCode = () => {
@@ -37173,13 +37480,13 @@ ${cue.notes ?? ""}`;
37173
37480
  const codecs = [];
37174
37481
  let videoCount = 0;
37175
37482
  let audioCount = 0;
37176
- let requiresRotationMetadata = false;
37483
+ let requiresTransformationMetadata = false;
37177
37484
  let candidate = null;
37178
37485
  let candidateScore = -Infinity;
37179
37486
  for (const track of tracks) {
37180
37487
  if (track.isVideoTrack()) {
37181
37488
  videoCount++;
37182
- requiresRotationMetadata ||= (track.metadata.rotation ?? 0) !== 0;
37489
+ requiresTransformationMetadata ||= (track.metadata.rotation ?? 0) !== 0 || !!track.metadata.flip || !!track.metadata.transformationMatrix;
37183
37490
  } else if (track.isAudioTrack()) {
37184
37491
  audioCount++;
37185
37492
  }
@@ -37198,7 +37505,7 @@ ${cue.notes ?? ""}`;
37198
37505
  continue;
37199
37506
  }
37200
37507
  let score = 0;
37201
- if (requiresRotationMetadata && format.supportsVideoRotationMetadata) {
37508
+ if (requiresTransformationMetadata && format.supportsVideoTransformationMetadata) {
37202
37509
  score++;
37203
37510
  }
37204
37511
  if (score > candidateScore) {
@@ -38126,6 +38433,13 @@ ${cue.notes ?? ""}`;
38126
38433
 
38127
38434
  // src/output-format.ts
38128
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
+ }
38129
38443
  /** Returns a list of video codecs that this output format can contain. */
38130
38444
  getSupportedVideoCodecs() {
38131
38445
  return this.getSupportedCodecs().filter((codec) => VIDEO_CODECS.includes(codec));
@@ -38191,7 +38505,7 @@ ${cue.notes ?? ""}`;
38191
38505
  total: { min: 0, max }
38192
38506
  };
38193
38507
  }
38194
- get supportsVideoRotationMetadata() {
38508
+ get supportsVideoTransformationMetadata() {
38195
38509
  return true;
38196
38510
  }
38197
38511
  get supportsTimestampedMediaData() {
@@ -38368,7 +38682,7 @@ ${cue.notes ?? ""}`;
38368
38682
  ...SUBTITLE_CODECS
38369
38683
  ];
38370
38684
  }
38371
- get supportsVideoRotationMetadata() {
38685
+ get supportsVideoTransformationMetadata() {
38372
38686
  return false;
38373
38687
  }
38374
38688
  get supportsTimestampedMediaData() {
@@ -38448,7 +38762,7 @@ ${cue.notes ?? ""}`;
38448
38762
  getSupportedCodecs() {
38449
38763
  return ["mp3"];
38450
38764
  }
38451
- get supportsVideoRotationMetadata() {
38765
+ get supportsVideoTransformationMetadata() {
38452
38766
  return false;
38453
38767
  }
38454
38768
  get supportsTimestampedMediaData() {
@@ -38505,7 +38819,7 @@ ${cue.notes ?? ""}`;
38505
38819
  )
38506
38820
  ];
38507
38821
  }
38508
- get supportsVideoRotationMetadata() {
38822
+ get supportsVideoTransformationMetadata() {
38509
38823
  return false;
38510
38824
  }
38511
38825
  get supportsTimestampedMediaData() {
@@ -38558,7 +38872,7 @@ ${cue.notes ?? ""}`;
38558
38872
  ...AUDIO_CODECS.filter((codec) => ["vorbis", "opus"].includes(codec))
38559
38873
  ];
38560
38874
  }
38561
- get supportsVideoRotationMetadata() {
38875
+ get supportsVideoTransformationMetadata() {
38562
38876
  return false;
38563
38877
  }
38564
38878
  get supportsTimestampedMediaData() {
@@ -38605,7 +38919,7 @@ ${cue.notes ?? ""}`;
38605
38919
  getSupportedCodecs() {
38606
38920
  return ["aac"];
38607
38921
  }
38608
- get supportsVideoRotationMetadata() {
38922
+ get supportsVideoTransformationMetadata() {
38609
38923
  return false;
38610
38924
  }
38611
38925
  get supportsTimestampedMediaData() {
@@ -38652,7 +38966,7 @@ ${cue.notes ?? ""}`;
38652
38966
  getSupportedCodecs() {
38653
38967
  return ["flac"];
38654
38968
  }
38655
- get supportsVideoRotationMetadata() {
38969
+ get supportsVideoTransformationMetadata() {
38656
38970
  return false;
38657
38971
  }
38658
38972
  get supportsTimestampedMediaData() {
@@ -38705,7 +39019,7 @@ ${cue.notes ?? ""}`;
38705
39019
  ...AUDIO_CODECS.filter((codec) => ["aac", "mp3", "ac3", "eac3", "dts"].includes(codec))
38706
39020
  ];
38707
39021
  }
38708
- get supportsVideoRotationMetadata() {
39022
+ get supportsVideoTransformationMetadata() {
38709
39023
  return false;
38710
39024
  }
38711
39025
  get supportsTimestampedMediaData() {
@@ -38801,8 +39115,8 @@ ${cue.notes ?? ""}`;
38801
39115
  total: { min: 0, max: Infinity }
38802
39116
  };
38803
39117
  }
38804
- get supportsVideoRotationMetadata() {
38805
- return toArray(this._options.segmentFormat).some((format) => format.supportsVideoRotationMetadata);
39118
+ get supportsVideoTransformationMetadata() {
39119
+ return toArray(this._options.segmentFormat).some((format) => format.supportsVideoTransformationMetadata);
38806
39120
  }
38807
39121
  get supportsTimestampedMediaData() {
38808
39122
  return true;
@@ -38935,6 +39249,12 @@ ${cue.notes ?? ""}`;
38935
39249
  if (metadata.maximumPacketCount !== void 0 && (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)) {
38936
39250
  throw new TypeError("metadata.maximumPacketCount, when provided, must be a non-negative integer.");
38937
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
+ }
38938
39258
  if (metadata.group !== void 0 && !(metadata.group instanceof OutputTrackGroup) && (!Array.isArray(metadata.group) || metadata.group.some((group) => !(group instanceof OutputTrackGroup)))) {
38939
39259
  throw new TypeError(
38940
39260
  "metadata.group, when provided, must be an OutputTrackGroup instance or an array of OutputTrackGroup instances."
@@ -39134,8 +39454,11 @@ ${cue.notes ?? ""}`;
39134
39454
  if (metadata.rotation !== void 0 && ![0, 90, 180, 270].includes(metadata.rotation)) {
39135
39455
  throw new TypeError(`Invalid video rotation: ${metadata.rotation}. Has to be 0, 90, 180 or 270.`);
39136
39456
  }
39137
- if (!this.format.supportsVideoRotationMetadata && metadata.rotation) {
39138
- 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.");
39139
39462
  }
39140
39463
  if (metadata.frameRate !== void 0 && (!Number.isFinite(metadata.frameRate) || metadata.frameRate <= 0)) {
39141
39464
  throw new TypeError(
@@ -39361,9 +39684,8 @@ ${cue.notes ?? ""}`;
39361
39684
  * @returns A promise that resolves once all internal resources have been released.
39362
39685
  */
39363
39686
  async cancel() {
39364
- if (this._cancelPromise) {
39365
- Logging._warn("Output has already been canceled.");
39366
- return this._cancelPromise;
39687
+ if (this.state === "canceled") {
39688
+ return this._cancelPromise ?? void 0;
39367
39689
  } else if (this.state === "finalizing" || this.state === "finalized") {
39368
39690
  if (this.state === "finalized") {
39369
39691
  Logging._warn("Output has already been finalized.");
@@ -39416,6 +39738,9 @@ ${cue.notes ?? ""}`;
39416
39738
  await this._onFinalize();
39417
39739
  }
39418
39740
  this.state = "finalized";
39741
+ } catch (error) {
39742
+ this.state = "canceled";
39743
+ throw error;
39419
39744
  } finally {
39420
39745
  await Promise.all([...this._unfinalizedTargets].map((target) => target._close().catch(() => {
39421
39746
  })));
@@ -39469,6 +39794,12 @@ ${cue.notes ?? ""}`;
39469
39794
  if (videoOptions?.rotate !== void 0 && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
39470
39795
  throw new TypeError("options.video.rotate, when provided, must be 0, 90, 180 or 270.");
39471
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
+ }
39472
39803
  if (videoOptions?.allowRotationMetadata !== void 0 && typeof videoOptions.allowRotationMetadata !== "boolean") {
39473
39804
  throw new TypeError("options.video.allowRotationMetadata, when provided, must be a boolean.");
39474
39805
  }
@@ -40138,9 +40469,17 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40138
40469
  }
40139
40470
  let videoSource;
40140
40471
  const innateRotation = await track.getRotation();
40141
- 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
+ );
40142
40479
  let outputTrackRotation = totalRotation;
40143
- 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);
40144
40483
  const squarePixelWidth = await track.getSquarePixelWidth();
40145
40484
  const squarePixelHeight = await track.getSquarePixelHeight();
40146
40485
  const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0 ? [squarePixelWidth, squarePixelHeight] : [squarePixelHeight, squarePixelWidth];
@@ -40164,7 +40503,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40164
40503
  }
40165
40504
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
40166
40505
  const alpha = trackOptions.alpha ?? "discard";
40167
- 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;
40168
40507
  let copyStartPacket = null;
40169
40508
  if (!needsTranscode) {
40170
40509
  const sink = new EncodedPacketSink(track);
@@ -40318,7 +40657,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40318
40657
  transform: {}
40319
40658
  };
40320
40659
  assert(encodingConfig.transform);
40321
- 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();
40322
40661
  if (!needsRerender) {
40323
40662
  var _stack = [];
40324
40663
  try {
@@ -40363,10 +40702,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40363
40702
  }
40364
40703
  if (needsRerender) {
40365
40704
  outputTrackRotation = 0;
40705
+ outputTrackFlip = false;
40706
+ outputTrackMatrix = null;
40366
40707
  encodingConfig.transform.width = width;
40367
40708
  encodingConfig.transform.height = height;
40368
40709
  encodingConfig.transform.fit = trackOptions.fit ?? "fill";
40369
- encodingConfig.transform.rotate = normalizeRotation(totalRotation - innateRotation);
40710
+ encodingConfig.transform.rotate = trackOptions.rotate;
40711
+ encodingConfig.transform.flip = trackOptions.flip;
40370
40712
  encodingConfig.transform.crop = crop;
40371
40713
  encodingConfig.transform.alpha = alpha;
40372
40714
  }
@@ -40418,6 +40760,8 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40418
40760
  const videoTrackLanguageCode = await track.getLanguageCode();
40419
40761
  const trackName = await track.getName();
40420
40762
  const trackDisposition = await track.getDisposition();
40763
+ const bitrate = needsTranscode ? null : await track.getBitrate();
40764
+ const averageBitrate = needsTranscode ? null : await track.getAverageBitrate();
40421
40765
  this.output.addVideoTrack(videoSource, {
40422
40766
  frameRate: trackOptions.frameRate,
40423
40767
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
@@ -40425,7 +40769,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40425
40769
  name: trackName ?? void 0,
40426
40770
  disposition: trackDisposition,
40427
40771
  rotation: outputTrackRotation,
40428
- 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
40429
40777
  });
40430
40778
  this.utilizedTracks.push(track);
40431
40779
  this._outputTrackIds.push(outputTrackId);
@@ -40709,12 +41057,16 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40709
41057
  const audioTrackLanguageCode = await track.getLanguageCode();
40710
41058
  const trackName = await track.getName();
40711
41059
  const trackDisposition = await track.getDisposition();
41060
+ const bitrate = needsTranscode ? null : await track.getBitrate();
41061
+ const averageBitrate = needsTranscode ? null : await track.getAverageBitrate();
40712
41062
  this.output.addAudioTrack(audioSource, {
40713
41063
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
40714
41064
  languageCode: isIso639Dash2LanguageCode(audioTrackLanguageCode) ? audioTrackLanguageCode : void 0,
40715
41065
  name: trackName ?? void 0,
40716
41066
  disposition: trackDisposition,
40717
- group: ownGroup ?? trackOptions.group
41067
+ group: ownGroup ?? trackOptions.group,
41068
+ bitrate: bitrate ?? void 0,
41069
+ averageBitrate: averageBitrate ?? void 0
40718
41070
  });
40719
41071
  this.utilizedTracks.push(track);
40720
41072
  this._outputTrackIds.push(outputTrackId);