mediabunny 1.13.3 → 1.14.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 (111) hide show
  1. package/dist/bundles/mediabunny.cjs +3423 -293
  2. package/dist/bundles/mediabunny.min.cjs +4 -4
  3. package/dist/bundles/mediabunny.min.mjs +4 -4
  4. package/dist/bundles/mediabunny.mjs +3423 -293
  5. package/dist/mediabunny.d.ts +126 -2
  6. package/dist/modules/shared/mp3-misc.d.ts +2 -0
  7. package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
  8. package/dist/modules/shared/mp3-misc.js +23 -0
  9. package/dist/modules/src/adts/adts-demuxer.d.ts +1 -0
  10. package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
  11. package/dist/modules/src/adts/adts-demuxer.js +3 -0
  12. package/dist/modules/src/conversion.d.ts +9 -0
  13. package/dist/modules/src/conversion.d.ts.map +1 -1
  14. package/dist/modules/src/conversion.js +24 -0
  15. package/dist/modules/src/demuxer.d.ts +2 -0
  16. package/dist/modules/src/demuxer.d.ts.map +1 -1
  17. package/dist/modules/src/index.d.ts +1 -0
  18. package/dist/modules/src/index.d.ts.map +1 -1
  19. package/dist/modules/src/index.js +1 -0
  20. package/dist/modules/src/input-format.d.ts.map +1 -1
  21. package/dist/modules/src/input-format.js +16 -6
  22. package/dist/modules/src/input-track.d.ts +4 -2
  23. package/dist/modules/src/input-track.d.ts.map +1 -1
  24. package/dist/modules/src/input-track.js +4 -2
  25. package/dist/modules/src/input.d.ts +2 -0
  26. package/dist/modules/src/input.d.ts.map +1 -1
  27. package/dist/modules/src/input.js +5 -0
  28. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +3 -3
  29. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  30. package/dist/modules/src/isobmff/isobmff-boxes.js +315 -64
  31. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +13 -0
  32. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  33. package/dist/modules/src/isobmff/isobmff-demuxer.js +434 -32
  34. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +2 -2
  35. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  36. package/dist/modules/src/isobmff/isobmff-muxer.js +4 -4
  37. package/dist/modules/src/isobmff/isobmff-reader.d.ts +3 -0
  38. package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
  39. package/dist/modules/src/isobmff/isobmff-reader.js +25 -1
  40. package/dist/modules/src/matroska/ebml.d.ts +20 -1
  41. package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
  42. package/dist/modules/src/matroska/ebml.js +19 -0
  43. package/dist/modules/src/matroska/matroska-demuxer.d.ts +16 -0
  44. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  45. package/dist/modules/src/matroska/matroska-demuxer.js +317 -2
  46. package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -1
  47. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  48. package/dist/modules/src/matroska/matroska-muxer.js +239 -18
  49. package/dist/modules/src/misc.d.ts +14 -0
  50. package/dist/modules/src/misc.d.ts.map +1 -1
  51. package/dist/modules/src/misc.js +63 -0
  52. package/dist/modules/src/mp3/mp3-demuxer.d.ts +3 -0
  53. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  54. package/dist/modules/src/mp3/mp3-demuxer.js +66 -15
  55. package/dist/modules/src/mp3/mp3-muxer.d.ts +3 -0
  56. package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
  57. package/dist/modules/src/mp3/mp3-muxer.js +162 -4
  58. package/dist/modules/src/mp3/mp3-reader.d.ts +50 -3
  59. package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
  60. package/dist/modules/src/mp3/mp3-reader.js +561 -21
  61. package/dist/modules/src/mp3/mp3-writer.d.ts +10 -0
  62. package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
  63. package/dist/modules/src/mp3/mp3-writer.js +110 -1
  64. package/dist/modules/src/ogg/ogg-demuxer.d.ts +4 -0
  65. package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
  66. package/dist/modules/src/ogg/ogg-demuxer.js +167 -2
  67. package/dist/modules/src/ogg/ogg-muxer.d.ts +1 -0
  68. package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
  69. package/dist/modules/src/ogg/ogg-muxer.js +189 -8
  70. package/dist/modules/src/output.d.ts +8 -0
  71. package/dist/modules/src/output.d.ts.map +1 -1
  72. package/dist/modules/src/output.js +16 -0
  73. package/dist/modules/src/source.d.ts.map +1 -1
  74. package/dist/modules/src/source.js +24 -0
  75. package/dist/modules/src/tags.d.ts +112 -0
  76. package/dist/modules/src/tags.d.ts.map +1 -0
  77. package/dist/modules/src/tags.js +119 -0
  78. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  79. package/dist/modules/src/wave/wave-demuxer.d.ts +4 -0
  80. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  81. package/dist/modules/src/wave/wave-demuxer.js +117 -0
  82. package/dist/modules/src/wave/wave-muxer.d.ts +6 -0
  83. package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
  84. package/dist/modules/src/wave/wave-muxer.js +148 -6
  85. package/package.json +1 -1
  86. package/src/adts/adts-demuxer.ts +4 -0
  87. package/src/conversion.ts +39 -0
  88. package/src/demuxer.ts +2 -0
  89. package/src/index.ts +5 -0
  90. package/src/input-format.ts +15 -6
  91. package/src/input-track.ts +4 -2
  92. package/src/input.ts +6 -0
  93. package/src/isobmff/isobmff-boxes.ts +290 -66
  94. package/src/isobmff/isobmff-demuxer.ts +408 -31
  95. package/src/isobmff/isobmff-muxer.ts +6 -6
  96. package/src/isobmff/isobmff-reader.ts +30 -1
  97. package/src/matroska/ebml.ts +19 -0
  98. package/src/matroska/matroska-demuxer.ts +298 -3
  99. package/src/matroska/matroska-muxer.ts +236 -37
  100. package/src/misc.ts +82 -0
  101. package/src/mp3/mp3-demuxer.ts +81 -14
  102. package/src/mp3/mp3-muxer.ts +142 -4
  103. package/src/mp3/mp3-reader.ts +618 -20
  104. package/src/mp3/mp3-writer.ts +129 -0
  105. package/src/ogg/ogg-demuxer.ts +169 -2
  106. package/src/ogg/ogg-muxer.ts +201 -9
  107. package/src/output.ts +19 -0
  108. package/src/source.ts +32 -0
  109. package/src/tags.ts +215 -0
  110. package/src/wave/wave-demuxer.ts +109 -0
  111. package/src/wave/wave-muxer.ts +152 -6
@@ -93,6 +93,7 @@ var Mediabunny = (() => {
93
93
  Quality: () => Quality,
94
94
  QuickTimeInputFormat: () => QuickTimeInputFormat,
95
95
  ReadableStreamSource: () => ReadableStreamSource,
96
+ RichImageData: () => RichImageData,
96
97
  SUBTITLE_CODECS: () => SUBTITLE_CODECS,
97
98
  Source: () => Source,
98
99
  StreamSource: () => StreamSource,
@@ -253,6 +254,15 @@ var Mediabunny = (() => {
253
254
  };
254
255
  var textDecoder = new TextDecoder();
255
256
  var textEncoder = new TextEncoder();
257
+ var isIso88591Compatible = (text) => {
258
+ for (let i = 0; i < text.length; i++) {
259
+ const code = text.charCodeAt(i);
260
+ if (code > 255) {
261
+ return false;
262
+ }
263
+ }
264
+ return true;
265
+ };
256
266
  var invertObject = (object) => {
257
267
  return Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key]));
258
268
  };
@@ -576,9 +586,149 @@ var Mediabunny = (() => {
576
586
  isSafariCache = result;
577
587
  return result;
578
588
  };
589
+ var coalesceIndex = (a, b) => {
590
+ return a !== -1 ? a : b;
591
+ };
579
592
  var closedIntervalsOverlap = (startA, endA, startB, endB) => {
580
593
  return startA <= endB && startB <= endA;
581
594
  };
595
+ var keyValueIterator = function* (object) {
596
+ for (const key in object) {
597
+ const value = object[key];
598
+ if (value === void 0) {
599
+ continue;
600
+ }
601
+ yield { key, value };
602
+ }
603
+ };
604
+ var imageMimeTypeToExtension = (mimeType) => {
605
+ switch (mimeType.toLowerCase()) {
606
+ case "image/jpeg":
607
+ case "image/jpg":
608
+ return ".jpg";
609
+ case "image/png":
610
+ return ".png";
611
+ case "image/gif":
612
+ return ".gif";
613
+ case "image/webp":
614
+ return ".webp";
615
+ case "image/bmp":
616
+ return ".bmp";
617
+ case "image/svg+xml":
618
+ return ".svg";
619
+ case "image/tiff":
620
+ return ".tiff";
621
+ case "image/avif":
622
+ return ".avif";
623
+ case "image/x-icon":
624
+ case "image/vnd.microsoft.icon":
625
+ return ".ico";
626
+ default:
627
+ return null;
628
+ }
629
+ };
630
+ var base64ToBytes = (base64) => {
631
+ const decoded = atob(base64);
632
+ const bytes2 = new Uint8Array(decoded.length);
633
+ for (let i = 0; i < decoded.length; i++) {
634
+ bytes2[i] = decoded.charCodeAt(i);
635
+ }
636
+ return bytes2;
637
+ };
638
+ var bytesToBase64 = (bytes2) => {
639
+ let string = "";
640
+ for (let i = 0; i < bytes2.length; i++) {
641
+ string += String.fromCharCode(bytes2[i]);
642
+ }
643
+ return btoa(string);
644
+ };
645
+
646
+ // src/tags.ts
647
+ var RichImageData = class {
648
+ /** Creates a new {@link RichImageData}. */
649
+ constructor(data, mimeType) {
650
+ this.data = data;
651
+ this.mimeType = mimeType;
652
+ }
653
+ };
654
+ var validateMetadataTags = (tags) => {
655
+ if (!tags || typeof tags !== "object") {
656
+ throw new TypeError("tags must be an object.");
657
+ }
658
+ if (tags.title !== void 0 && typeof tags.title !== "string") {
659
+ throw new TypeError("tags.title, when provided, must be a string.");
660
+ }
661
+ if (tags.description !== void 0 && typeof tags.description !== "string") {
662
+ throw new TypeError("tags.description, when provided, must be a string.");
663
+ }
664
+ if (tags.artist !== void 0 && typeof tags.artist !== "string") {
665
+ throw new TypeError("tags.artist, when provided, must be a string.");
666
+ }
667
+ if (tags.album !== void 0 && typeof tags.album !== "string") {
668
+ throw new TypeError("tags.album, when provided, must be a string.");
669
+ }
670
+ if (tags.albumArtist !== void 0 && typeof tags.albumArtist !== "string") {
671
+ throw new TypeError("tags.albumArtist, when provided, must be a string.");
672
+ }
673
+ if (tags.trackNumber !== void 0 && (!Number.isInteger(tags.trackNumber) || tags.trackNumber <= 0)) {
674
+ throw new TypeError("tags.trackNumber, when provided, must be a positive integer.");
675
+ }
676
+ if (tags.tracksTotal !== void 0 && (!Number.isInteger(tags.tracksTotal) || tags.tracksTotal <= 0)) {
677
+ throw new TypeError("tags.tracksTotal, when provided, must be a positive integer.");
678
+ }
679
+ if (tags.discNumber !== void 0 && (!Number.isInteger(tags.discNumber) || tags.discNumber <= 0)) {
680
+ throw new TypeError("tags.discNumber, when provided, must be a positive integer.");
681
+ }
682
+ if (tags.discsTotal !== void 0 && (!Number.isInteger(tags.discsTotal) || tags.discsTotal <= 0)) {
683
+ throw new TypeError("tags.discsTotal, when provided, must be a positive integer.");
684
+ }
685
+ if (tags.genre !== void 0 && typeof tags.genre !== "string") {
686
+ throw new TypeError("tags.genre, when provided, must be a string.");
687
+ }
688
+ if (tags.date !== void 0 && (!(tags.date instanceof Date) || Number.isNaN(tags.date.getTime()))) {
689
+ throw new TypeError("tags.date, when provided, must be a valid Date.");
690
+ }
691
+ if (tags.lyrics !== void 0 && typeof tags.lyrics !== "string") {
692
+ throw new TypeError("tags.lyrics, when provided, must be a string.");
693
+ }
694
+ if (tags.images !== void 0) {
695
+ if (!Array.isArray(tags.images)) {
696
+ throw new TypeError("tags.images, when provided, must be an array.");
697
+ }
698
+ for (const image of tags.images) {
699
+ if (!image || typeof image !== "object") {
700
+ throw new TypeError("Each image in tags.images must be an object.");
701
+ }
702
+ if (!(image.data instanceof Uint8Array)) {
703
+ throw new TypeError("Each image.data must be a Uint8Array.");
704
+ }
705
+ if (typeof image.mimeType !== "string") {
706
+ throw new TypeError("Each image.mimeType must be a string.");
707
+ }
708
+ if (!["coverFront", "coverBack", "unknown"].includes(image.kind)) {
709
+ throw new TypeError("Each image.kind must be 'coverFront', 'coverBack', or 'unknown'.");
710
+ }
711
+ }
712
+ }
713
+ if (tags.comment !== void 0 && typeof tags.comment !== "string") {
714
+ throw new TypeError("tags.comment, when provided, must be a string.");
715
+ }
716
+ if (tags.raw !== void 0) {
717
+ if (!tags.raw || typeof tags.raw !== "object") {
718
+ throw new TypeError("tags.raw, when provided, must be an object.");
719
+ }
720
+ for (const value of Object.values(tags.raw)) {
721
+ if (value !== null && typeof value !== "string" && !(value instanceof Uint8Array) && !(value instanceof RichImageData)) {
722
+ throw new TypeError(
723
+ "Each value in tags.raw must be a string, Uint8Array, RichImageData, or null."
724
+ );
725
+ }
726
+ }
727
+ }
728
+ };
729
+ var metadataTagsAreEmpty = (tags) => {
730
+ return tags.title === void 0 && tags.description === void 0 && tags.artist === void 0 && tags.album === void 0 && tags.albumArtist === void 0 && tags.trackNumber === void 0 && tags.tracksTotal === void 0 && tags.discNumber === void 0 && tags.discsTotal === void 0 && tags.genre === void 0 && tags.date === void 0 && tags.lyrics === void 0 && (!tags.images || tags.images.length === 0) && tags.comment === void 0 && (tags.raw === void 0 || Object.keys(tags.raw).length === 0);
731
+ };
582
732
 
583
733
  // src/codec.ts
584
734
  var VIDEO_CODECS = [
@@ -1460,13 +1610,13 @@ var Mediabunny = (() => {
1460
1610
  async addEncodedVideoPacket() {
1461
1611
  throw new Error("ADTS does not support video.");
1462
1612
  }
1463
- async addEncodedAudioPacket(track, packet, meta) {
1613
+ async addEncodedAudioPacket(track, packet, meta2) {
1464
1614
  const release = await this.mutex.acquire();
1465
1615
  try {
1466
1616
  this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === "key");
1467
1617
  if (!this.audioSpecificConfig) {
1468
- validateAudioChunkMetadata(meta);
1469
- const description = meta?.decoderConfig?.description;
1618
+ validateAudioChunkMetadata(meta2);
1619
+ const description = meta2?.decoderConfig?.description;
1470
1620
  assert(description);
1471
1621
  this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description));
1472
1622
  const { objectType, frequencyIndex, channelConfiguration } = this.audioSpecificConfig;
@@ -1562,14 +1712,14 @@ var Mediabunny = (() => {
1562
1712
  settings: cueSettings,
1563
1713
  notes
1564
1714
  };
1565
- const meta = {};
1715
+ const meta2 = {};
1566
1716
  if (!this.preambleEmitted) {
1567
- meta.config = {
1717
+ meta2.config = {
1568
1718
  description: this.preambleText
1569
1719
  };
1570
1720
  this.preambleEmitted = true;
1571
1721
  }
1572
- this.options.output(cue, meta);
1722
+ this.options.output(cue, meta2);
1573
1723
  }
1574
1724
  }
1575
1725
  };
@@ -2902,10 +3052,11 @@ var Mediabunny = (() => {
2902
3052
  ]);
2903
3053
  };
2904
3054
  var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize });
2905
- var moov = (trackDatas, creationTime, fragmented = false) => box("moov", void 0, [
2906
- mvhd(creationTime, trackDatas),
2907
- ...trackDatas.map((x) => trak(x, creationTime)),
2908
- fragmented ? mvex(trackDatas) : null
3055
+ var moov = (muxer, fragmented = false) => box("moov", void 0, [
3056
+ mvhd(muxer.creationTime, muxer.trackDatas),
3057
+ ...muxer.trackDatas.map((x) => trak(x, muxer.creationTime)),
3058
+ fragmented ? mvex(muxer.trackDatas) : null,
3059
+ udta(muxer)
2909
3060
  ]);
2910
3061
  var mvhd = (creationTime, trackDatas) => {
2911
3062
  const duration = intoTimescale(Math.max(
@@ -2947,7 +3098,8 @@ var Mediabunny = (() => {
2947
3098
  tkhd(trackData, creationTime),
2948
3099
  mdia(trackData, creationTime),
2949
3100
  trackMetadata.name !== void 0 ? box("udta", void 0, [
2950
- box("\xA9nam", [
3101
+ box("name", [
3102
+ // VLC (and Mediabunny) also recognize ©nam
2951
3103
  ...textEncoder.encode(trackMetadata.name)
2952
3104
  ])
2953
3105
  ]) : null
@@ -3010,11 +3162,6 @@ var Mediabunny = (() => {
3010
3162
  );
3011
3163
  const needsU64 = !isU32(creationTime) || !isU32(localDuration);
3012
3164
  const u32OrU64 = needsU64 ? u64 : u32;
3013
- let language = 0;
3014
- for (const character of trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE) {
3015
- language <<= 5;
3016
- language += character.charCodeAt(0) - 96;
3017
- }
3018
3165
  return fullBox("mdhd", +needsU64, 0, [
3019
3166
  u32OrU64(creationTime),
3020
3167
  // Creation time
@@ -3024,7 +3171,7 @@ var Mediabunny = (() => {
3024
3171
  // Timescale
3025
3172
  u32OrU64(localDuration),
3026
3173
  // Duration
3027
- u16(language),
3174
+ u16(getLanguageCodeInt(trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE)),
3028
3175
  // Language
3029
3176
  u16(0)
3030
3177
  // Quality
@@ -3040,12 +3187,12 @@ var Mediabunny = (() => {
3040
3187
  audio: "MediabunnySoundHandler",
3041
3188
  subtitle: "MediabunnyTextHandler"
3042
3189
  };
3043
- var hdlr = (hasComponentType, handlerType, name) => fullBox("hdlr", 0, 0, [
3190
+ var hdlr = (hasComponentType, handlerType, name, manufacturer = "\0\0\0\0") => fullBox("hdlr", 0, 0, [
3044
3191
  hasComponentType ? ascii("mhlr") : u32(0),
3045
3192
  // Component type
3046
3193
  ascii(handlerType),
3047
3194
  // Component subtype
3048
- u32(0),
3195
+ ascii(manufacturer),
3049
3196
  // Component manufacturer
3050
3197
  u32(0),
3051
3198
  // Component flags
@@ -3740,6 +3887,303 @@ var Mediabunny = (() => {
3740
3887
  box("payl", [...textEncoder.encode(payload)])
3741
3888
  ]);
3742
3889
  var vtta = (notes) => box("vtta", [...textEncoder.encode(notes)]);
3890
+ var udta = (muxer) => {
3891
+ const boxes = [];
3892
+ if (muxer.isQuickTime) {
3893
+ addQuickTimeMetadataTagBoxes(boxes, muxer.output._metadataTags);
3894
+ } else {
3895
+ const metaBox = meta(muxer.output._metadataTags);
3896
+ if (metaBox) {
3897
+ boxes.push(metaBox);
3898
+ }
3899
+ }
3900
+ if (boxes.length === 0) {
3901
+ return null;
3902
+ }
3903
+ return box("udta", void 0, boxes);
3904
+ };
3905
+ var addQuickTimeMetadataTagBoxes = (boxes, tags) => {
3906
+ for (const { key, value } of keyValueIterator(tags)) {
3907
+ switch (key) {
3908
+ case "title":
3909
+ {
3910
+ boxes.push(metadataTagStringBoxShort("\xA9nam", value));
3911
+ }
3912
+ ;
3913
+ break;
3914
+ case "description":
3915
+ {
3916
+ boxes.push(metadataTagStringBoxShort("\xA9des", value));
3917
+ }
3918
+ ;
3919
+ break;
3920
+ case "artist":
3921
+ {
3922
+ boxes.push(metadataTagStringBoxShort("\xA9ART", value));
3923
+ }
3924
+ ;
3925
+ break;
3926
+ case "album":
3927
+ {
3928
+ boxes.push(metadataTagStringBoxShort("\xA9alb", value));
3929
+ }
3930
+ ;
3931
+ break;
3932
+ case "albumArtist":
3933
+ {
3934
+ boxes.push(metadataTagStringBoxShort("albr", value));
3935
+ }
3936
+ ;
3937
+ break;
3938
+ case "genre":
3939
+ {
3940
+ boxes.push(metadataTagStringBoxShort("\xA9gen", value));
3941
+ }
3942
+ ;
3943
+ break;
3944
+ case "date":
3945
+ {
3946
+ boxes.push(metadataTagStringBoxShort("\xA9day", value.toISOString().slice(0, 10)));
3947
+ }
3948
+ ;
3949
+ break;
3950
+ case "comment":
3951
+ {
3952
+ boxes.push(metadataTagStringBoxShort("\xA9cmt", value));
3953
+ }
3954
+ ;
3955
+ break;
3956
+ case "lyrics":
3957
+ {
3958
+ boxes.push(metadataTagStringBoxShort("\xA9lyr", value));
3959
+ }
3960
+ ;
3961
+ break;
3962
+ case "raw":
3963
+ {
3964
+ }
3965
+ ;
3966
+ break;
3967
+ case "discNumber":
3968
+ case "discsTotal":
3969
+ case "trackNumber":
3970
+ case "tracksTotal":
3971
+ case "images":
3972
+ {
3973
+ }
3974
+ ;
3975
+ break;
3976
+ default:
3977
+ assertNever(key);
3978
+ }
3979
+ }
3980
+ if (tags.raw) {
3981
+ for (const key in tags.raw) {
3982
+ const value = tags.raw[key];
3983
+ if (value == null || key.length !== 4 || boxes.some((x) => x.type === key)) {
3984
+ continue;
3985
+ }
3986
+ if (typeof value === "string") {
3987
+ boxes.push(metadataTagStringBoxShort(key, value));
3988
+ } else if (value instanceof Uint8Array) {
3989
+ boxes.push(box(key, Array.from(value)));
3990
+ }
3991
+ }
3992
+ }
3993
+ };
3994
+ var metadataTagStringBoxShort = (name, value) => {
3995
+ const encoded = textEncoder.encode(value);
3996
+ return box(name, [
3997
+ u16(encoded.length),
3998
+ u16(getLanguageCodeInt("und")),
3999
+ Array.from(encoded)
4000
+ ]);
4001
+ };
4002
+ var DATA_BOX_MIME_TYPE_MAP = {
4003
+ "image/jpeg": 13,
4004
+ "image/png": 14,
4005
+ "image/bmp": 27
4006
+ };
4007
+ var meta = (tags) => {
4008
+ const boxes = [];
4009
+ for (const { key, value } of keyValueIterator(tags)) {
4010
+ switch (key) {
4011
+ case "title":
4012
+ {
4013
+ boxes.push(metadataTagStringBoxLong("\xA9nam", value));
4014
+ }
4015
+ ;
4016
+ break;
4017
+ case "description":
4018
+ {
4019
+ boxes.push(metadataTagStringBoxLong("\xA9des", value));
4020
+ }
4021
+ ;
4022
+ break;
4023
+ case "artist":
4024
+ {
4025
+ boxes.push(metadataTagStringBoxLong("\xA9ART", value));
4026
+ }
4027
+ ;
4028
+ break;
4029
+ case "album":
4030
+ {
4031
+ boxes.push(metadataTagStringBoxLong("\xA9alb", value));
4032
+ }
4033
+ ;
4034
+ break;
4035
+ case "albumArtist":
4036
+ {
4037
+ boxes.push(metadataTagStringBoxLong("aART", value));
4038
+ }
4039
+ ;
4040
+ break;
4041
+ case "comment":
4042
+ {
4043
+ boxes.push(metadataTagStringBoxLong("\xA9cmt", value));
4044
+ }
4045
+ ;
4046
+ break;
4047
+ case "genre":
4048
+ {
4049
+ boxes.push(metadataTagStringBoxLong("\xA9gen", value));
4050
+ }
4051
+ ;
4052
+ break;
4053
+ case "lyrics":
4054
+ {
4055
+ boxes.push(metadataTagStringBoxLong("\xA9lyr", value));
4056
+ }
4057
+ ;
4058
+ break;
4059
+ case "date":
4060
+ {
4061
+ boxes.push(metadataTagStringBoxLong("\xA9day", value.toISOString().slice(0, 10)));
4062
+ }
4063
+ ;
4064
+ break;
4065
+ case "images":
4066
+ {
4067
+ for (const image of value) {
4068
+ if (image.kind !== "coverFront") {
4069
+ continue;
4070
+ }
4071
+ boxes.push(box("covr", void 0, [
4072
+ box("data", [
4073
+ u32(DATA_BOX_MIME_TYPE_MAP[image.mimeType] ?? 0),
4074
+ // Type indicator
4075
+ u32(0),
4076
+ // Locale indicator
4077
+ Array.from(image.data)
4078
+ // Kinda slow, hopefully temp
4079
+ ])
4080
+ ]));
4081
+ }
4082
+ }
4083
+ ;
4084
+ break;
4085
+ case "trackNumber":
4086
+ {
4087
+ boxes.push(box("trkn", void 0, [
4088
+ box("data", [
4089
+ u32(0),
4090
+ // 8 bytes empty
4091
+ u32(0),
4092
+ u16(0),
4093
+ // Empty
4094
+ u16(value),
4095
+ u16(tags.tracksTotal ?? 0),
4096
+ u16(0)
4097
+ // Empty
4098
+ ])
4099
+ ]));
4100
+ }
4101
+ ;
4102
+ break;
4103
+ case "discNumber":
4104
+ {
4105
+ boxes.push(box("disc", void 0, [
4106
+ box("data", [
4107
+ u32(0),
4108
+ // 8 bytes empty
4109
+ u32(0),
4110
+ u16(0),
4111
+ // Empty
4112
+ u16(value),
4113
+ u16(tags.discsTotal ?? 0),
4114
+ u16(0)
4115
+ // Empty
4116
+ ])
4117
+ ]));
4118
+ }
4119
+ ;
4120
+ break;
4121
+ case "tracksTotal":
4122
+ case "discsTotal":
4123
+ {
4124
+ }
4125
+ ;
4126
+ break;
4127
+ case "raw":
4128
+ {
4129
+ }
4130
+ ;
4131
+ break;
4132
+ default:
4133
+ assertNever(key);
4134
+ }
4135
+ }
4136
+ if (tags.raw) {
4137
+ for (const key in tags.raw) {
4138
+ const value = tags.raw[key];
4139
+ if (value == null || key.length !== 4 || boxes.some((x) => x.type === key)) {
4140
+ continue;
4141
+ }
4142
+ if (typeof value === "string") {
4143
+ boxes.push(metadataTagStringBoxLong(key, value));
4144
+ } else if (value instanceof Uint8Array) {
4145
+ boxes.push(box(key, void 0, [
4146
+ box("data", [
4147
+ u32(0),
4148
+ // Type indicator
4149
+ u32(0),
4150
+ // Locale indicator
4151
+ Array.from(value)
4152
+ ])
4153
+ ]));
4154
+ } else if (value instanceof RichImageData) {
4155
+ boxes.push(box(key, void 0, [
4156
+ box("data", [
4157
+ u32(DATA_BOX_MIME_TYPE_MAP[value.mimeType] ?? 0),
4158
+ // Type indicator
4159
+ u32(0),
4160
+ // Locale indicator
4161
+ Array.from(value.data)
4162
+ // Kinda slow, hopefully temp
4163
+ ])
4164
+ ]));
4165
+ }
4166
+ }
4167
+ }
4168
+ if (boxes.length === 0) {
4169
+ return null;
4170
+ }
4171
+ return fullBox("meta", 0, 0, void 0, [
4172
+ hdlr(false, "mdir", "", "appl"),
4173
+ box("ilst", void 0, boxes)
4174
+ ]);
4175
+ };
4176
+ var metadataTagStringBoxLong = (name, value) => {
4177
+ return box(name, void 0, [
4178
+ box("data", [
4179
+ u32(1),
4180
+ // Type indicator (UTF-8)
4181
+ u32(0),
4182
+ // Locale indicator
4183
+ ...textEncoder.encode(value)
4184
+ ])
4185
+ ]);
4186
+ };
3743
4187
  var VIDEO_CODEC_TO_BOX_NAME = {
3744
4188
  avc: "avc1",
3745
4189
  hevc: "hvc1",
@@ -3887,6 +4331,16 @@ var Mediabunny = (() => {
3887
4331
  var SUBTITLE_CODEC_TO_CONFIGURATION_BOX = {
3888
4332
  webvtt: vttC
3889
4333
  };
4334
+ var getLanguageCodeInt = (code) => {
4335
+ assert(code.length === 3);
4336
+ ;
4337
+ let language = 0;
4338
+ for (let i = 0; i < 3; i++) {
4339
+ language <<= 5;
4340
+ language += code.charCodeAt(i) - 96;
4341
+ }
4342
+ return language;
4343
+ };
3890
4344
 
3891
4345
  // src/writer.ts
3892
4346
  var Writer = class {
@@ -4522,6 +4976,36 @@ var Mediabunny = (() => {
4522
4976
  }
4523
4977
  return result;
4524
4978
  };
4979
+ var readMetadataStringShort = (slice) => {
4980
+ const stringLength = readU16Be(slice);
4981
+ slice.skip(2);
4982
+ return textDecoder.decode(readBytes(slice, stringLength));
4983
+ };
4984
+ var readDataBox = (slice) => {
4985
+ const header = readBoxHeader(slice);
4986
+ if (!header || header.name !== "data") {
4987
+ return null;
4988
+ }
4989
+ const typeIndicator = readU32Be(slice);
4990
+ slice.skip(4);
4991
+ const data = readBytes(slice, header.contentSize - 8);
4992
+ switch (typeIndicator) {
4993
+ case 1:
4994
+ return textDecoder.decode(data);
4995
+ // UTF-8
4996
+ case 2:
4997
+ return new TextDecoder("utf-16be").decode(data);
4998
+ // UTF-16-BE
4999
+ case 13:
5000
+ return new RichImageData(data, "image/jpeg");
5001
+ case 14:
5002
+ return new RichImageData(data, "image/png");
5003
+ case 27:
5004
+ return new RichImageData(data, "image/bmp");
5005
+ default:
5006
+ return data;
5007
+ }
5008
+ };
4525
5009
 
4526
5010
  // src/isobmff/isobmff-muxer.ts
4527
5011
  var GLOBAL_TIMESCALE = 1e3;
@@ -4538,7 +5022,7 @@ var Mediabunny = (() => {
4538
5022
  const value = timeInSeconds * timescale;
4539
5023
  return round ? Math.round(value) : value;
4540
5024
  };
4541
- var IsobmffMuxer = class extends Muxer {
5025
+ var IsobmffMuxer2 = class extends Muxer {
4542
5026
  constructor(output, format) {
4543
5027
  super(output);
4544
5028
  this.auxTarget = new BufferTarget();
@@ -4623,15 +5107,15 @@ var Mediabunny = (() => {
4623
5107
  codecStrings
4624
5108
  });
4625
5109
  }
4626
- getVideoTrackData(track, packet, meta) {
5110
+ getVideoTrackData(track, packet, meta2) {
4627
5111
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
4628
5112
  if (existingTrackData) {
4629
5113
  return existingTrackData;
4630
5114
  }
4631
- validateVideoChunkMetadata(meta);
4632
- assert(meta);
4633
- assert(meta.decoderConfig);
4634
- const decoderConfig = { ...meta.decoderConfig };
5115
+ validateVideoChunkMetadata(meta2);
5116
+ assert(meta2);
5117
+ assert(meta2.decoderConfig);
5118
+ const decoderConfig = { ...meta2.decoderConfig };
4635
5119
  assert(decoderConfig.codedWidth !== void 0);
4636
5120
  assert(decoderConfig.codedHeight !== void 0);
4637
5121
  let requiresAnnexBTransformation = false;
@@ -4684,25 +5168,25 @@ var Mediabunny = (() => {
4684
5168
  }
4685
5169
  return newTrackData;
4686
5170
  }
4687
- getAudioTrackData(track, meta) {
5171
+ getAudioTrackData(track, meta2) {
4688
5172
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
4689
5173
  if (existingTrackData) {
4690
5174
  return existingTrackData;
4691
5175
  }
4692
- validateAudioChunkMetadata(meta);
4693
- assert(meta);
4694
- assert(meta.decoderConfig);
5176
+ validateAudioChunkMetadata(meta2);
5177
+ assert(meta2);
5178
+ assert(meta2.decoderConfig);
4695
5179
  const newTrackData = {
4696
5180
  muxer: this,
4697
5181
  track,
4698
5182
  type: "audio",
4699
5183
  info: {
4700
- numberOfChannels: meta.decoderConfig.numberOfChannels,
4701
- sampleRate: meta.decoderConfig.sampleRate,
4702
- decoderConfig: meta.decoderConfig,
5184
+ numberOfChannels: meta2.decoderConfig.numberOfChannels,
5185
+ sampleRate: meta2.decoderConfig.sampleRate,
5186
+ decoderConfig: meta2.decoderConfig,
4703
5187
  requiresPcmTransformation: !this.isFragmented && PCM_AUDIO_CODECS.includes(track.source._codec)
4704
5188
  },
4705
- timescale: meta.decoderConfig.sampleRate,
5189
+ timescale: meta2.decoderConfig.sampleRate,
4706
5190
  samples: [],
4707
5191
  sampleQueue: [],
4708
5192
  timestampProcessingQueue: [],
@@ -4721,20 +5205,20 @@ var Mediabunny = (() => {
4721
5205
  }
4722
5206
  return newTrackData;
4723
5207
  }
4724
- getSubtitleTrackData(track, meta) {
5208
+ getSubtitleTrackData(track, meta2) {
4725
5209
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
4726
5210
  if (existingTrackData) {
4727
5211
  return existingTrackData;
4728
5212
  }
4729
- validateSubtitleMetadata(meta);
4730
- assert(meta);
4731
- assert(meta.config);
5213
+ validateSubtitleMetadata(meta2);
5214
+ assert(meta2);
5215
+ assert(meta2.config);
4732
5216
  const newTrackData = {
4733
5217
  muxer: this,
4734
5218
  track,
4735
5219
  type: "subtitle",
4736
5220
  info: {
4737
- config: meta.config
5221
+ config: meta2.config
4738
5222
  },
4739
5223
  timescale: 1e3,
4740
5224
  // Reasonable
@@ -4760,10 +5244,10 @@ var Mediabunny = (() => {
4760
5244
  }
4761
5245
  return newTrackData;
4762
5246
  }
4763
- async addEncodedVideoPacket(track, packet, meta) {
5247
+ async addEncodedVideoPacket(track, packet, meta2) {
4764
5248
  const release = await this.mutex.acquire();
4765
5249
  try {
4766
- const trackData = this.getVideoTrackData(track, packet, meta);
5250
+ const trackData = this.getVideoTrackData(track, packet, meta2);
4767
5251
  let packetData = packet.data;
4768
5252
  if (trackData.info.requiresAnnexBTransformation) {
4769
5253
  const transformedData = transformAnnexBToLengthPrefixed(packetData);
@@ -4791,10 +5275,10 @@ var Mediabunny = (() => {
4791
5275
  release();
4792
5276
  }
4793
5277
  }
4794
- async addEncodedAudioPacket(track, packet, meta) {
5278
+ async addEncodedAudioPacket(track, packet, meta2) {
4795
5279
  const release = await this.mutex.acquire();
4796
5280
  try {
4797
- const trackData = this.getAudioTrackData(track, meta);
5281
+ const trackData = this.getAudioTrackData(track, meta2);
4798
5282
  const timestamp = this.validateAndNormalizeTimestamp(
4799
5283
  trackData.track,
4800
5284
  packet.timestamp,
@@ -4836,10 +5320,10 @@ var Mediabunny = (() => {
4836
5320
  await this.registerSample(trackData, paddingSample);
4837
5321
  }
4838
5322
  }
4839
- async addSubtitleCue(track, cue, meta) {
5323
+ async addSubtitleCue(track, cue, meta2) {
4840
5324
  const release = await this.mutex.acquire();
4841
5325
  try {
4842
- const trackData = this.getSubtitleTrackData(track, meta);
5326
+ const trackData = this.getSubtitleTrackData(track, meta2);
4843
5327
  this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true);
4844
5328
  if (track.source._codec === "webvtt") {
4845
5329
  trackData.cueQueue.push(cue);
@@ -5154,7 +5638,7 @@ var Mediabunny = (() => {
5154
5638
  if (this.format._options.onMoov) {
5155
5639
  this.writer.startTrackingWrites();
5156
5640
  }
5157
- const movieBox = moov(this.trackDatas, this.creationTime, true);
5641
+ const movieBox = moov(this, true);
5158
5642
  this.boxWriter.writeBox(movieBox);
5159
5643
  if (this.format._options.onMoov) {
5160
5644
  const { data, start } = this.writer.stopTrackingWrites();
@@ -5260,7 +5744,7 @@ var Mediabunny = (() => {
5260
5744
  assert(this.mdat);
5261
5745
  let mdatSize;
5262
5746
  for (let i = 0; i < 2; i++) {
5263
- const movieBox2 = moov(this.trackDatas, this.creationTime);
5747
+ const movieBox2 = moov(this);
5264
5748
  const movieBoxSize = this.boxWriter.measureBox(movieBox2);
5265
5749
  mdatSize = this.boxWriter.measureBox(this.mdat);
5266
5750
  let currentChunkPos = this.writer.getPos() + movieBoxSize + mdatSize;
@@ -5278,7 +5762,7 @@ var Mediabunny = (() => {
5278
5762
  if (this.format._options.onMoov) {
5279
5763
  this.writer.startTrackingWrites();
5280
5764
  }
5281
- const movieBox = moov(this.trackDatas, this.creationTime);
5765
+ const movieBox = moov(this);
5282
5766
  this.boxWriter.writeBox(movieBox);
5283
5767
  if (this.format._options.onMoov) {
5284
5768
  const { data, start } = this.writer.stopTrackingWrites();
@@ -5322,7 +5806,7 @@ var Mediabunny = (() => {
5322
5806
  if (this.format._options.onMoov) {
5323
5807
  this.writer.startTrackingWrites();
5324
5808
  }
5325
- const movieBox = moov(this.trackDatas, this.creationTime);
5809
+ const movieBox = moov(this);
5326
5810
  this.boxWriter.writeBox(movieBox);
5327
5811
  if (this.format._options.onMoov) {
5328
5812
  const { data, start } = this.writer.stopTrackingWrites();
@@ -5767,7 +6251,7 @@ var Mediabunny = (() => {
5767
6251
  // src/matroska/matroska-muxer.ts
5768
6252
  var MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
5769
6253
  var MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
5770
- var APP_NAME = "https://github.com/Vanilagy/mediabunny";
6254
+ var APP_NAME = "Mediabunny";
5771
6255
  var SEGMENT_SIZE_BYTES = 6;
5772
6256
  var CLUSTER_SIZE_BYTES = 5;
5773
6257
  var TRACK_TYPE_MAP = {
@@ -5784,6 +6268,8 @@ var Mediabunny = (() => {
5784
6268
  this.segmentInfo = null;
5785
6269
  this.seekHead = null;
5786
6270
  this.tracksElement = null;
6271
+ this.tagsElement = null;
6272
+ this.attachmentsElement = null;
5787
6273
  this.segmentDuration = null;
5788
6274
  this.cues = null;
5789
6275
  this.currentCluster = null;
@@ -5801,9 +6287,6 @@ var Mediabunny = (() => {
5801
6287
  async start() {
5802
6288
  const release = await this.mutex.acquire();
5803
6289
  this.writeEBMLHeader();
5804
- if (!this.format._options.appendOnly) {
5805
- this.createSeekHead();
5806
- }
5807
6290
  this.createSegmentInfo();
5808
6291
  this.createCues();
5809
6292
  await this.writer.flush();
@@ -5832,23 +6315,56 @@ var Mediabunny = (() => {
5832
6315
  * Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
5833
6316
  * relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
5834
6317
  */
5835
- createSeekHead() {
6318
+ maybeCreateSeekHead(writeOffsets) {
6319
+ if (this.format._options.appendOnly) {
6320
+ return;
6321
+ }
5836
6322
  const kaxCues = new Uint8Array([28, 83, 187, 107]);
5837
6323
  const kaxInfo = new Uint8Array([21, 73, 169, 102]);
5838
6324
  const kaxTracks = new Uint8Array([22, 84, 174, 107]);
6325
+ const kaxAttachments = new Uint8Array([25, 65, 164, 105]);
6326
+ const kaxTags = new Uint8Array([18, 84, 195, 103]);
5839
6327
  const seekHead = { id: 290298740 /* SeekHead */, data: [
5840
6328
  { id: 19899 /* Seek */, data: [
5841
6329
  { id: 21419 /* SeekID */, data: kaxCues },
5842
- { id: 21420 /* SeekPosition */, size: 5, data: 0 }
6330
+ {
6331
+ id: 21420 /* SeekPosition */,
6332
+ size: 5,
6333
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.cues) - this.segmentDataOffset : 0
6334
+ }
5843
6335
  ] },
5844
6336
  { id: 19899 /* Seek */, data: [
5845
6337
  { id: 21419 /* SeekID */, data: kaxInfo },
5846
- { id: 21420 /* SeekPosition */, size: 5, data: 0 }
6338
+ {
6339
+ id: 21420 /* SeekPosition */,
6340
+ size: 5,
6341
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.segmentInfo) - this.segmentDataOffset : 0
6342
+ }
5847
6343
  ] },
5848
6344
  { id: 19899 /* Seek */, data: [
5849
6345
  { id: 21419 /* SeekID */, data: kaxTracks },
5850
- { id: 21420 /* SeekPosition */, size: 5, data: 0 }
5851
- ] }
6346
+ {
6347
+ id: 21420 /* SeekPosition */,
6348
+ size: 5,
6349
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.tracksElement) - this.segmentDataOffset : 0
6350
+ }
6351
+ ] },
6352
+ this.attachmentsElement ? { id: 19899 /* Seek */, data: [
6353
+ { id: 21419 /* SeekID */, data: kaxAttachments },
6354
+ {
6355
+ id: 21420 /* SeekPosition */,
6356
+ size: 5,
6357
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.attachmentsElement) - this.segmentDataOffset : 0
6358
+ }
6359
+ ] } : null,
6360
+ this.tagsElement ? { id: 19899 /* Seek */, data: [
6361
+ { id: 21419 /* SeekID */, data: kaxTags },
6362
+ {
6363
+ id: 21420 /* SeekPosition */,
6364
+ size: 5,
6365
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.tagsElement) - this.segmentDataOffset : 0
6366
+ }
6367
+ ] } : null
5852
6368
  ] };
5853
6369
  this.seekHead = seekHead;
5854
6370
  }
@@ -5971,48 +6487,221 @@ var Mediabunny = (() => {
5971
6487
  { id: 25506 /* CodecPrivate */, data: textEncoder.encode(trackData.info.config.description) }
5972
6488
  ];
5973
6489
  }
5974
- createSegment() {
5975
- const segment = {
5976
- id: 408125543 /* Segment */,
5977
- size: this.format._options.appendOnly ? -1 : SEGMENT_SIZE_BYTES,
5978
- data: [
5979
- !this.format._options.appendOnly ? this.seekHead : null,
5980
- this.segmentInfo,
5981
- this.tracksElement
5982
- ]
6490
+ maybeCreateTags() {
6491
+ const simpleTags = [];
6492
+ const addSimpleTag = (key, value) => {
6493
+ simpleTags.push({ id: 26568 /* SimpleTag */, data: [
6494
+ { id: 17827 /* TagName */, data: new EBMLUnicodeString(key) },
6495
+ typeof value === "string" ? { id: 17543 /* TagString */, data: new EBMLUnicodeString(value) } : { id: 17541 /* TagBinary */, data: value }
6496
+ ] });
5983
6497
  };
5984
- this.segment = segment;
5985
- if (this.format._options.onSegmentHeader) {
5986
- this.writer.startTrackingWrites();
5987
- }
5988
- this.ebmlWriter.writeEBML(segment);
5989
- if (this.format._options.onSegmentHeader) {
5990
- const { data, start } = this.writer.stopTrackingWrites();
5991
- this.format._options.onSegmentHeader(data, start);
5992
- }
5993
- }
5994
- createCues() {
5995
- this.cues = { id: 475249515 /* Cues */, data: [] };
5996
- }
5997
- get segmentDataOffset() {
5998
- assert(this.segment);
5999
- return this.ebmlWriter.dataOffsets.get(this.segment);
6000
- }
6001
- allTracksAreKnown() {
6002
- for (const track of this.output._tracks) {
6003
- if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
6004
- return false;
6005
- }
6006
- }
6007
- return true;
6008
- }
6009
- async getMimeType() {
6010
- await this.allTracksKnown.promise;
6011
- const codecStrings = this.trackDatas.map((trackData) => {
6012
- if (trackData.type === "video") {
6013
- return trackData.info.decoderConfig.codec;
6014
- } else if (trackData.type === "audio") {
6015
- return trackData.info.decoderConfig.codec;
6498
+ const metadataTags = this.output._metadataTags;
6499
+ const writtenTags = /* @__PURE__ */ new Set();
6500
+ for (const { key, value } of keyValueIterator(metadataTags)) {
6501
+ switch (key) {
6502
+ case "title":
6503
+ {
6504
+ addSimpleTag("TITLE", value);
6505
+ writtenTags.add("TITLE");
6506
+ }
6507
+ ;
6508
+ break;
6509
+ case "description":
6510
+ {
6511
+ addSimpleTag("DESCRIPTION", value);
6512
+ writtenTags.add("DESCRIPTION");
6513
+ }
6514
+ ;
6515
+ break;
6516
+ case "artist":
6517
+ {
6518
+ addSimpleTag("ARTIST", value);
6519
+ writtenTags.add("ARTIST");
6520
+ }
6521
+ ;
6522
+ break;
6523
+ case "album":
6524
+ {
6525
+ addSimpleTag("ALBUM", value);
6526
+ writtenTags.add("ALBUM");
6527
+ }
6528
+ ;
6529
+ break;
6530
+ case "albumArtist":
6531
+ {
6532
+ addSimpleTag("ALBUM_ARTIST", value);
6533
+ writtenTags.add("ALBUM_ARTIST");
6534
+ }
6535
+ ;
6536
+ break;
6537
+ case "genre":
6538
+ {
6539
+ addSimpleTag("GENRE", value);
6540
+ writtenTags.add("GENRE");
6541
+ }
6542
+ ;
6543
+ break;
6544
+ case "comment":
6545
+ {
6546
+ addSimpleTag("COMMENT", value);
6547
+ writtenTags.add("COMMENT");
6548
+ }
6549
+ ;
6550
+ break;
6551
+ case "lyrics":
6552
+ {
6553
+ addSimpleTag("LYRICS", value);
6554
+ writtenTags.add("LYRICS");
6555
+ }
6556
+ ;
6557
+ break;
6558
+ case "date":
6559
+ {
6560
+ addSimpleTag("DATE", value.toISOString().slice(0, 10));
6561
+ writtenTags.add("DATE");
6562
+ }
6563
+ ;
6564
+ break;
6565
+ case "trackNumber":
6566
+ {
6567
+ const string = metadataTags.tracksTotal !== void 0 ? `${value}/${metadataTags.tracksTotal}` : value.toString();
6568
+ addSimpleTag("PART_NUMBER", string);
6569
+ writtenTags.add("PART_NUMBER");
6570
+ }
6571
+ ;
6572
+ break;
6573
+ case "discNumber":
6574
+ {
6575
+ const string = metadataTags.discsTotal !== void 0 ? `${value}/${metadataTags.discsTotal}` : value.toString();
6576
+ addSimpleTag("DISC", string);
6577
+ writtenTags.add("DISC");
6578
+ }
6579
+ ;
6580
+ break;
6581
+ case "tracksTotal":
6582
+ case "discsTotal":
6583
+ {
6584
+ }
6585
+ ;
6586
+ break;
6587
+ case "images":
6588
+ case "raw":
6589
+ {
6590
+ }
6591
+ ;
6592
+ break;
6593
+ default:
6594
+ assertNever(key);
6595
+ }
6596
+ }
6597
+ if (metadataTags.raw) {
6598
+ for (const key in metadataTags.raw) {
6599
+ const value = metadataTags.raw[key];
6600
+ if (value == null || writtenTags.has(key)) {
6601
+ continue;
6602
+ }
6603
+ if (typeof value === "string" || value instanceof Uint8Array) {
6604
+ addSimpleTag(key, value);
6605
+ }
6606
+ }
6607
+ }
6608
+ if (simpleTags.length === 0) {
6609
+ return;
6610
+ }
6611
+ this.tagsElement = {
6612
+ id: 307544935 /* Tags */,
6613
+ data: [{ id: 29555 /* Tag */, data: [
6614
+ { id: 25536 /* Targets */, data: [
6615
+ { id: 26826 /* TargetTypeValue */, data: 50 },
6616
+ { id: 25546 /* TargetType */, data: "MOVIE" }
6617
+ ] },
6618
+ ...simpleTags
6619
+ ] }]
6620
+ };
6621
+ }
6622
+ maybeCreateAttachments() {
6623
+ const metadataTags = this.output._metadataTags;
6624
+ if (!metadataTags.images || metadataTags.images.length === 0) {
6625
+ return;
6626
+ }
6627
+ const existingFileUids = /* @__PURE__ */ new Set();
6628
+ this.attachmentsElement = { id: 423732329 /* Attachments */, data: metadataTags.images.map((image) => {
6629
+ let imageName = image.name;
6630
+ if (imageName === void 0) {
6631
+ const baseName = image.kind === "coverFront" ? "cover" : image.kind === "coverBack" ? "back" : "image";
6632
+ imageName = baseName + (imageMimeTypeToExtension(image.mimeType) ?? "");
6633
+ }
6634
+ let fileUid;
6635
+ while (true) {
6636
+ fileUid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
6637
+ if (fileUid !== 0 && !existingFileUids.has(fileUid)) {
6638
+ break;
6639
+ }
6640
+ }
6641
+ existingFileUids.add(fileUid);
6642
+ return {
6643
+ id: 24999 /* AttachedFile */,
6644
+ data: [
6645
+ image.description !== void 0 ? { id: 18046 /* FileDescription */, data: new EBMLUnicodeString(image.description) } : null,
6646
+ { id: 18030 /* FileName */, data: new EBMLUnicodeString(imageName) },
6647
+ { id: 18016 /* FileMediaType */, data: image.mimeType },
6648
+ { id: 18012 /* FileData */, data: image.data },
6649
+ { id: 18094 /* FileUID */, data: fileUid }
6650
+ ]
6651
+ };
6652
+ }) };
6653
+ }
6654
+ createSegment() {
6655
+ this.createTracks();
6656
+ this.maybeCreateTags();
6657
+ this.maybeCreateAttachments();
6658
+ this.maybeCreateSeekHead(false);
6659
+ const segment = {
6660
+ id: 408125543 /* Segment */,
6661
+ size: this.format._options.appendOnly ? -1 : SEGMENT_SIZE_BYTES,
6662
+ data: [
6663
+ this.seekHead,
6664
+ // null if append-only
6665
+ this.segmentInfo,
6666
+ this.tracksElement,
6667
+ // Matroska spec says put this at the end of the file, but I think placing it before the first cluster
6668
+ // makes more sense, and FFmpeg agrees (argumentum ad ffmpegum fallacy)
6669
+ this.attachmentsElement,
6670
+ this.tagsElement
6671
+ ]
6672
+ };
6673
+ this.segment = segment;
6674
+ if (this.format._options.onSegmentHeader) {
6675
+ this.writer.startTrackingWrites();
6676
+ }
6677
+ this.ebmlWriter.writeEBML(segment);
6678
+ if (this.format._options.onSegmentHeader) {
6679
+ const { data, start } = this.writer.stopTrackingWrites();
6680
+ this.format._options.onSegmentHeader(data, start);
6681
+ }
6682
+ }
6683
+ createCues() {
6684
+ this.cues = { id: 475249515 /* Cues */, data: [] };
6685
+ }
6686
+ get segmentDataOffset() {
6687
+ assert(this.segment);
6688
+ return this.ebmlWriter.dataOffsets.get(this.segment);
6689
+ }
6690
+ allTracksAreKnown() {
6691
+ for (const track of this.output._tracks) {
6692
+ if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
6693
+ return false;
6694
+ }
6695
+ }
6696
+ return true;
6697
+ }
6698
+ async getMimeType() {
6699
+ await this.allTracksKnown.promise;
6700
+ const codecStrings = this.trackDatas.map((trackData) => {
6701
+ if (trackData.type === "video") {
6702
+ return trackData.info.decoderConfig.codec;
6703
+ } else if (trackData.type === "audio") {
6704
+ return trackData.info.decoderConfig.codec;
6016
6705
  } else {
6017
6706
  const map = {
6018
6707
  webvtt: "wvtt"
@@ -6027,23 +6716,23 @@ var Mediabunny = (() => {
6027
6716
  codecStrings
6028
6717
  });
6029
6718
  }
6030
- getVideoTrackData(track, meta) {
6719
+ getVideoTrackData(track, meta2) {
6031
6720
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
6032
6721
  if (existingTrackData) {
6033
6722
  return existingTrackData;
6034
6723
  }
6035
- validateVideoChunkMetadata(meta);
6036
- assert(meta);
6037
- assert(meta.decoderConfig);
6038
- assert(meta.decoderConfig.codedWidth !== void 0);
6039
- assert(meta.decoderConfig.codedHeight !== void 0);
6724
+ validateVideoChunkMetadata(meta2);
6725
+ assert(meta2);
6726
+ assert(meta2.decoderConfig);
6727
+ assert(meta2.decoderConfig.codedWidth !== void 0);
6728
+ assert(meta2.decoderConfig.codedHeight !== void 0);
6040
6729
  const newTrackData = {
6041
6730
  track,
6042
6731
  type: "video",
6043
6732
  info: {
6044
- width: meta.decoderConfig.codedWidth,
6045
- height: meta.decoderConfig.codedHeight,
6046
- decoderConfig: meta.decoderConfig
6733
+ width: meta2.decoderConfig.codedWidth,
6734
+ height: meta2.decoderConfig.codedHeight,
6735
+ decoderConfig: meta2.decoderConfig
6047
6736
  },
6048
6737
  chunkQueue: [],
6049
6738
  lastWrittenMsTimestamp: null
@@ -6070,21 +6759,21 @@ var Mediabunny = (() => {
6070
6759
  }
6071
6760
  return newTrackData;
6072
6761
  }
6073
- getAudioTrackData(track, meta) {
6762
+ getAudioTrackData(track, meta2) {
6074
6763
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
6075
6764
  if (existingTrackData) {
6076
6765
  return existingTrackData;
6077
6766
  }
6078
- validateAudioChunkMetadata(meta);
6079
- assert(meta);
6080
- assert(meta.decoderConfig);
6767
+ validateAudioChunkMetadata(meta2);
6768
+ assert(meta2);
6769
+ assert(meta2.decoderConfig);
6081
6770
  const newTrackData = {
6082
6771
  track,
6083
6772
  type: "audio",
6084
6773
  info: {
6085
- numberOfChannels: meta.decoderConfig.numberOfChannels,
6086
- sampleRate: meta.decoderConfig.sampleRate,
6087
- decoderConfig: meta.decoderConfig
6774
+ numberOfChannels: meta2.decoderConfig.numberOfChannels,
6775
+ sampleRate: meta2.decoderConfig.sampleRate,
6776
+ decoderConfig: meta2.decoderConfig
6088
6777
  },
6089
6778
  chunkQueue: [],
6090
6779
  lastWrittenMsTimestamp: null
@@ -6096,19 +6785,19 @@ var Mediabunny = (() => {
6096
6785
  }
6097
6786
  return newTrackData;
6098
6787
  }
6099
- getSubtitleTrackData(track, meta) {
6788
+ getSubtitleTrackData(track, meta2) {
6100
6789
  const existingTrackData = this.trackDatas.find((x) => x.track === track);
6101
6790
  if (existingTrackData) {
6102
6791
  return existingTrackData;
6103
6792
  }
6104
- validateSubtitleMetadata(meta);
6105
- assert(meta);
6106
- assert(meta.config);
6793
+ validateSubtitleMetadata(meta2);
6794
+ assert(meta2);
6795
+ assert(meta2.config);
6107
6796
  const newTrackData = {
6108
6797
  track,
6109
6798
  type: "subtitle",
6110
6799
  info: {
6111
- config: meta.config
6800
+ config: meta2.config
6112
6801
  },
6113
6802
  chunkQueue: [],
6114
6803
  lastWrittenMsTimestamp: null
@@ -6120,10 +6809,10 @@ var Mediabunny = (() => {
6120
6809
  }
6121
6810
  return newTrackData;
6122
6811
  }
6123
- async addEncodedVideoPacket(track, packet, meta) {
6812
+ async addEncodedVideoPacket(track, packet, meta2) {
6124
6813
  const release = await this.mutex.acquire();
6125
6814
  try {
6126
- const trackData = this.getVideoTrackData(track, meta);
6815
+ const trackData = this.getVideoTrackData(track, meta2);
6127
6816
  const isKeyFrame = packet.type === "key";
6128
6817
  let timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
6129
6818
  let duration = packet.duration;
@@ -6139,10 +6828,10 @@ var Mediabunny = (() => {
6139
6828
  release();
6140
6829
  }
6141
6830
  }
6142
- async addEncodedAudioPacket(track, packet, meta) {
6831
+ async addEncodedAudioPacket(track, packet, meta2) {
6143
6832
  const release = await this.mutex.acquire();
6144
6833
  try {
6145
- const trackData = this.getAudioTrackData(track, meta);
6834
+ const trackData = this.getAudioTrackData(track, meta2);
6146
6835
  const isKeyFrame = packet.type === "key";
6147
6836
  const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame);
6148
6837
  const audioChunk = this.createInternalChunk(packet.data, timestamp, packet.duration, packet.type);
@@ -6152,10 +6841,10 @@ var Mediabunny = (() => {
6152
6841
  release();
6153
6842
  }
6154
6843
  }
6155
- async addSubtitleCue(track, cue, meta) {
6844
+ async addSubtitleCue(track, cue, meta2) {
6156
6845
  const release = await this.mutex.acquire();
6157
6846
  try {
6158
- const trackData = this.getSubtitleTrackData(track, meta);
6847
+ const trackData = this.getSubtitleTrackData(track, meta2);
6159
6848
  const timestamp = this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true);
6160
6849
  let bodyText = cue.text;
6161
6850
  const timestampMs = Math.round(timestamp * 1e3);
@@ -6254,7 +6943,6 @@ ${cue.notes ?? ""}`;
6254
6943
  /** Writes a block containing media data to the file. */
6255
6944
  writeBlock(trackData, chunk) {
6256
6945
  if (!this.segment) {
6257
- this.createTracks();
6258
6946
  this.createSegment();
6259
6947
  }
6260
6948
  const msTimestamp = Math.round(1e3 * chunk.timestamp);
@@ -6396,7 +7084,6 @@ ${cue.notes ?? ""}`;
6396
7084
  const release = await this.mutex.acquire();
6397
7085
  this.allTracksKnown.resolve();
6398
7086
  if (!this.segment) {
6399
- this.createTracks();
6400
7087
  this.createSegment();
6401
7088
  }
6402
7089
  await this.interleaveChunks(true);
@@ -6413,10 +7100,9 @@ ${cue.notes ?? ""}`;
6413
7100
  this.segmentDuration.data = new EBMLFloat64(this.duration);
6414
7101
  this.writer.seek(this.ebmlWriter.offsets.get(this.segmentDuration));
6415
7102
  this.ebmlWriter.writeEBML(this.segmentDuration);
6416
- this.seekHead.data[0].data[1].data = this.ebmlWriter.offsets.get(this.cues) - this.segmentDataOffset;
6417
- this.seekHead.data[1].data[1].data = this.ebmlWriter.offsets.get(this.segmentInfo) - this.segmentDataOffset;
6418
- this.seekHead.data[2].data[1].data = this.ebmlWriter.offsets.get(this.tracksElement) - this.segmentDataOffset;
7103
+ assert(this.seekHead);
6419
7104
  this.writer.seek(this.ebmlWriter.offsets.get(this.seekHead));
7105
+ this.maybeCreateSeekHead(true);
6420
7106
  this.ebmlWriter.writeEBML(this.seekHead);
6421
7107
  this.writer.seek(endPos);
6422
7108
  }
@@ -6532,6 +7218,726 @@ ${cue.notes ?? ""}`;
6532
7218
  bytesAdvanced: 1
6533
7219
  };
6534
7220
  };
7221
+ var encodeSynchsafe = (unsynchsafed) => {
7222
+ let mask = 127;
7223
+ let synchsafed = 0;
7224
+ let unsynchsafedRest = unsynchsafed;
7225
+ while ((mask ^ 2147483647) !== 0) {
7226
+ synchsafed = unsynchsafedRest & ~mask;
7227
+ synchsafed <<= 1;
7228
+ synchsafed |= unsynchsafedRest & mask;
7229
+ mask = (mask + 1 << 8) - 1;
7230
+ unsynchsafedRest = synchsafed;
7231
+ }
7232
+ return synchsafed;
7233
+ };
7234
+ var decodeSynchsafe = (synchsafed) => {
7235
+ let mask = 2130706432;
7236
+ let unsynchsafed = 0;
7237
+ while (mask !== 0) {
7238
+ unsynchsafed >>= 1;
7239
+ unsynchsafed |= synchsafed & mask;
7240
+ mask >>= 8;
7241
+ }
7242
+ return unsynchsafed;
7243
+ };
7244
+
7245
+ // src/mp3/mp3-reader.ts
7246
+ var ID3_V1_TAG_SIZE = 128;
7247
+ var ID3_V2_HEADER_SIZE = 10;
7248
+ var ID3_V1_GENRES = [
7249
+ "Blues",
7250
+ "Classic rock",
7251
+ "Country",
7252
+ "Dance",
7253
+ "Disco",
7254
+ "Funk",
7255
+ "Grunge",
7256
+ "Hip-hop",
7257
+ "Jazz",
7258
+ "Metal",
7259
+ "New age",
7260
+ "Oldies",
7261
+ "Other",
7262
+ "Pop",
7263
+ "Rhythm and blues",
7264
+ "Rap",
7265
+ "Reggae",
7266
+ "Rock",
7267
+ "Techno",
7268
+ "Industrial",
7269
+ "Alternative",
7270
+ "Ska",
7271
+ "Death metal",
7272
+ "Pranks",
7273
+ "Soundtrack",
7274
+ "Euro-techno",
7275
+ "Ambient",
7276
+ "Trip-hop",
7277
+ "Vocal",
7278
+ "Jazz & funk",
7279
+ "Fusion",
7280
+ "Trance",
7281
+ "Classical",
7282
+ "Instrumental",
7283
+ "Acid",
7284
+ "House",
7285
+ "Game",
7286
+ "Sound clip",
7287
+ "Gospel",
7288
+ "Noise",
7289
+ "Alternative rock",
7290
+ "Bass",
7291
+ "Soul",
7292
+ "Punk",
7293
+ "Space",
7294
+ "Meditative",
7295
+ "Instrumental pop",
7296
+ "Instrumental rock",
7297
+ "Ethnic",
7298
+ "Gothic",
7299
+ "Darkwave",
7300
+ "Techno-industrial",
7301
+ "Electronic",
7302
+ "Pop-folk",
7303
+ "Eurodance",
7304
+ "Dream",
7305
+ "Southern rock",
7306
+ "Comedy",
7307
+ "Cult",
7308
+ "Gangsta",
7309
+ "Top 40",
7310
+ "Christian rap",
7311
+ "Pop/funk",
7312
+ "Jungle music",
7313
+ "Native US",
7314
+ "Cabaret",
7315
+ "New wave",
7316
+ "Psychedelic",
7317
+ "Rave",
7318
+ "Showtunes",
7319
+ "Trailer",
7320
+ "Lo-fi",
7321
+ "Tribal",
7322
+ "Acid punk",
7323
+ "Acid jazz",
7324
+ "Polka",
7325
+ "Retro",
7326
+ "Musical",
7327
+ "Rock 'n' roll",
7328
+ "Hard rock",
7329
+ "Folk",
7330
+ "Folk rock",
7331
+ "National folk",
7332
+ "Swing",
7333
+ "Fast fusion",
7334
+ "Bebop",
7335
+ "Latin",
7336
+ "Revival",
7337
+ "Celtic",
7338
+ "Bluegrass",
7339
+ "Avantgarde",
7340
+ "Gothic rock",
7341
+ "Progressive rock",
7342
+ "Psychedelic rock",
7343
+ "Symphonic rock",
7344
+ "Slow rock",
7345
+ "Big band",
7346
+ "Chorus",
7347
+ "Easy listening",
7348
+ "Acoustic",
7349
+ "Humour",
7350
+ "Speech",
7351
+ "Chanson",
7352
+ "Opera",
7353
+ "Chamber music",
7354
+ "Sonata",
7355
+ "Symphony",
7356
+ "Booty bass",
7357
+ "Primus",
7358
+ "Porn groove",
7359
+ "Satire",
7360
+ "Slow jam",
7361
+ "Club",
7362
+ "Tango",
7363
+ "Samba",
7364
+ "Folklore",
7365
+ "Ballad",
7366
+ "Power ballad",
7367
+ "Rhythmic Soul",
7368
+ "Freestyle",
7369
+ "Duet",
7370
+ "Punk rock",
7371
+ "Drum solo",
7372
+ "A cappella",
7373
+ "Euro-house",
7374
+ "Dance hall",
7375
+ "Goa music",
7376
+ "Drum & bass",
7377
+ "Club-house",
7378
+ "Hardcore techno",
7379
+ "Terror",
7380
+ "Indie",
7381
+ "Britpop",
7382
+ "Negerpunk",
7383
+ "Polsk punk",
7384
+ "Beat",
7385
+ "Christian gangsta rap",
7386
+ "Heavy metal",
7387
+ "Black metal",
7388
+ "Crossover",
7389
+ "Contemporary Christian",
7390
+ "Christian rock",
7391
+ "Merengue",
7392
+ "Salsa",
7393
+ "Thrash metal",
7394
+ "Anime",
7395
+ "Jpop",
7396
+ "Synthpop",
7397
+ "Christmas",
7398
+ "Art rock",
7399
+ "Baroque",
7400
+ "Bhangra",
7401
+ "Big beat",
7402
+ "Breakbeat",
7403
+ "Chillout",
7404
+ "Downtempo",
7405
+ "Dub",
7406
+ "EBM",
7407
+ "Eclectic",
7408
+ "Electro",
7409
+ "Electroclash",
7410
+ "Emo",
7411
+ "Experimental",
7412
+ "Garage",
7413
+ "Global",
7414
+ "IDM",
7415
+ "Illbient",
7416
+ "Industro-Goth",
7417
+ "Jam Band",
7418
+ "Krautrock",
7419
+ "Leftfield",
7420
+ "Lounge",
7421
+ "Math rock",
7422
+ "New romantic",
7423
+ "Nu-breakz",
7424
+ "Post-punk",
7425
+ "Post-rock",
7426
+ "Psytrance",
7427
+ "Shoegaze",
7428
+ "Space rock",
7429
+ "Trop rock",
7430
+ "World music",
7431
+ "Neoclassical",
7432
+ "Audiobook",
7433
+ "Audio theatre",
7434
+ "Neue Deutsche Welle",
7435
+ "Podcast",
7436
+ "Indie rock",
7437
+ "G-Funk",
7438
+ "Dubstep",
7439
+ "Garage rock",
7440
+ "Psybient"
7441
+ ];
7442
+ var readNextFrameHeader = async (reader, startPos, until) => {
7443
+ let currentPos = startPos;
7444
+ while (until === null || currentPos < until) {
7445
+ let slice = reader.requestSlice(currentPos, FRAME_HEADER_SIZE);
7446
+ if (slice instanceof Promise) slice = await slice;
7447
+ if (!slice) break;
7448
+ const word = readU32Be(slice);
7449
+ const result = readFrameHeader(word, reader.fileSize !== null ? reader.fileSize - currentPos : null);
7450
+ if (result.header) {
7451
+ return { header: result.header, startPos: currentPos };
7452
+ }
7453
+ currentPos += result.bytesAdvanced;
7454
+ }
7455
+ return null;
7456
+ };
7457
+ var parseId3V1Tag = (slice, tags) => {
7458
+ const startPos = slice.filePos;
7459
+ tags.raw ??= {};
7460
+ tags.raw["TAG"] ??= readBytes(slice, ID3_V1_TAG_SIZE - 3);
7461
+ slice.filePos = startPos;
7462
+ const title = readId3V1String(slice, 30);
7463
+ if (title) tags.title ??= title;
7464
+ const artist = readId3V1String(slice, 30);
7465
+ if (artist) tags.artist ??= artist;
7466
+ const album = readId3V1String(slice, 30);
7467
+ if (album) tags.album ??= album;
7468
+ const yearText = readId3V1String(slice, 4);
7469
+ const year = Number.parseInt(yearText, 10);
7470
+ if (Number.isInteger(year) && year > 0) {
7471
+ tags.date ??= new Date(year, 0, 1);
7472
+ }
7473
+ const commentBytes = readBytes(slice, 30);
7474
+ let comment;
7475
+ if (commentBytes[28] === 0 && commentBytes[29] !== 0) {
7476
+ const trackNum = commentBytes[29];
7477
+ if (trackNum > 0) {
7478
+ tags.trackNumber ??= trackNum;
7479
+ }
7480
+ slice.skip(-30);
7481
+ comment = readId3V1String(slice, 28);
7482
+ slice.skip(2);
7483
+ } else {
7484
+ slice.skip(-30);
7485
+ comment = readId3V1String(slice, 30);
7486
+ }
7487
+ if (comment) tags.comment ??= comment;
7488
+ const genreIndex = readU8(slice);
7489
+ if (genreIndex < ID3_V1_GENRES.length) {
7490
+ tags.genre ??= ID3_V1_GENRES[genreIndex];
7491
+ }
7492
+ };
7493
+ var readId3V1String = (slice, length) => {
7494
+ const bytes2 = readBytes(slice, length);
7495
+ const endIndex = coalesceIndex(bytes2.indexOf(0), bytes2.length);
7496
+ const relevantBytes = bytes2.subarray(0, endIndex);
7497
+ let str = "";
7498
+ for (let i = 0; i < relevantBytes.length; i++) {
7499
+ str += String.fromCharCode(relevantBytes[i]);
7500
+ }
7501
+ return str.trimEnd();
7502
+ };
7503
+ var readId3V2Header = (slice) => {
7504
+ const startPos = slice.filePos;
7505
+ const tag = readAscii(slice, 3);
7506
+ const majorVersion = readU8(slice);
7507
+ const revision = readU8(slice);
7508
+ const flags = readU8(slice);
7509
+ const sizeRaw = readU32Be(slice);
7510
+ if (tag !== "ID3" || majorVersion === 255 || revision === 255 || (sizeRaw & 2155905152) !== 0) {
7511
+ slice.filePos = startPos;
7512
+ return null;
7513
+ }
7514
+ const size = decodeSynchsafe(sizeRaw);
7515
+ return { majorVersion, revision, flags, size };
7516
+ };
7517
+ var parseId3V2Tag = (slice, header, tags) => {
7518
+ if (![2, 3, 4].includes(header.majorVersion)) {
7519
+ console.warn(`Unsupported ID3v2 major version: ${header.majorVersion}`);
7520
+ return;
7521
+ }
7522
+ const bytes2 = readBytes(slice, header.size);
7523
+ const reader = new Id3V2Reader(header, bytes2);
7524
+ if (header.flags & 16 /* Footer */) {
7525
+ reader.removeFooter();
7526
+ }
7527
+ if (header.flags & 128 /* Unsynchronisation */ && header.majorVersion === 3) {
7528
+ reader.ununsynchronizeAll();
7529
+ }
7530
+ if (header.flags & 64 /* ExtendedHeader */) {
7531
+ const extendedHeaderSize = reader.readU32();
7532
+ if (header.majorVersion === 3) {
7533
+ reader.pos += extendedHeaderSize;
7534
+ } else {
7535
+ reader.pos += extendedHeaderSize - 4;
7536
+ }
7537
+ }
7538
+ while (reader.pos <= reader.bytes.length - reader.frameHeaderSize()) {
7539
+ const frame = reader.readId3V2Frame();
7540
+ if (!frame) {
7541
+ break;
7542
+ }
7543
+ const frameStartPos = reader.pos;
7544
+ const frameEndPos = reader.pos + frame.size;
7545
+ let frameEncrypted = false;
7546
+ let frameCompressed = false;
7547
+ let frameUnsynchronized = false;
7548
+ if (header.majorVersion === 3) {
7549
+ frameEncrypted = !!(frame.flags & 1 << 6);
7550
+ frameCompressed = !!(frame.flags & 1 << 7);
7551
+ } else if (header.majorVersion === 4) {
7552
+ frameEncrypted = !!(frame.flags & 1 << 2);
7553
+ frameCompressed = !!(frame.flags & 1 << 3);
7554
+ frameUnsynchronized = !!(frame.flags & 1 << 1) || !!(header.flags & 128 /* Unsynchronisation */);
7555
+ }
7556
+ if (frameEncrypted) {
7557
+ console.warn(`Skipping encrypted ID3v2 frame ${frame.id}`);
7558
+ reader.pos = frameEndPos;
7559
+ continue;
7560
+ }
7561
+ if (frameCompressed) {
7562
+ console.warn(`Skipping compressed ID3v2 frame ${frame.id}`);
7563
+ reader.pos = frameEndPos;
7564
+ continue;
7565
+ }
7566
+ if (frameUnsynchronized) {
7567
+ reader.ununsynchronizeRegion(reader.pos, frameEndPos);
7568
+ }
7569
+ tags.raw ??= {};
7570
+ if (frame.id[0] === "T") {
7571
+ tags.raw[frame.id] ??= reader.readId3V2EncodingAndText(frameEndPos);
7572
+ } else {
7573
+ tags.raw[frame.id] ??= reader.readBytes(frame.size);
7574
+ }
7575
+ reader.pos = frameStartPos;
7576
+ switch (frame.id) {
7577
+ case "TIT2":
7578
+ case "TT2":
7579
+ {
7580
+ tags.title ??= reader.readId3V2EncodingAndText(frameEndPos);
7581
+ }
7582
+ ;
7583
+ break;
7584
+ case "TIT3":
7585
+ case "TT3":
7586
+ {
7587
+ tags.description ??= reader.readId3V2EncodingAndText(frameEndPos);
7588
+ }
7589
+ ;
7590
+ break;
7591
+ case "TPE1":
7592
+ case "TP1":
7593
+ {
7594
+ tags.artist ??= reader.readId3V2EncodingAndText(frameEndPos);
7595
+ }
7596
+ ;
7597
+ break;
7598
+ case "TALB":
7599
+ case "TAL":
7600
+ {
7601
+ tags.album ??= reader.readId3V2EncodingAndText(frameEndPos);
7602
+ }
7603
+ ;
7604
+ break;
7605
+ case "TPE2":
7606
+ case "TP2":
7607
+ {
7608
+ tags.albumArtist ??= reader.readId3V2EncodingAndText(frameEndPos);
7609
+ }
7610
+ ;
7611
+ break;
7612
+ case "TRCK":
7613
+ case "TRK":
7614
+ {
7615
+ const trackText = reader.readId3V2EncodingAndText(frameEndPos);
7616
+ const parts = trackText.split("/");
7617
+ const trackNum = Number.parseInt(parts[0], 10);
7618
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
7619
+ if (Number.isInteger(trackNum) && trackNum > 0) {
7620
+ tags.trackNumber ??= trackNum;
7621
+ }
7622
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
7623
+ tags.tracksTotal ??= tracksTotal;
7624
+ }
7625
+ }
7626
+ ;
7627
+ break;
7628
+ case "TPOS":
7629
+ case "TPA":
7630
+ {
7631
+ const discText = reader.readId3V2EncodingAndText(frameEndPos);
7632
+ const parts = discText.split("/");
7633
+ const discNum = Number.parseInt(parts[0], 10);
7634
+ const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
7635
+ if (Number.isInteger(discNum) && discNum > 0) {
7636
+ tags.discNumber ??= discNum;
7637
+ }
7638
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
7639
+ tags.discsTotal ??= discsTotal;
7640
+ }
7641
+ }
7642
+ ;
7643
+ break;
7644
+ case "TCON":
7645
+ case "TCO":
7646
+ {
7647
+ const genreText = reader.readId3V2EncodingAndText(frameEndPos);
7648
+ let match = /^\((\d+)\)/.exec(genreText);
7649
+ if (match) {
7650
+ const genreNumber = Number.parseInt(match[1]);
7651
+ if (ID3_V1_GENRES[genreNumber] !== void 0) {
7652
+ tags.genre ??= ID3_V1_GENRES[genreNumber];
7653
+ break;
7654
+ }
7655
+ }
7656
+ match = /^\d+$/.exec(genreText);
7657
+ if (match) {
7658
+ const genreNumber = Number.parseInt(match[0]);
7659
+ if (ID3_V1_GENRES[genreNumber] !== void 0) {
7660
+ tags.genre ??= ID3_V1_GENRES[genreNumber];
7661
+ break;
7662
+ }
7663
+ }
7664
+ tags.genre ??= genreText;
7665
+ }
7666
+ ;
7667
+ break;
7668
+ case "TDRC":
7669
+ case "TDAT":
7670
+ {
7671
+ const dateText = reader.readId3V2EncodingAndText(frameEndPos);
7672
+ const date = new Date(dateText);
7673
+ if (!Number.isNaN(date.getTime())) {
7674
+ tags.date ??= date;
7675
+ }
7676
+ }
7677
+ ;
7678
+ break;
7679
+ case "TYER":
7680
+ case "TYE":
7681
+ {
7682
+ const yearText = reader.readId3V2EncodingAndText(frameEndPos);
7683
+ const year = Number.parseInt(yearText, 10);
7684
+ if (Number.isInteger(year)) {
7685
+ tags.date ??= new Date(year, 0, 1);
7686
+ }
7687
+ }
7688
+ ;
7689
+ break;
7690
+ case "USLT":
7691
+ case "ULT":
7692
+ {
7693
+ const encoding = reader.readU8();
7694
+ reader.pos += 3;
7695
+ reader.readId3V2Text(encoding, frameEndPos);
7696
+ tags.lyrics ??= reader.readId3V2Text(encoding, frameEndPos);
7697
+ }
7698
+ ;
7699
+ break;
7700
+ case "COMM":
7701
+ case "COM":
7702
+ {
7703
+ const encoding = reader.readU8();
7704
+ reader.pos += 3;
7705
+ reader.readId3V2Text(encoding, frameEndPos);
7706
+ tags.comment ??= reader.readId3V2Text(encoding, frameEndPos);
7707
+ }
7708
+ ;
7709
+ break;
7710
+ case "APIC":
7711
+ case "PIC":
7712
+ {
7713
+ const encoding = reader.readId3V2TextEncoding();
7714
+ let mimeType;
7715
+ if (header.majorVersion === 2) {
7716
+ const imageFormat = reader.readAscii(3);
7717
+ mimeType = imageFormat === "PNG" ? "image/png" : imageFormat === "JPG" ? "image/jpeg" : "image/*";
7718
+ } else {
7719
+ mimeType = reader.readId3V2Text(encoding, frameEndPos);
7720
+ }
7721
+ const pictureType = reader.readU8();
7722
+ const description = reader.readId3V2Text(encoding, frameEndPos).trimEnd();
7723
+ const imageDataSize = frameEndPos - reader.pos;
7724
+ if (imageDataSize >= 0) {
7725
+ const imageData = reader.readBytes(imageDataSize);
7726
+ if (!tags.images) tags.images = [];
7727
+ tags.images.push({
7728
+ data: imageData,
7729
+ mimeType,
7730
+ kind: pictureType === 3 ? "coverFront" : pictureType === 4 ? "coverBack" : "unknown",
7731
+ description
7732
+ });
7733
+ }
7734
+ }
7735
+ ;
7736
+ break;
7737
+ default:
7738
+ {
7739
+ reader.pos += frame.size;
7740
+ }
7741
+ ;
7742
+ break;
7743
+ }
7744
+ reader.pos = frameEndPos;
7745
+ }
7746
+ };
7747
+ var Id3V2Reader = class {
7748
+ constructor(header, bytes2) {
7749
+ this.header = header;
7750
+ this.bytes = bytes2;
7751
+ this.pos = 0;
7752
+ this.view = new DataView(bytes2.buffer, bytes2.byteOffset, bytes2.byteLength);
7753
+ }
7754
+ frameHeaderSize() {
7755
+ return this.header.majorVersion === 2 ? 6 : 10;
7756
+ }
7757
+ ununsynchronizeAll() {
7758
+ const newBytes = [];
7759
+ for (let i = 0; i < this.bytes.length; i++) {
7760
+ const value1 = this.bytes[i];
7761
+ newBytes.push(value1);
7762
+ if (value1 === 255 && i !== this.bytes.length - 1) {
7763
+ const value2 = this.bytes[i];
7764
+ if (value2 === 0) {
7765
+ i++;
7766
+ }
7767
+ }
7768
+ }
7769
+ this.bytes = new Uint8Array(newBytes);
7770
+ this.view = new DataView(this.bytes.buffer);
7771
+ }
7772
+ ununsynchronizeRegion(start, end) {
7773
+ const newBytes = [];
7774
+ for (let i = start; i < end; i++) {
7775
+ const value1 = this.bytes[i];
7776
+ newBytes.push(value1);
7777
+ if (value1 === 255 && i !== end - 1) {
7778
+ const value2 = this.bytes[i + 1];
7779
+ if (value2 === 0) {
7780
+ i++;
7781
+ }
7782
+ }
7783
+ }
7784
+ const before = this.bytes.subarray(0, start);
7785
+ const after = this.bytes.subarray(end);
7786
+ this.bytes = new Uint8Array(before.length + newBytes.length + after.length);
7787
+ this.bytes.set(before, 0);
7788
+ this.bytes.set(newBytes, before.length);
7789
+ this.bytes.set(after, before.length + newBytes.length);
7790
+ this.view = new DataView(this.bytes.buffer);
7791
+ }
7792
+ removeFooter() {
7793
+ this.bytes = this.bytes.subarray(0, this.bytes.length - ID3_V2_HEADER_SIZE);
7794
+ this.view = new DataView(this.bytes.buffer);
7795
+ }
7796
+ readBytes(length) {
7797
+ const slice = this.bytes.subarray(this.pos, this.pos + length);
7798
+ this.pos += length;
7799
+ return slice;
7800
+ }
7801
+ readU8() {
7802
+ const value = this.view.getUint8(this.pos);
7803
+ this.pos += 1;
7804
+ return value;
7805
+ }
7806
+ readU16() {
7807
+ const value = this.view.getUint16(this.pos, false);
7808
+ this.pos += 2;
7809
+ return value;
7810
+ }
7811
+ readU24() {
7812
+ const high = this.view.getUint16(this.pos, false);
7813
+ const low = this.view.getUint8(this.pos + 1);
7814
+ this.pos += 3;
7815
+ return high * 256 + low;
7816
+ }
7817
+ readU32() {
7818
+ const value = this.view.getUint32(this.pos, false);
7819
+ this.pos += 4;
7820
+ return value;
7821
+ }
7822
+ readAscii(length) {
7823
+ let str = "";
7824
+ for (let i = 0; i < length; i++) {
7825
+ str += String.fromCharCode(this.view.getUint8(this.pos + i));
7826
+ }
7827
+ this.pos += length;
7828
+ return str;
7829
+ }
7830
+ readId3V2Frame() {
7831
+ if (this.header.majorVersion === 2) {
7832
+ const id = this.readAscii(3);
7833
+ if (id === "\0\0\0") {
7834
+ return null;
7835
+ }
7836
+ const size = this.readU24();
7837
+ return { id, size, flags: 0 };
7838
+ } else {
7839
+ const id = this.readAscii(4);
7840
+ if (id === "\0\0\0\0") {
7841
+ return null;
7842
+ }
7843
+ const sizeRaw = this.readU32();
7844
+ let size = this.header.majorVersion === 4 ? decodeSynchsafe(sizeRaw) : sizeRaw;
7845
+ const flags = this.readU16();
7846
+ const headerEndPos = this.pos;
7847
+ const isSizeValid = (size2) => {
7848
+ const nextPos = this.pos + size2;
7849
+ if (nextPos > this.bytes.length) {
7850
+ return false;
7851
+ }
7852
+ if (nextPos <= this.bytes.length - this.frameHeaderSize()) {
7853
+ this.pos += size2;
7854
+ const nextId = this.readAscii(4);
7855
+ if (nextId !== "\0\0\0\0" && !/[0-9A-Z]{4}/.test(nextId)) {
7856
+ return false;
7857
+ }
7858
+ }
7859
+ return true;
7860
+ };
7861
+ if (!isSizeValid(size)) {
7862
+ const otherSize = this.header.majorVersion === 4 ? sizeRaw : decodeSynchsafe(sizeRaw);
7863
+ if (isSizeValid(otherSize)) {
7864
+ size = otherSize;
7865
+ }
7866
+ }
7867
+ this.pos = headerEndPos;
7868
+ return { id, size, flags };
7869
+ }
7870
+ }
7871
+ readId3V2TextEncoding() {
7872
+ const number = this.readU8();
7873
+ if (number > 3) {
7874
+ throw new Error(`Unsupported text encoding: ${number}`);
7875
+ }
7876
+ return number;
7877
+ }
7878
+ readId3V2Text(encoding, until) {
7879
+ const startPos = this.pos;
7880
+ const data = this.readBytes(until);
7881
+ switch (encoding) {
7882
+ case 0 /* ISO_8859_1 */: {
7883
+ let str = "";
7884
+ for (let i = 0; i < data.length; i++) {
7885
+ const value = data[i];
7886
+ if (value === 0) {
7887
+ this.pos = startPos + i + 1;
7888
+ break;
7889
+ }
7890
+ str += String.fromCharCode(value);
7891
+ }
7892
+ return str;
7893
+ }
7894
+ case 1 /* UTF_16_WITH_BOM */: {
7895
+ if (data[0] === 255 && data[1] === 254) {
7896
+ const decoder = new TextDecoder("utf-16le");
7897
+ const endIndex = coalesceIndex(
7898
+ data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0),
7899
+ data.length
7900
+ );
7901
+ this.pos = startPos + Math.min(endIndex + 2, data.length);
7902
+ return decoder.decode(data.subarray(2, endIndex));
7903
+ } else if (data[0] === 254 && data[1] === 255) {
7904
+ const decoder = new TextDecoder("utf-16be");
7905
+ const endIndex = coalesceIndex(
7906
+ data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0),
7907
+ data.length
7908
+ );
7909
+ this.pos = startPos + Math.min(endIndex + 2, data.length);
7910
+ return decoder.decode(data.subarray(2, endIndex));
7911
+ } else {
7912
+ const endIndex = coalesceIndex(data.findIndex((x) => x === 0), data.length);
7913
+ this.pos = startPos + Math.min(endIndex + 1, data.length);
7914
+ return textDecoder.decode(data.subarray(0, endIndex));
7915
+ }
7916
+ }
7917
+ case 2 /* UTF_16_BE_NO_BOM */: {
7918
+ const decoder = new TextDecoder("utf-16be");
7919
+ const endIndex = coalesceIndex(
7920
+ data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0),
7921
+ data.length
7922
+ );
7923
+ this.pos = startPos + Math.min(endIndex + 2, data.length);
7924
+ return decoder.decode(data.subarray(0, endIndex));
7925
+ }
7926
+ case 3 /* UTF_8 */: {
7927
+ const endIndex = coalesceIndex(data.findIndex((x) => x === 0), data.length);
7928
+ this.pos = startPos + Math.min(endIndex + 1, data.length);
7929
+ return textDecoder.decode(data.subarray(0, endIndex));
7930
+ }
7931
+ }
7932
+ }
7933
+ readId3V2EncodingAndText(until) {
7934
+ if (this.pos >= until) {
7935
+ return "";
7936
+ }
7937
+ const encoding = this.readId3V2TextEncoding();
7938
+ return this.readId3V2Text(encoding, until);
7939
+ }
7940
+ };
6535
7941
 
6536
7942
  // src/mp3/mp3-writer.ts
6537
7943
  var Mp3Writer = class {
@@ -6540,10 +7946,112 @@ ${cue.notes ?? ""}`;
6540
7946
  this.helper = new Uint8Array(8);
6541
7947
  this.helperView = new DataView(this.helper.buffer);
6542
7948
  }
7949
+ writeU8(value) {
7950
+ this.helper[0] = value;
7951
+ this.writer.write(this.helper.subarray(0, 1));
7952
+ }
7953
+ writeU16(value) {
7954
+ this.helperView.setUint16(0, value, false);
7955
+ this.writer.write(this.helper.subarray(0, 2));
7956
+ }
6543
7957
  writeU32(value) {
6544
7958
  this.helperView.setUint32(0, value, false);
6545
7959
  this.writer.write(this.helper.subarray(0, 4));
6546
7960
  }
7961
+ writeAscii(text) {
7962
+ for (let i = 0; i < text.length; i++) {
7963
+ this.helper[i] = text.charCodeAt(i);
7964
+ }
7965
+ this.writer.write(this.helper.subarray(0, text.length));
7966
+ }
7967
+ writeSynchsafeU32(value) {
7968
+ this.writeU32(encodeSynchsafe(value));
7969
+ }
7970
+ writeIsoString(text) {
7971
+ const bytes2 = new Uint8Array(text.length + 1);
7972
+ for (let i = 0; i < text.length; i++) {
7973
+ bytes2[i] = text.charCodeAt(i);
7974
+ }
7975
+ bytes2[text.length] = 0;
7976
+ this.writer.write(bytes2);
7977
+ }
7978
+ writeUtf8String(text) {
7979
+ const utf8Data = textEncoder.encode(text);
7980
+ this.writer.write(utf8Data);
7981
+ this.writeU8(0);
7982
+ }
7983
+ writeId3V2TextFrame(frameId, text) {
7984
+ const useIso88591 = isIso88591Compatible(text);
7985
+ const textDataLength = useIso88591 ? text.length : textEncoder.encode(text).byteLength;
7986
+ const frameSize = 1 + textDataLength + 1;
7987
+ this.writeAscii(frameId);
7988
+ this.writeSynchsafeU32(frameSize);
7989
+ this.writeU16(0);
7990
+ this.writeU8(useIso88591 ? 0 /* ISO_8859_1 */ : 3 /* UTF_8 */);
7991
+ if (useIso88591) {
7992
+ this.writeIsoString(text);
7993
+ } else {
7994
+ this.writeUtf8String(text);
7995
+ }
7996
+ }
7997
+ writeId3V2LyricsFrame(lyrics) {
7998
+ const useIso88591 = isIso88591Compatible(lyrics);
7999
+ const shortDescription = "";
8000
+ const frameSize = 1 + 3 + shortDescription.length + 1 + lyrics.length + 1;
8001
+ this.writeAscii("USLT");
8002
+ this.writeSynchsafeU32(frameSize);
8003
+ this.writeU16(0);
8004
+ this.writeU8(useIso88591 ? 0 /* ISO_8859_1 */ : 3 /* UTF_8 */);
8005
+ this.writeAscii("und");
8006
+ if (useIso88591) {
8007
+ this.writeIsoString(shortDescription);
8008
+ this.writeIsoString(lyrics);
8009
+ } else {
8010
+ this.writeUtf8String(shortDescription);
8011
+ this.writeUtf8String(lyrics);
8012
+ }
8013
+ }
8014
+ writeId3V2CommentFrame(comment) {
8015
+ const useIso88591 = isIso88591Compatible(comment);
8016
+ const textDataLength = useIso88591 ? comment.length : textEncoder.encode(comment).byteLength;
8017
+ const shortDescription = "";
8018
+ const frameSize = 1 + 3 + shortDescription.length + 1 + textDataLength + 1;
8019
+ this.writeAscii("COMM");
8020
+ this.writeSynchsafeU32(frameSize);
8021
+ this.writeU16(0);
8022
+ this.writeU8(useIso88591 ? 0 /* ISO_8859_1 */ : 3 /* UTF_8 */);
8023
+ this.writeU8(117);
8024
+ this.writeU8(110);
8025
+ this.writeU8(100);
8026
+ if (useIso88591) {
8027
+ this.writeIsoString(shortDescription);
8028
+ this.writeIsoString(comment);
8029
+ } else {
8030
+ this.writeUtf8String(shortDescription);
8031
+ this.writeUtf8String(comment);
8032
+ }
8033
+ }
8034
+ writeId3V2ApicFrame(mimeType, pictureType, description, imageData) {
8035
+ const useIso88591 = isIso88591Compatible(mimeType) && isIso88591Compatible(description);
8036
+ const descriptionDataLength = useIso88591 ? description.length : textEncoder.encode(description).byteLength;
8037
+ const frameSize = 1 + mimeType.length + 1 + 1 + descriptionDataLength + 1 + imageData.byteLength;
8038
+ this.writeAscii("APIC");
8039
+ this.writeSynchsafeU32(frameSize);
8040
+ this.writeU16(0);
8041
+ this.writeU8(useIso88591 ? 0 /* ISO_8859_1 */ : 3 /* UTF_8 */);
8042
+ if (useIso88591) {
8043
+ this.writeIsoString(mimeType);
8044
+ } else {
8045
+ this.writeUtf8String(mimeType);
8046
+ }
8047
+ this.writeU8(pictureType);
8048
+ if (useIso88591) {
8049
+ this.writeIsoString(description);
8050
+ } else {
8051
+ this.writeUtf8String(description);
8052
+ }
8053
+ this.writer.write(imageData);
8054
+ }
6547
8055
  writeXingFrame(data) {
6548
8056
  const startPos = this.writer.getPos();
6549
8057
  const firstByte = 255;
@@ -6601,11 +8109,15 @@ ${cue.notes ?? ""}`;
6601
8109
  this.xingFrameData = null;
6602
8110
  this.frameCount = 0;
6603
8111
  this.framePositions = [];
8112
+ this.xingFramePos = null;
6604
8113
  this.format = format;
6605
8114
  this.writer = output._writer;
6606
8115
  this.mp3Writer = new Mp3Writer(output._writer);
6607
8116
  }
6608
8117
  async start() {
8118
+ if (!metadataTagsAreEmpty(this.output._metadataTags)) {
8119
+ this.writeId3v2Tag(this.output._metadataTags);
8120
+ }
6609
8121
  }
6610
8122
  async getMimeType() {
6611
8123
  return "audio/mpeg";
@@ -6648,6 +8160,7 @@ ${cue.notes ?? ""}`;
6648
8160
  fileSize: null,
6649
8161
  toc: null
6650
8162
  };
8163
+ this.xingFramePos = this.writer.getPos();
6651
8164
  this.mp3Writer.writeXingFrame(this.xingFrameData);
6652
8165
  this.frameCount++;
6653
8166
  }
@@ -6665,13 +8178,157 @@ ${cue.notes ?? ""}`;
6665
8178
  async addSubtitleCue() {
6666
8179
  throw new Error("MP3 does not support subtitles.");
6667
8180
  }
8181
+ writeId3v2Tag(tags) {
8182
+ this.mp3Writer.writeAscii("ID3");
8183
+ this.mp3Writer.writeU8(4);
8184
+ this.mp3Writer.writeU8(0);
8185
+ this.mp3Writer.writeU8(0);
8186
+ this.mp3Writer.writeSynchsafeU32(0);
8187
+ const startPos = this.writer.getPos();
8188
+ const writtenTags = /* @__PURE__ */ new Set();
8189
+ for (const { key, value } of keyValueIterator(tags)) {
8190
+ switch (key) {
8191
+ case "title":
8192
+ {
8193
+ this.mp3Writer.writeId3V2TextFrame("TIT2", value);
8194
+ writtenTags.add("TIT2");
8195
+ }
8196
+ ;
8197
+ break;
8198
+ case "description":
8199
+ {
8200
+ this.mp3Writer.writeId3V2TextFrame("TIT3", value);
8201
+ writtenTags.add("TIT3");
8202
+ }
8203
+ ;
8204
+ break;
8205
+ case "artist":
8206
+ {
8207
+ this.mp3Writer.writeId3V2TextFrame("TPE1", value);
8208
+ writtenTags.add("TPE1");
8209
+ }
8210
+ ;
8211
+ break;
8212
+ case "album":
8213
+ {
8214
+ this.mp3Writer.writeId3V2TextFrame("TALB", value);
8215
+ writtenTags.add("TALB");
8216
+ }
8217
+ ;
8218
+ break;
8219
+ case "albumArtist":
8220
+ {
8221
+ this.mp3Writer.writeId3V2TextFrame("TPE2", value);
8222
+ writtenTags.add("TPE2");
8223
+ }
8224
+ ;
8225
+ break;
8226
+ case "trackNumber":
8227
+ {
8228
+ const string = tags.tracksTotal !== void 0 ? `${value}/${tags.tracksTotal}` : value.toString();
8229
+ this.mp3Writer.writeId3V2TextFrame("TRCK", string);
8230
+ writtenTags.add("TRCK");
8231
+ }
8232
+ ;
8233
+ break;
8234
+ case "discNumber":
8235
+ {
8236
+ const string = tags.discsTotal !== void 0 ? `${value}/${tags.discsTotal}` : value.toString();
8237
+ this.mp3Writer.writeId3V2TextFrame("TPOS", string);
8238
+ writtenTags.add("TPOS");
8239
+ }
8240
+ ;
8241
+ break;
8242
+ case "genre":
8243
+ {
8244
+ this.mp3Writer.writeId3V2TextFrame("TCON", value);
8245
+ writtenTags.add("TCON");
8246
+ }
8247
+ ;
8248
+ break;
8249
+ case "date":
8250
+ {
8251
+ this.mp3Writer.writeId3V2TextFrame("TDRC", value.toISOString().slice(0, 10));
8252
+ writtenTags.add("TDRC");
8253
+ }
8254
+ ;
8255
+ break;
8256
+ case "lyrics":
8257
+ {
8258
+ this.mp3Writer.writeId3V2LyricsFrame(value);
8259
+ writtenTags.add("USLT");
8260
+ }
8261
+ ;
8262
+ break;
8263
+ case "comment":
8264
+ {
8265
+ this.mp3Writer.writeId3V2CommentFrame(value);
8266
+ writtenTags.add("COMM");
8267
+ }
8268
+ ;
8269
+ break;
8270
+ case "images":
8271
+ {
8272
+ const pictureTypeMap = { coverFront: 3, coverBack: 4, unknown: 0 };
8273
+ for (const image of value) {
8274
+ const pictureType = pictureTypeMap[image.kind];
8275
+ const description = image.description ?? "";
8276
+ this.mp3Writer.writeId3V2ApicFrame(image.mimeType, pictureType, description, image.data);
8277
+ }
8278
+ }
8279
+ ;
8280
+ break;
8281
+ case "tracksTotal":
8282
+ case "discsTotal":
8283
+ {
8284
+ }
8285
+ ;
8286
+ break;
8287
+ case "raw":
8288
+ {
8289
+ }
8290
+ ;
8291
+ break;
8292
+ default:
8293
+ assertNever(key);
8294
+ }
8295
+ }
8296
+ if (tags.raw) {
8297
+ for (const key in tags.raw) {
8298
+ const value = tags.raw[key];
8299
+ if (value == null || key.length !== 4 || writtenTags.has(key)) {
8300
+ continue;
8301
+ }
8302
+ let bytes2;
8303
+ if (typeof value === "string") {
8304
+ const encoded = textEncoder.encode(value);
8305
+ bytes2 = new Uint8Array(encoded.byteLength + 2);
8306
+ bytes2[0] = 3 /* UTF_8 */;
8307
+ bytes2.set(encoded, 1);
8308
+ } else if (value instanceof Uint8Array) {
8309
+ bytes2 = value;
8310
+ } else {
8311
+ continue;
8312
+ }
8313
+ this.mp3Writer.writeAscii(key);
8314
+ this.mp3Writer.writeSynchsafeU32(bytes2.byteLength);
8315
+ this.mp3Writer.writeU16(0);
8316
+ this.writer.write(bytes2);
8317
+ }
8318
+ }
8319
+ const endPos = this.writer.getPos();
8320
+ const framesSize = endPos - startPos;
8321
+ this.writer.seek(6);
8322
+ this.mp3Writer.writeSynchsafeU32(framesSize);
8323
+ this.writer.seek(endPos);
8324
+ }
6668
8325
  async finalize() {
6669
- if (!this.xingFrameData) {
8326
+ if (!this.xingFrameData || this.xingFramePos === null) {
6670
8327
  return;
6671
8328
  }
6672
8329
  const release = await this.mutex.acquire();
6673
8330
  const endPos = this.writer.getPos();
6674
- this.writer.seek(0);
8331
+ this.writer.seek(this.xingFramePos);
6675
8332
  const toc = new Uint8Array(100);
6676
8333
  for (let i = 0; i < 100; i++) {
6677
8334
  const index = Math.floor(this.framePositions.length * (i / 100));
@@ -6841,7 +8498,7 @@ ${cue.notes ?? ""}`;
6841
8498
  addEncodedVideoPacket() {
6842
8499
  throw new Error("Video tracks are not supported.");
6843
8500
  }
6844
- getTrackData(track, meta) {
8501
+ getTrackData(track, meta2) {
6845
8502
  const existingTrackData = this.trackDatas.find((td) => td.track === track);
6846
8503
  if (existingTrackData) {
6847
8504
  return existingTrackData;
@@ -6851,13 +8508,13 @@ ${cue.notes ?? ""}`;
6851
8508
  serialNumber = Math.floor(2 ** 32 * Math.random());
6852
8509
  } while (this.trackDatas.some((td) => td.serialNumber === serialNumber));
6853
8510
  assert(track.source._codec === "vorbis" || track.source._codec === "opus");
6854
- validateAudioChunkMetadata(meta);
6855
- assert(meta);
6856
- assert(meta.decoderConfig);
8511
+ validateAudioChunkMetadata(meta2);
8512
+ assert(meta2);
8513
+ assert(meta2.decoderConfig);
6857
8514
  const newTrackData = {
6858
8515
  track,
6859
8516
  serialNumber,
6860
- internalSampleRate: track.source._codec === "opus" ? OPUS_INTERNAL_SAMPLE_RATE : meta.decoderConfig.sampleRate,
8517
+ internalSampleRate: track.source._codec === "opus" ? OPUS_INTERNAL_SAMPLE_RATE : meta2.decoderConfig.sampleRate,
6861
8518
  codecInfo: {
6862
8519
  codec: track.source._codec,
6863
8520
  vorbisInfo: null,
@@ -6873,18 +8530,18 @@ ${cue.notes ?? ""}`;
6873
8530
  currentPageSize: 27,
6874
8531
  currentPageStartsWithFreshPacket: true
6875
8532
  };
6876
- this.queueHeaderPackets(newTrackData, meta);
8533
+ this.queueHeaderPackets(newTrackData, meta2);
6877
8534
  this.trackDatas.push(newTrackData);
6878
8535
  if (this.allTracksAreKnown()) {
6879
8536
  this.allTracksKnown.resolve();
6880
8537
  }
6881
8538
  return newTrackData;
6882
8539
  }
6883
- queueHeaderPackets(trackData, meta) {
6884
- assert(meta.decoderConfig);
8540
+ queueHeaderPackets(trackData, meta2) {
8541
+ assert(meta2.decoderConfig);
6885
8542
  if (trackData.track.source._codec === "vorbis") {
6886
- assert(meta.decoderConfig.description);
6887
- const bytes2 = toUint8Array(meta.decoderConfig.description);
8543
+ assert(meta2.decoderConfig.description);
8544
+ const bytes2 = toUint8Array(meta2.decoderConfig.description);
6888
8545
  if (bytes2[0] !== 2) {
6889
8546
  throw new TypeError("First byte of Vorbis decoder description must be 2.");
6890
8547
  }
@@ -6909,8 +8566,17 @@ ${cue.notes ?? ""}`;
6909
8566
  throw new TypeError("Vorbis decoder description is too short.");
6910
8567
  }
6911
8568
  const identificationHeader = bytes2.subarray(pos, pos += identificationHeaderLength);
6912
- const commentHeader = bytes2.subarray(pos, pos += commentHeaderLength);
8569
+ pos += commentHeaderLength;
6913
8570
  const setupHeader = bytes2.subarray(pos);
8571
+ const commentHeaderHeader = new Uint8Array(7);
8572
+ commentHeaderHeader[0] = 3;
8573
+ commentHeaderHeader[1] = 118;
8574
+ commentHeaderHeader[2] = 111;
8575
+ commentHeaderHeader[3] = 114;
8576
+ commentHeaderHeader[4] = 98;
8577
+ commentHeaderHeader[5] = 105;
8578
+ commentHeaderHeader[6] = 115;
8579
+ const commentHeader = this.createVorbisComments(commentHeaderHeader);
6914
8580
  trackData.packetQueue.push({
6915
8581
  data: identificationHeader,
6916
8582
  endGranulePosition: 0,
@@ -6938,16 +8604,15 @@ ${cue.notes ?? ""}`;
6938
8604
  modeBlockflags: parseModesFromVorbisSetupPacket(setupHeader).modeBlockflags
6939
8605
  };
6940
8606
  } else if (trackData.track.source._codec === "opus") {
6941
- if (!meta.decoderConfig.description) {
8607
+ if (!meta2.decoderConfig.description) {
6942
8608
  throw new TypeError("For Ogg, Opus decoder description is required.");
6943
8609
  }
6944
- const identificationHeader = toUint8Array(meta.decoderConfig.description);
6945
- const commentHeader = new Uint8Array(8 + 4 + 4);
6946
- const view2 = new DataView(commentHeader.buffer);
6947
- view2.setUint32(0, 1332770163, false);
6948
- view2.setUint32(4, 1415669619, false);
6949
- view2.setUint32(8, 0, true);
6950
- view2.setUint32(12, 0, true);
8610
+ const identificationHeader = toUint8Array(meta2.decoderConfig.description);
8611
+ const commentHeaderHeader = new Uint8Array(8);
8612
+ const commentHeaderHeaderView = toDataView(commentHeaderHeader);
8613
+ commentHeaderHeaderView.setUint32(0, 1332770163, false);
8614
+ commentHeaderHeaderView.setUint32(4, 1415669619, false);
8615
+ const commentHeader = this.createVorbisComments(commentHeaderHeader);
6951
8616
  trackData.packetQueue.push({
6952
8617
  data: identificationHeader,
6953
8618
  endGranulePosition: 0,
@@ -6965,10 +8630,182 @@ ${cue.notes ?? ""}`;
6965
8630
  };
6966
8631
  }
6967
8632
  }
6968
- async addEncodedAudioPacket(track, packet, meta) {
8633
+ createVorbisComments(headerBytes) {
8634
+ const tags = this.output._metadataTags;
8635
+ const commentHeaderParts = [
8636
+ headerBytes
8637
+ ];
8638
+ let vendorString = "";
8639
+ if (typeof tags.raw?.["vendor"] === "string") {
8640
+ vendorString = tags.raw?.["vendor"];
8641
+ }
8642
+ const encodedVendorString = textEncoder.encode(vendorString);
8643
+ let currentBuffer = new Uint8Array(4 + encodedVendorString.length);
8644
+ let currentView = new DataView(currentBuffer.buffer);
8645
+ currentView.setUint32(0, encodedVendorString.length, true);
8646
+ currentBuffer.set(encodedVendorString, 4);
8647
+ commentHeaderParts.push(currentBuffer);
8648
+ const writtenTags = /* @__PURE__ */ new Set();
8649
+ const addCommentTag = (key, value) => {
8650
+ const joined = `${key}=${value}`;
8651
+ const encoded = textEncoder.encode(joined);
8652
+ currentBuffer = new Uint8Array(4 + encoded.length);
8653
+ currentView = new DataView(currentBuffer.buffer);
8654
+ currentView.setUint32(0, encoded.length, true);
8655
+ currentBuffer.set(encoded, 4);
8656
+ commentHeaderParts.push(currentBuffer);
8657
+ writtenTags.add(key);
8658
+ };
8659
+ for (const { key, value } of keyValueIterator(tags)) {
8660
+ switch (key) {
8661
+ case "title":
8662
+ {
8663
+ addCommentTag("TITLE", value);
8664
+ }
8665
+ ;
8666
+ break;
8667
+ case "description":
8668
+ {
8669
+ addCommentTag("DESCRIPTION", value);
8670
+ }
8671
+ ;
8672
+ break;
8673
+ case "artist":
8674
+ {
8675
+ addCommentTag("ARTIST", value);
8676
+ }
8677
+ ;
8678
+ break;
8679
+ case "album":
8680
+ {
8681
+ addCommentTag("ALBUM", value);
8682
+ }
8683
+ ;
8684
+ break;
8685
+ case "albumArtist":
8686
+ {
8687
+ addCommentTag("ALBUMARTIST", value);
8688
+ }
8689
+ ;
8690
+ break;
8691
+ case "genre":
8692
+ {
8693
+ addCommentTag("GENRE", value);
8694
+ }
8695
+ ;
8696
+ break;
8697
+ case "date":
8698
+ {
8699
+ addCommentTag("DATE", value.toISOString().slice(0, 10));
8700
+ }
8701
+ ;
8702
+ break;
8703
+ case "comment":
8704
+ {
8705
+ addCommentTag("COMMENT", value);
8706
+ }
8707
+ ;
8708
+ break;
8709
+ case "lyrics":
8710
+ {
8711
+ addCommentTag("LYRICS", value);
8712
+ }
8713
+ ;
8714
+ break;
8715
+ case "trackNumber":
8716
+ {
8717
+ addCommentTag("TRACKNUMBER", value.toString());
8718
+ }
8719
+ ;
8720
+ break;
8721
+ case "tracksTotal":
8722
+ {
8723
+ addCommentTag("TRACKTOTAL", value.toString());
8724
+ }
8725
+ ;
8726
+ break;
8727
+ case "discNumber":
8728
+ {
8729
+ addCommentTag("DISCNUMBER", value.toString());
8730
+ }
8731
+ ;
8732
+ break;
8733
+ case "discsTotal":
8734
+ {
8735
+ addCommentTag("DISCTOTAL", value.toString());
8736
+ }
8737
+ ;
8738
+ break;
8739
+ case "images":
8740
+ {
8741
+ for (const image of value) {
8742
+ const pictureType = image.kind === "coverFront" ? 3 : image.kind === "coverBack" ? 4 : 0;
8743
+ const encodedMediaType = new Uint8Array(image.mimeType.length);
8744
+ for (let i = 0; i < image.mimeType.length; i++) {
8745
+ encodedMediaType[i] = image.mimeType.charCodeAt(i);
8746
+ }
8747
+ const encodedDescription = textEncoder.encode(image.description ?? "");
8748
+ const buffer = new Uint8Array(
8749
+ 4 + 4 + encodedMediaType.length + 4 + encodedDescription.length + 16 + 4 + image.data.length
8750
+ // Picture data
8751
+ );
8752
+ const view2 = toDataView(buffer);
8753
+ view2.setUint32(0, pictureType, false);
8754
+ view2.setUint32(4, encodedMediaType.length, false);
8755
+ buffer.set(encodedMediaType, 8);
8756
+ view2.setUint32(8 + encodedMediaType.length, encodedDescription.length, false);
8757
+ buffer.set(encodedDescription, 12 + encodedMediaType.length);
8758
+ view2.setUint32(
8759
+ 28 + encodedMediaType.length + encodedDescription.length,
8760
+ image.data.length,
8761
+ false
8762
+ );
8763
+ buffer.set(
8764
+ image.data,
8765
+ 32 + encodedMediaType.length + encodedDescription.length
8766
+ );
8767
+ const encoded = bytesToBase64(buffer);
8768
+ addCommentTag("METADATA_BLOCK_PICTURE", encoded);
8769
+ }
8770
+ }
8771
+ ;
8772
+ break;
8773
+ case "raw":
8774
+ {
8775
+ }
8776
+ ;
8777
+ break;
8778
+ default:
8779
+ assertNever(key);
8780
+ }
8781
+ }
8782
+ if (tags.raw) {
8783
+ for (const key in tags.raw) {
8784
+ const value = tags.raw[key];
8785
+ if (key === "vendor" || value == null || writtenTags.has(key)) {
8786
+ continue;
8787
+ }
8788
+ if (typeof value === "string") {
8789
+ addCommentTag(key, value);
8790
+ }
8791
+ }
8792
+ }
8793
+ const listLengthBuffer = new Uint8Array(4);
8794
+ toDataView(listLengthBuffer).setUint32(0, writtenTags.size, true);
8795
+ commentHeaderParts.splice(2, 0, listLengthBuffer);
8796
+ const commentHeaderLength = commentHeaderParts.reduce((a, b) => a + b.length, 0);
8797
+ const commentHeader = new Uint8Array(commentHeaderLength);
8798
+ let pos = 0;
8799
+ for (const part of commentHeaderParts) {
8800
+ commentHeader.set(part, pos);
8801
+ pos += part.length;
8802
+ }
8803
+ return commentHeader;
8804
+ }
8805
+ async addEncodedAudioPacket(track, packet, meta2) {
6969
8806
  const release = await this.mutex.acquire();
6970
8807
  try {
6971
- const trackData = this.getTrackData(track, meta);
8808
+ const trackData = this.getTrackData(track, meta2);
6972
8809
  this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === "key");
6973
8810
  const currentTimestampInSamples = trackData.currentTimestampInSamples;
6974
8811
  const { durationInSamples, vorbisBlockSize } = extractSampleMetadata(
@@ -9643,7 +11480,8 @@ ${cue.notes ?? ""}`;
9643
11480
  }
9644
11481
  /**
9645
11482
  * Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#video-decoder-config) for decoding the
9646
- * track's packets using a VideoDecoder. Returns null if the track's codec is unknown.
11483
+ * track's packets using a [`VideoDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder). Returns
11484
+ * null if the track's codec is unknown.
9647
11485
  */
9648
11486
  getDecoderConfig() {
9649
11487
  return this._backing.getDecoderConfig();
@@ -9708,7 +11546,8 @@ ${cue.notes ?? ""}`;
9708
11546
  }
9709
11547
  /**
9710
11548
  * Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#audio-decoder-config) for decoding the
9711
- * track's packets using an AudioDecoder. Returns null if the track's codec is unknown.
11549
+ * track's packets using an [`AudioDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder). Returns
11550
+ * null if the track's codec is unknown.
9712
11551
  */
9713
11552
  getDecoderConfig() {
9714
11553
  return this._backing.getDecoderConfig();
@@ -9763,6 +11602,7 @@ ${cue.notes ?? ""}`;
9763
11602
  this.audioInfo = null;
9764
11603
  this.tracks = [];
9765
11604
  this.lastKnownPacketIndex = 0;
11605
+ this.metadataTags = {};
9766
11606
  this.reader = input._reader;
9767
11607
  }
9768
11608
  async readMetadata() {
@@ -9798,10 +11638,15 @@ ${cue.notes ?? ""}`;
9798
11638
  dataChunkSize ??= chunkSize;
9799
11639
  this.dataStart = slice2.filePos;
9800
11640
  this.dataSize = Math.min(dataChunkSize, (totalFileSize ?? Infinity) - this.dataStart);
11641
+ if (this.reader.fileSize === null) {
11642
+ break;
11643
+ }
9801
11644
  } else if (chunkId === "ds64") {
9802
11645
  const riffChunkSize = readU64(slice2, littleEndian);
9803
11646
  dataChunkSize = readU64(slice2, littleEndian);
9804
11647
  totalFileSize = Math.min(riffChunkSize + 8, this.reader.fileSize ?? Infinity);
11648
+ } else if (chunkId === "LIST") {
11649
+ await this.parseListChunk(startPos, chunkSize, littleEndian);
9805
11650
  }
9806
11651
  currentPos = startPos + chunkSize + (chunkSize & 1);
9807
11652
  chunksRead++;
@@ -9853,6 +11698,110 @@ ${cue.notes ?? ""}`;
9853
11698
  blockSizeInBytes: blockAlign
9854
11699
  };
9855
11700
  }
11701
+ async parseListChunk(startPos, size, littleEndian) {
11702
+ let slice = this.reader.requestSlice(startPos, size);
11703
+ if (slice instanceof Promise) slice = await slice;
11704
+ if (!slice) return;
11705
+ const infoType = readAscii(slice, 4);
11706
+ if (infoType !== "INFO" && infoType !== "INF0") {
11707
+ return;
11708
+ }
11709
+ let currentPos = slice.filePos;
11710
+ while (currentPos <= startPos + size - 8) {
11711
+ slice.filePos = currentPos;
11712
+ const chunkName = readAscii(slice, 4);
11713
+ const chunkSize = readU32(slice, littleEndian);
11714
+ const bytes2 = readBytes(slice, chunkSize);
11715
+ let stringLength = 0;
11716
+ for (let i = 0; i < bytes2.length; i++) {
11717
+ if (bytes2[i] === 0) {
11718
+ break;
11719
+ }
11720
+ stringLength++;
11721
+ }
11722
+ const value = String.fromCharCode(...bytes2.subarray(0, stringLength));
11723
+ this.metadataTags.raw ??= {};
11724
+ this.metadataTags.raw[chunkName] = value;
11725
+ switch (chunkName) {
11726
+ case "INAM":
11727
+ case "TITL":
11728
+ {
11729
+ this.metadataTags.title ??= value;
11730
+ }
11731
+ ;
11732
+ break;
11733
+ case "TIT3":
11734
+ {
11735
+ this.metadataTags.description ??= value;
11736
+ }
11737
+ ;
11738
+ break;
11739
+ case "IART":
11740
+ {
11741
+ this.metadataTags.artist ??= value;
11742
+ }
11743
+ ;
11744
+ break;
11745
+ case "IPRD":
11746
+ {
11747
+ this.metadataTags.album ??= value;
11748
+ }
11749
+ ;
11750
+ break;
11751
+ case "IPRT":
11752
+ case "ITRK":
11753
+ case "TRCK":
11754
+ {
11755
+ const parts = value.split("/");
11756
+ const trackNum = Number.parseInt(parts[0], 10);
11757
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
11758
+ if (Number.isInteger(trackNum) && trackNum > 0) {
11759
+ this.metadataTags.trackNumber ??= trackNum;
11760
+ }
11761
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
11762
+ this.metadataTags.tracksTotal ??= tracksTotal;
11763
+ }
11764
+ }
11765
+ ;
11766
+ break;
11767
+ case "ICRD":
11768
+ case "IDIT":
11769
+ {
11770
+ const date = new Date(value);
11771
+ if (!Number.isNaN(date.getTime())) {
11772
+ this.metadataTags.date ??= date;
11773
+ }
11774
+ }
11775
+ ;
11776
+ break;
11777
+ case "YEAR":
11778
+ {
11779
+ const year = Number.parseInt(value, 10);
11780
+ if (Number.isInteger(year) && year > 0) {
11781
+ this.metadataTags.date ??= new Date(year, 0, 1);
11782
+ }
11783
+ }
11784
+ ;
11785
+ break;
11786
+ case "IGNR":
11787
+ case "GENR":
11788
+ {
11789
+ this.metadataTags.genre ??= value;
11790
+ }
11791
+ ;
11792
+ break;
11793
+ case "ICMT":
11794
+ case "CMNT":
11795
+ case "COMM":
11796
+ {
11797
+ this.metadataTags.comment ??= value;
11798
+ }
11799
+ ;
11800
+ break;
11801
+ }
11802
+ currentPos += 8 + chunkSize + (chunkSize & 1);
11803
+ }
11804
+ }
9856
11805
  getCodec() {
9857
11806
  assert(this.audioInfo);
9858
11807
  if (this.audioInfo.format === 7 /* MULAW */) {
@@ -9892,6 +11841,10 @@ ${cue.notes ?? ""}`;
9892
11841
  await this.readMetadata();
9893
11842
  return this.tracks;
9894
11843
  }
11844
+ async getMetadataTags() {
11845
+ await this.readMetadata();
11846
+ return this.metadataTags;
11847
+ }
9895
11848
  };
9896
11849
  var PACKET_SIZE_IN_FRAMES = 2048;
9897
11850
  var WaveAudioTrackBacking = class {
@@ -10059,6 +12012,11 @@ ${cue.notes ?? ""}`;
10059
12012
  this.dataSize = 0;
10060
12013
  this.sampleRate = null;
10061
12014
  this.sampleCount = 0;
12015
+ this.riffSizePos = null;
12016
+ this.dataSizePos = null;
12017
+ this.ds64RiffSizePos = null;
12018
+ this.ds64DataSizePos = null;
12019
+ this.ds64SampleCountPos = null;
10062
12020
  this.format = format;
10063
12021
  this.writer = output._writer;
10064
12022
  this.riffWriter = new RiffWriter(output._writer);
@@ -10072,15 +12030,15 @@ ${cue.notes ?? ""}`;
10072
12030
  async addEncodedVideoPacket() {
10073
12031
  throw new Error("WAVE does not support video.");
10074
12032
  }
10075
- async addEncodedAudioPacket(track, packet, meta) {
12033
+ async addEncodedAudioPacket(track, packet, meta2) {
10076
12034
  const release = await this.mutex.acquire();
10077
12035
  try {
10078
12036
  if (!this.headerWritten) {
10079
- validateAudioChunkMetadata(meta);
10080
- assert(meta);
10081
- assert(meta.decoderConfig);
10082
- this.writeHeader(track, meta.decoderConfig);
10083
- this.sampleRate = meta.decoderConfig.sampleRate;
12037
+ validateAudioChunkMetadata(meta2);
12038
+ assert(meta2);
12039
+ assert(meta2.decoderConfig);
12040
+ this.writeHeader(track, meta2.decoderConfig);
12041
+ this.sampleRate = meta2.decoderConfig.sampleRate;
10084
12042
  this.headerWritten = true;
10085
12043
  }
10086
12044
  this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === "key");
@@ -10123,50 +12081,179 @@ ${cue.notes ?? ""}`;
10123
12081
  if (this.isRf64) {
10124
12082
  this.riffWriter.writeU32(4294967295);
10125
12083
  } else {
12084
+ this.riffSizePos = this.writer.getPos();
10126
12085
  this.riffWriter.writeU32(0);
10127
12086
  }
10128
12087
  this.riffWriter.writeAscii("WAVE");
10129
12088
  if (this.isRf64) {
10130
12089
  this.riffWriter.writeAscii("ds64");
10131
12090
  this.riffWriter.writeU32(28);
12091
+ this.ds64RiffSizePos = this.writer.getPos();
10132
12092
  this.riffWriter.writeU64(0);
12093
+ this.ds64DataSizePos = this.writer.getPos();
10133
12094
  this.riffWriter.writeU64(0);
12095
+ this.ds64SampleCountPos = this.writer.getPos();
10134
12096
  this.riffWriter.writeU64(0);
10135
12097
  this.riffWriter.writeU32(0);
10136
12098
  }
10137
- this.riffWriter.writeAscii("fmt ");
10138
- this.riffWriter.writeU32(16);
10139
- this.riffWriter.writeU16(format);
10140
- this.riffWriter.writeU16(channels);
10141
- this.riffWriter.writeU32(sampleRate);
10142
- this.riffWriter.writeU32(sampleRate * blockSize);
10143
- this.riffWriter.writeU16(blockSize);
10144
- this.riffWriter.writeU16(8 * pcmInfo.sampleSize);
10145
- this.riffWriter.writeAscii("data");
10146
- if (this.isRf64) {
10147
- this.riffWriter.writeU32(4294967295);
10148
- } else {
10149
- this.riffWriter.writeU32(0);
12099
+ this.riffWriter.writeAscii("fmt ");
12100
+ this.riffWriter.writeU32(16);
12101
+ this.riffWriter.writeU16(format);
12102
+ this.riffWriter.writeU16(channels);
12103
+ this.riffWriter.writeU32(sampleRate);
12104
+ this.riffWriter.writeU32(sampleRate * blockSize);
12105
+ this.riffWriter.writeU16(blockSize);
12106
+ this.riffWriter.writeU16(8 * pcmInfo.sampleSize);
12107
+ if (!metadataTagsAreEmpty(this.output._metadataTags)) {
12108
+ this.writeInfoChunk(this.output._metadataTags);
12109
+ }
12110
+ this.riffWriter.writeAscii("data");
12111
+ if (this.isRf64) {
12112
+ this.riffWriter.writeU32(4294967295);
12113
+ } else {
12114
+ this.dataSizePos = this.writer.getPos();
12115
+ this.riffWriter.writeU32(0);
12116
+ }
12117
+ if (this.format._options.onHeader) {
12118
+ const { data, start } = this.writer.stopTrackingWrites();
12119
+ this.format._options.onHeader(data, start);
12120
+ }
12121
+ }
12122
+ writeInfoChunk(metadata) {
12123
+ const startPos = this.writer.getPos();
12124
+ this.riffWriter.writeAscii("LIST");
12125
+ this.riffWriter.writeU32(0);
12126
+ this.riffWriter.writeAscii("INFO");
12127
+ const writtenTags = /* @__PURE__ */ new Set();
12128
+ const writeInfoTag = (tag, value) => {
12129
+ if (!isIso88591Compatible(value)) {
12130
+ console.warn(`Didn't write tag '${tag}' because '${value}' is not ISO 8859-1-compatible.`);
12131
+ return;
12132
+ }
12133
+ const size = value.length + 1;
12134
+ const bytes2 = new Uint8Array(size);
12135
+ for (let i = 0; i < value.length; i++) {
12136
+ bytes2[i] = value.charCodeAt(i);
12137
+ }
12138
+ this.riffWriter.writeAscii(tag);
12139
+ this.riffWriter.writeU32(size);
12140
+ this.writer.write(bytes2);
12141
+ if (size & 1) {
12142
+ this.writer.write(new Uint8Array(1));
12143
+ }
12144
+ writtenTags.add(tag);
12145
+ };
12146
+ for (const { key, value } of keyValueIterator(metadata)) {
12147
+ switch (key) {
12148
+ case "title":
12149
+ {
12150
+ writeInfoTag("INAM", value);
12151
+ writtenTags.add("INAM");
12152
+ }
12153
+ ;
12154
+ break;
12155
+ case "artist":
12156
+ {
12157
+ writeInfoTag("IART", value);
12158
+ writtenTags.add("IART");
12159
+ }
12160
+ ;
12161
+ break;
12162
+ case "album":
12163
+ {
12164
+ writeInfoTag("IPRD", value);
12165
+ writtenTags.add("IPRD");
12166
+ }
12167
+ ;
12168
+ break;
12169
+ case "trackNumber":
12170
+ {
12171
+ const string = metadata.tracksTotal !== void 0 ? `${value}/${metadata.tracksTotal}` : value.toString();
12172
+ writeInfoTag("ITRK", string);
12173
+ writtenTags.add("ITRK");
12174
+ }
12175
+ ;
12176
+ break;
12177
+ case "genre":
12178
+ {
12179
+ writeInfoTag("IGNR", value);
12180
+ writtenTags.add("IGNR");
12181
+ }
12182
+ ;
12183
+ break;
12184
+ case "date":
12185
+ {
12186
+ writeInfoTag("ICRD", value.toISOString().slice(0, 10));
12187
+ writtenTags.add("ICRD");
12188
+ }
12189
+ ;
12190
+ break;
12191
+ case "comment":
12192
+ {
12193
+ writeInfoTag("ICMT", value);
12194
+ writtenTags.add("ICMT");
12195
+ }
12196
+ ;
12197
+ break;
12198
+ case "albumArtist":
12199
+ case "discNumber":
12200
+ case "tracksTotal":
12201
+ case "discsTotal":
12202
+ case "description":
12203
+ case "lyrics":
12204
+ case "images":
12205
+ {
12206
+ }
12207
+ ;
12208
+ break;
12209
+ case "raw":
12210
+ {
12211
+ }
12212
+ ;
12213
+ break;
12214
+ default:
12215
+ assertNever(key);
12216
+ }
12217
+ }
12218
+ if (metadata.raw) {
12219
+ for (const key in metadata.raw) {
12220
+ const value = metadata.raw[key];
12221
+ if (value == null || key.length !== 4 || writtenTags.has(key)) {
12222
+ continue;
12223
+ }
12224
+ if (typeof value === "string") {
12225
+ writeInfoTag(key, value);
12226
+ }
12227
+ }
10150
12228
  }
10151
- if (this.format._options.onHeader) {
10152
- const { data, start } = this.writer.stopTrackingWrites();
10153
- this.format._options.onHeader(data, start);
12229
+ const endPos = this.writer.getPos();
12230
+ const chunkSize = endPos - startPos - 8;
12231
+ this.writer.seek(startPos + 4);
12232
+ this.riffWriter.writeU32(chunkSize);
12233
+ this.writer.seek(endPos);
12234
+ if (chunkSize & 1) {
12235
+ this.writer.write(new Uint8Array(1));
10154
12236
  }
10155
12237
  }
10156
12238
  async finalize() {
10157
12239
  const release = await this.mutex.acquire();
10158
12240
  const endPos = this.writer.getPos();
10159
12241
  if (this.isRf64) {
10160
- this.writer.seek(20);
12242
+ assert(this.ds64RiffSizePos !== null);
12243
+ this.writer.seek(this.ds64RiffSizePos);
10161
12244
  this.riffWriter.writeU64(endPos - 8);
10162
- this.writer.seek(28);
12245
+ assert(this.ds64DataSizePos !== null);
12246
+ this.writer.seek(this.ds64DataSizePos);
10163
12247
  this.riffWriter.writeU64(this.dataSize);
10164
- this.writer.seek(36);
12248
+ assert(this.ds64SampleCountPos !== null);
12249
+ this.writer.seek(this.ds64SampleCountPos);
10165
12250
  this.riffWriter.writeU64(this.sampleCount);
10166
12251
  } else {
10167
- this.writer.seek(4);
12252
+ assert(this.riffSizePos !== null);
12253
+ this.writer.seek(this.riffSizePos);
10168
12254
  this.riffWriter.writeU32(endPos - 8);
10169
- this.writer.seek(40);
12255
+ assert(this.dataSizePos !== null);
12256
+ this.writer.seek(this.dataSizePos);
10170
12257
  this.riffWriter.writeU32(this.dataSize);
10171
12258
  }
10172
12259
  this.writer.seek(endPos);
@@ -10235,7 +12322,7 @@ ${cue.notes ?? ""}`;
10235
12322
  }
10236
12323
  /** @internal */
10237
12324
  _createMuxer(output) {
10238
- return new IsobmffMuxer(output, this);
12325
+ return new IsobmffMuxer2(output, this);
10239
12326
  }
10240
12327
  };
10241
12328
  var Mp4OutputFormat = class extends IsobmffOutputFormat2 {
@@ -11049,18 +13136,18 @@ ${cue.notes ?? ""}`;
11049
13136
  * @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
11050
13137
  * to respect writer and encoder backpressure.
11051
13138
  */
11052
- add(packet, meta) {
13139
+ add(packet, meta2) {
11053
13140
  if (!(packet instanceof EncodedPacket)) {
11054
13141
  throw new TypeError("packet must be an EncodedPacket.");
11055
13142
  }
11056
13143
  if (packet.isMetadataOnly) {
11057
13144
  throw new TypeError("Metadata-only packets cannot be added.");
11058
13145
  }
11059
- if (meta !== void 0 && (!meta || typeof meta !== "object")) {
13146
+ if (meta2 !== void 0 && (!meta2 || typeof meta2 !== "object")) {
11060
13147
  throw new TypeError("meta, when provided, must be an object.");
11061
13148
  }
11062
13149
  this._ensureValidAdd();
11063
- return this._connectedTrack.output._muxer.addEncodedVideoPacket(this._connectedTrack, packet, meta);
13150
+ return this._connectedTrack.output._muxer.addEncodedVideoPacket(this._connectedTrack, packet, meta2);
11064
13151
  }
11065
13152
  };
11066
13153
  var VideoEncoderWrapper = class {
@@ -11195,15 +13282,15 @@ ${cue.notes ?? ""}`;
11195
13282
  this.customEncoder = new MatchingCustomEncoder();
11196
13283
  this.customEncoder.codec = this.encodingConfig.codec;
11197
13284
  this.customEncoder.config = encoderConfig;
11198
- this.customEncoder.onPacket = (packet, meta) => {
13285
+ this.customEncoder.onPacket = (packet, meta2) => {
11199
13286
  if (!(packet instanceof EncodedPacket)) {
11200
13287
  throw new TypeError("The first argument passed to onPacket must be an EncodedPacket.");
11201
13288
  }
11202
- if (meta !== void 0 && (!meta || typeof meta !== "object")) {
13289
+ if (meta2 !== void 0 && (!meta2 || typeof meta2 !== "object")) {
11203
13290
  throw new TypeError("The second argument passed to onPacket must be an object or undefined.");
11204
13291
  }
11205
- this.encodingConfig.onEncodedPacket?.(packet, meta);
11206
- void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta);
13292
+ this.encodingConfig.onEncodedPacket?.(packet, meta2);
13293
+ void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta2);
11207
13294
  };
11208
13295
  await this.customEncoder.init();
11209
13296
  } else {
@@ -11217,10 +13304,10 @@ ${cue.notes ?? ""}`;
11217
13304
  );
11218
13305
  }
11219
13306
  this.encoder = new VideoEncoder({
11220
- output: (chunk, meta) => {
13307
+ output: (chunk, meta2) => {
11221
13308
  const packet = EncodedPacket.fromEncodedChunk(chunk);
11222
- this.encodingConfig.onEncodedPacket?.(packet, meta);
11223
- void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta);
13309
+ this.encodingConfig.onEncodedPacket?.(packet, meta2);
13310
+ void this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta2);
11224
13311
  },
11225
13312
  error: (error) => {
11226
13313
  error.stack = encoderError.stack;
@@ -11491,18 +13578,18 @@ ${cue.notes ?? ""}`;
11491
13578
  * @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
11492
13579
  * to respect writer and encoder backpressure.
11493
13580
  */
11494
- add(packet, meta) {
13581
+ add(packet, meta2) {
11495
13582
  if (!(packet instanceof EncodedPacket)) {
11496
13583
  throw new TypeError("packet must be an EncodedPacket.");
11497
13584
  }
11498
13585
  if (packet.isMetadataOnly) {
11499
13586
  throw new TypeError("Metadata-only packets cannot be added.");
11500
13587
  }
11501
- if (meta !== void 0 && (!meta || typeof meta !== "object")) {
13588
+ if (meta2 !== void 0 && (!meta2 || typeof meta2 !== "object")) {
11502
13589
  throw new TypeError("meta, when provided, must be an object.");
11503
13590
  }
11504
13591
  this._ensureValidAdd();
11505
- return this._connectedTrack.output._muxer.addEncodedAudioPacket(this._connectedTrack, packet, meta);
13592
+ return this._connectedTrack.output._muxer.addEncodedAudioPacket(this._connectedTrack, packet, meta2);
11506
13593
  }
11507
13594
  };
11508
13595
  var AudioEncoderWrapper = class {
@@ -11613,7 +13700,7 @@ ${cue.notes ?? ""}`;
11613
13700
  if (shouldClose) {
11614
13701
  audioSample.close();
11615
13702
  }
11616
- const meta = {
13703
+ const meta2 = {
11617
13704
  decoderConfig: {
11618
13705
  codec: this.encodingConfig.codec,
11619
13706
  numberOfChannels,
@@ -11630,8 +13717,8 @@ ${cue.notes ?? ""}`;
11630
13717
  timestamp + startFrame / sampleRate,
11631
13718
  frameCount / sampleRate
11632
13719
  );
11633
- this.encodingConfig.onEncodedPacket?.(packet, meta);
11634
- await this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta);
13720
+ this.encodingConfig.onEncodedPacket?.(packet, meta2);
13721
+ await this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta2);
11635
13722
  }
11636
13723
  }
11637
13724
  ensureEncoder(audioSample) {
@@ -11655,15 +13742,15 @@ ${cue.notes ?? ""}`;
11655
13742
  this.customEncoder = new MatchingCustomEncoder();
11656
13743
  this.customEncoder.codec = this.encodingConfig.codec;
11657
13744
  this.customEncoder.config = encoderConfig;
11658
- this.customEncoder.onPacket = (packet, meta) => {
13745
+ this.customEncoder.onPacket = (packet, meta2) => {
11659
13746
  if (!(packet instanceof EncodedPacket)) {
11660
13747
  throw new TypeError("The first argument passed to onPacket must be an EncodedPacket.");
11661
13748
  }
11662
- if (meta !== void 0 && (!meta || typeof meta !== "object")) {
13749
+ if (meta2 !== void 0 && (!meta2 || typeof meta2 !== "object")) {
11663
13750
  throw new TypeError("The second argument passed to onPacket must be an object or undefined.");
11664
13751
  }
11665
- this.encodingConfig.onEncodedPacket?.(packet, meta);
11666
- void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta);
13752
+ this.encodingConfig.onEncodedPacket?.(packet, meta2);
13753
+ void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta2);
11667
13754
  };
11668
13755
  await this.customEncoder.init();
11669
13756
  } else if (PCM_AUDIO_CODECS.includes(this.encodingConfig.codec)) {
@@ -11679,10 +13766,10 @@ ${cue.notes ?? ""}`;
11679
13766
  );
11680
13767
  }
11681
13768
  this.encoder = new AudioEncoder({
11682
- output: (chunk, meta) => {
13769
+ output: (chunk, meta2) => {
11683
13770
  const packet = EncodedPacket.fromEncodedChunk(chunk);
11684
- this.encodingConfig.onEncodedPacket?.(packet, meta);
11685
- void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta);
13771
+ this.encodingConfig.onEncodedPacket?.(packet, meta2);
13772
+ void this.muxer.addEncodedAudioPacket(this.source._connectedTrack, packet, meta2);
11686
13773
  },
11687
13774
  error: (error) => {
11688
13775
  error.stack = encoderError.stack;
@@ -12182,6 +14269,8 @@ ${cue.notes ?? ""}`;
12182
14269
  this._finalizePromise = null;
12183
14270
  /** @internal */
12184
14271
  this._mutex = new AsyncMutex();
14272
+ /** @internal */
14273
+ this._metadataTags = {};
12185
14274
  if (!options || typeof options !== "object") {
12186
14275
  throw new TypeError("options must be an object.");
12187
14276
  }
@@ -12235,6 +14324,19 @@ ${cue.notes ?? ""}`;
12235
14324
  validateBaseTrackMetadata(metadata);
12236
14325
  this._addTrack("subtitle", source, metadata);
12237
14326
  }
14327
+ /**
14328
+ * Sets descriptive metadata tags about the media file, such as title, author, date, or cover art. When called
14329
+ * multiple times, only the metadata from the last call will be used.
14330
+ *
14331
+ * Must be called before output is started.
14332
+ */
14333
+ setMetadataTags(tags) {
14334
+ validateMetadataTags(tags);
14335
+ if (this.state !== "pending") {
14336
+ throw new Error("Cannot set metadata tags after output has been started or canceled.");
14337
+ }
14338
+ this._metadataTags = tags;
14339
+ }
12238
14340
  /** @internal */
12239
14341
  _addTrack(type, source, metadata) {
12240
14342
  if (this.state !== "pending") {
@@ -12466,6 +14568,10 @@ ${cue.notes ?? ""}`;
12466
14568
  offset: 0
12467
14569
  };
12468
14570
  }
14571
+ /** @internal */
14572
+ get _supportsRandomAccess() {
14573
+ return true;
14574
+ }
12469
14575
  };
12470
14576
  var BlobSource = class extends Source {
12471
14577
  /**
@@ -12524,6 +14630,10 @@ ${cue.notes ?? ""}`;
12524
14630
  }
12525
14631
  worker.running = false;
12526
14632
  }
14633
+ /** @internal */
14634
+ get _supportsRandomAccess() {
14635
+ return true;
14636
+ }
12527
14637
  };
12528
14638
  var URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20;
12529
14639
  var UrlSource = class extends Source {
@@ -12701,6 +14811,10 @@ ${cue.notes ?? ""}`;
12701
14811
  }
12702
14812
  }
12703
14813
  }
14814
+ /** @internal */
14815
+ get _supportsRandomAccess() {
14816
+ return true;
14817
+ }
12704
14818
  };
12705
14819
  var FilePathSource = class extends Source {
12706
14820
  /** Creates a new {@link FilePathSource} backed by the file at the specified file path. */
@@ -12745,6 +14859,10 @@ ${cue.notes ?? ""}`;
12745
14859
  _retrieveSize() {
12746
14860
  return this._streamSource._retrieveSize();
12747
14861
  }
14862
+ /** @internal */
14863
+ get _supportsRandomAccess() {
14864
+ return true;
14865
+ }
12748
14866
  };
12749
14867
  var StreamSource = class extends Source {
12750
14868
  /** Creates a new {@link StreamSource} whose behavior is specified by `options`. */
@@ -12841,6 +14959,10 @@ ${cue.notes ?? ""}`;
12841
14959
  }
12842
14960
  worker.running = false;
12843
14961
  }
14962
+ /** @internal */
14963
+ get _supportsRandomAccess() {
14964
+ return true;
14965
+ }
12844
14966
  };
12845
14967
  var ReadableStreamSource = class extends Source {
12846
14968
  /** Creates a new {@link ReadableStreamSource} backed by the specified `ReadableStream<Uint8Array>`. */
@@ -13009,6 +15131,10 @@ ${cue.notes ?? ""}`;
13009
15131
  }
13010
15132
  this._pulling = false;
13011
15133
  }
15134
+ /** @internal */
15135
+ get _supportsRandomAccess() {
15136
+ return false;
15137
+ }
13012
15138
  };
13013
15139
  var PREFETCH_PROFILES = {
13014
15140
  none: (start, end) => ({ start, end }),
@@ -13360,6 +15486,129 @@ ${cue.notes ?? ""}`;
13360
15486
  };
13361
15487
 
13362
15488
  // src/isobmff/isobmff-demuxer.ts
15489
+ var UDTA_STRING_KEYS = /* @__PURE__ */ new Set([
15490
+ "@day",
15491
+ "@mak",
15492
+ "@mod",
15493
+ "@swr",
15494
+ "@xyz",
15495
+ "CAME",
15496
+ "CNCV",
15497
+ "CNFV",
15498
+ "CNMN",
15499
+ "FIRM",
15500
+ "FOV\0",
15501
+ "GoPr",
15502
+ "LENS",
15503
+ "PXMN",
15504
+ "SIGM",
15505
+ "SNum",
15506
+ "TAGS",
15507
+ "albm",
15508
+ "albr",
15509
+ "angl",
15510
+ "auth",
15511
+ "ccid",
15512
+ "cdis",
15513
+ "clfn",
15514
+ "clid",
15515
+ "clsf",
15516
+ "cmid",
15517
+ "cmnm",
15518
+ "coll",
15519
+ "cprt",
15520
+ "cver",
15521
+ "cvru",
15522
+ "date",
15523
+ "dscp",
15524
+ "fsid",
15525
+ "gnre",
15526
+ "hinv",
15527
+ "icnu",
15528
+ "info",
15529
+ "infu",
15530
+ "kgtt",
15531
+ "loci",
15532
+ "lrcu",
15533
+ "mcvr",
15534
+ "name",
15535
+ "perf",
15536
+ "pmcc",
15537
+ "reel",
15538
+ "rtng",
15539
+ "scen",
15540
+ "shot",
15541
+ "slno",
15542
+ "thmb",
15543
+ "titl",
15544
+ "tnam",
15545
+ "urat",
15546
+ "uuid",
15547
+ "vndr",
15548
+ "yrrc",
15549
+ "\xA9ART",
15550
+ "\xA9TIM",
15551
+ "\xA9TSC",
15552
+ "\xA9TSZ",
15553
+ "\xA9alb",
15554
+ "\xA9arg",
15555
+ "\xA9ark",
15556
+ "\xA9cmt",
15557
+ "\xA9cok",
15558
+ "\xA9com",
15559
+ "\xA9cpy",
15560
+ "\xA9day",
15561
+ "\xA9dir",
15562
+ "\xA9ed1",
15563
+ "\xA9ed2",
15564
+ "\xA9ed3",
15565
+ "\xA9ed4",
15566
+ "\xA9ed5",
15567
+ "\xA9ed6",
15568
+ "\xA9ed7",
15569
+ "\xA9ed8",
15570
+ "\xA9ed9",
15571
+ "\xA9enc",
15572
+ "\xA9fmt",
15573
+ "\xA9fpt",
15574
+ "\xA9frl",
15575
+ "\xA9fyw",
15576
+ "\xA9gen",
15577
+ "\xA9gpt",
15578
+ "\xA9grl",
15579
+ "\xA9grp",
15580
+ "\xA9gyw",
15581
+ "\xA9inf",
15582
+ "\xA9isr",
15583
+ "\xA9lab",
15584
+ "\xA9lal",
15585
+ "\xA9lyr",
15586
+ "\xA9mak",
15587
+ "\xA9mal",
15588
+ "\xA9mdl",
15589
+ "\xA9mod",
15590
+ "\xA9nam",
15591
+ "\xA9pdk",
15592
+ "\xA9phg",
15593
+ "\xA9prd",
15594
+ "\xA9prf",
15595
+ "\xA9prk",
15596
+ "\xA9prl",
15597
+ "\xA9req",
15598
+ "\xA9snk",
15599
+ "\xA9snm",
15600
+ "\xA9src",
15601
+ "\xA9swf",
15602
+ "\xA9swk",
15603
+ "\xA9swr",
15604
+ "\xA9too",
15605
+ "\xA9trk",
15606
+ "\xA9wrt",
15607
+ "\xA9xsp",
15608
+ "\xA9xyz",
15609
+ "\xA9ysp",
15610
+ "\xA9zsp"
15611
+ ]);
13363
15612
  var IsobmffDemuxer = class extends Demuxer {
13364
15613
  constructor(input) {
13365
15614
  super(input);
@@ -13370,6 +15619,8 @@ ${cue.notes ?? ""}`;
13370
15619
  this.movieTimescale = -1;
13371
15620
  this.movieDurationInTimescale = -1;
13372
15621
  this.isQuickTime = false;
15622
+ this.metadataTags = {};
15623
+ this.currentMetadataKeys = null;
13373
15624
  this.isFragmented = false;
13374
15625
  this.fragmentTrackDefaults = [];
13375
15626
  this.fragments = [];
@@ -13396,6 +15647,10 @@ ${cue.notes ?? ""}`;
13396
15647
  codecStrings: codecStrings.filter(Boolean)
13397
15648
  });
13398
15649
  }
15650
+ async getMetadataTags() {
15651
+ await this.readMetadata();
15652
+ return this.metadataTags;
15653
+ }
13399
15654
  readMetadata() {
13400
15655
  return this.metadataPromise ??= (async () => {
13401
15656
  let currentPos = 0;
@@ -13634,6 +15889,19 @@ ${cue.notes ?? ""}`;
13634
15889
  }
13635
15890
  }
13636
15891
  }
15892
+ // eslint-disable-next-line @stylistic/generator-star-spacing
15893
+ *iterateContiguousBoxes(slice) {
15894
+ const startIndex = slice.filePos;
15895
+ while (slice.filePos - startIndex <= slice.length - MIN_BOX_HEADER_SIZE) {
15896
+ const startPos = slice.filePos;
15897
+ const boxInfo = readBoxHeader(slice);
15898
+ if (!boxInfo) {
15899
+ break;
15900
+ }
15901
+ yield { boxInfo, slice };
15902
+ slice.filePos = startPos + boxInfo.totalSize;
15903
+ }
15904
+ }
13637
15905
  traverseBox(slice) {
13638
15906
  const startPos = slice.filePos;
13639
15907
  const boxInfo = readBoxHeader(slice);
@@ -13648,7 +15916,6 @@ ${cue.notes ?? ""}`;
13648
15916
  case "dinf":
13649
15917
  case "mfra":
13650
15918
  case "edts":
13651
- case "udta":
13652
15919
  {
13653
15920
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
13654
15921
  }
@@ -13713,7 +15980,9 @@ ${cue.notes ?? ""}`;
13713
15980
  case "tkhd":
13714
15981
  {
13715
15982
  const track = this.currentTrack;
13716
- assert(track);
15983
+ if (!track) {
15984
+ break;
15985
+ }
13717
15986
  const version = readU8(slice);
13718
15987
  const flags = readU24Be(slice);
13719
15988
  const trackEnabled = (flags & 1) !== 0;
@@ -13754,7 +16023,9 @@ ${cue.notes ?? ""}`;
13754
16023
  case "elst":
13755
16024
  {
13756
16025
  const track = this.currentTrack;
13757
- assert(track);
16026
+ if (!track) {
16027
+ break;
16028
+ }
13758
16029
  const version = readU8(slice);
13759
16030
  slice.skip(3);
13760
16031
  let relevantEntryFound = false;
@@ -13791,7 +16062,9 @@ ${cue.notes ?? ""}`;
13791
16062
  case "mdhd":
13792
16063
  {
13793
16064
  const track = this.currentTrack;
13794
- assert(track);
16065
+ if (!track) {
16066
+ break;
16067
+ }
13795
16068
  const version = readU8(slice);
13796
16069
  slice.skip(3);
13797
16070
  if (version === 0) {
@@ -13820,7 +16093,9 @@ ${cue.notes ?? ""}`;
13820
16093
  case "hdlr":
13821
16094
  {
13822
16095
  const track = this.currentTrack;
13823
- assert(track);
16096
+ if (!track) {
16097
+ break;
16098
+ }
13824
16099
  slice.skip(8);
13825
16100
  const handlerType = readAscii(slice, 4);
13826
16101
  if (handlerType === "vide") {
@@ -13852,7 +16127,9 @@ ${cue.notes ?? ""}`;
13852
16127
  case "stbl":
13853
16128
  {
13854
16129
  const track = this.currentTrack;
13855
- assert(track);
16130
+ if (!track) {
16131
+ break;
16132
+ }
13856
16133
  track.sampleTableByteOffset = startPos;
13857
16134
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
13858
16135
  }
@@ -13861,7 +16138,9 @@ ${cue.notes ?? ""}`;
13861
16138
  case "stsd":
13862
16139
  {
13863
16140
  const track = this.currentTrack;
13864
- assert(track);
16141
+ if (!track) {
16142
+ break;
16143
+ }
13865
16144
  if (track.info === null || track.sampleTable) {
13866
16145
  break;
13867
16146
  }
@@ -14017,7 +16296,10 @@ ${cue.notes ?? ""}`;
14017
16296
  case "avcC":
14018
16297
  {
14019
16298
  const track = this.currentTrack;
14020
- assert(track && track.info);
16299
+ if (!track) {
16300
+ break;
16301
+ }
16302
+ assert(track.info);
14021
16303
  track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
14022
16304
  }
14023
16305
  ;
@@ -14025,7 +16307,10 @@ ${cue.notes ?? ""}`;
14025
16307
  case "hvcC":
14026
16308
  {
14027
16309
  const track = this.currentTrack;
14028
- assert(track && track.info);
16310
+ if (!track) {
16311
+ break;
16312
+ }
16313
+ assert(track.info);
14029
16314
  track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
14030
16315
  }
14031
16316
  ;
@@ -14033,7 +16318,10 @@ ${cue.notes ?? ""}`;
14033
16318
  case "vpcC":
14034
16319
  {
14035
16320
  const track = this.currentTrack;
14036
- assert(track && track.info?.type === "video");
16321
+ if (!track) {
16322
+ break;
16323
+ }
16324
+ assert(track.info?.type === "video");
14037
16325
  slice.skip(4);
14038
16326
  const profile = readU8(slice);
14039
16327
  const level = readU8(slice);
@@ -14060,7 +16348,10 @@ ${cue.notes ?? ""}`;
14060
16348
  case "av1C":
14061
16349
  {
14062
16350
  const track = this.currentTrack;
14063
- assert(track && track.info?.type === "video");
16351
+ if (!track) {
16352
+ break;
16353
+ }
16354
+ assert(track.info?.type === "video");
14064
16355
  slice.skip(1);
14065
16356
  const secondByte = readU8(slice);
14066
16357
  const profile = secondByte >> 5;
@@ -14090,7 +16381,10 @@ ${cue.notes ?? ""}`;
14090
16381
  case "colr":
14091
16382
  {
14092
16383
  const track = this.currentTrack;
14093
- assert(track && track.info?.type === "video");
16384
+ if (!track) {
16385
+ break;
16386
+ }
16387
+ assert(track.info?.type === "video");
14094
16388
  const colourType = readAscii(slice, 4);
14095
16389
  if (colourType !== "nclx") {
14096
16390
  break;
@@ -14117,7 +16411,10 @@ ${cue.notes ?? ""}`;
14117
16411
  case "esds":
14118
16412
  {
14119
16413
  const track = this.currentTrack;
14120
- assert(track && track.info?.type === "audio");
16414
+ if (!track) {
16415
+ break;
16416
+ }
16417
+ assert(track.info?.type === "audio");
14121
16418
  slice.skip(4);
14122
16419
  const tag = readU8(slice);
14123
16420
  assert(tag === 3);
@@ -14176,7 +16473,10 @@ ${cue.notes ?? ""}`;
14176
16473
  case "enda":
14177
16474
  {
14178
16475
  const track = this.currentTrack;
14179
- assert(track && track.info?.type === "audio");
16476
+ if (!track) {
16477
+ break;
16478
+ }
16479
+ assert(track.info?.type === "audio");
14180
16480
  const littleEndian = readU16Be(slice) & 255;
14181
16481
  if (littleEndian) {
14182
16482
  if (track.info.codec === "pcm-s16be") {
@@ -14197,7 +16497,10 @@ ${cue.notes ?? ""}`;
14197
16497
  case "pcmC":
14198
16498
  {
14199
16499
  const track = this.currentTrack;
14200
- assert(track && track.info?.type === "audio");
16500
+ if (!track) {
16501
+ break;
16502
+ }
16503
+ assert(track.info?.type === "audio");
14201
16504
  slice.skip(1 + 3);
14202
16505
  const formatFlags = readU8(slice);
14203
16506
  const isLittleEndian = Boolean(formatFlags & 1);
@@ -14253,7 +16556,10 @@ ${cue.notes ?? ""}`;
14253
16556
  case "dOps":
14254
16557
  {
14255
16558
  const track = this.currentTrack;
14256
- assert(track && track.info?.type === "audio");
16559
+ if (!track) {
16560
+ break;
16561
+ }
16562
+ assert(track.info?.type === "audio");
14257
16563
  slice.skip(1);
14258
16564
  const outputChannelCount = readU8(slice);
14259
16565
  const preSkip = readU16Be(slice);
@@ -14286,7 +16592,10 @@ ${cue.notes ?? ""}`;
14286
16592
  case "dfLa":
14287
16593
  {
14288
16594
  const track = this.currentTrack;
14289
- assert(track && track.info?.type === "audio");
16595
+ if (!track) {
16596
+ break;
16597
+ }
16598
+ assert(track.info?.type === "audio");
14290
16599
  slice.skip(4);
14291
16600
  const BLOCK_TYPE_MASK = 127;
14292
16601
  const LAST_METADATA_BLOCK_FLAG_MASK = 128;
@@ -14324,7 +16633,9 @@ ${cue.notes ?? ""}`;
14324
16633
  case "stts":
14325
16634
  {
14326
16635
  const track = this.currentTrack;
14327
- assert(track);
16636
+ if (!track) {
16637
+ break;
16638
+ }
14328
16639
  if (!track.sampleTable) {
14329
16640
  break;
14330
16641
  }
@@ -14350,7 +16661,9 @@ ${cue.notes ?? ""}`;
14350
16661
  case "ctts":
14351
16662
  {
14352
16663
  const track = this.currentTrack;
14353
- assert(track);
16664
+ if (!track) {
16665
+ break;
16666
+ }
14354
16667
  if (!track.sampleTable) {
14355
16668
  break;
14356
16669
  }
@@ -14373,7 +16686,9 @@ ${cue.notes ?? ""}`;
14373
16686
  case "stsz":
14374
16687
  {
14375
16688
  const track = this.currentTrack;
14376
- assert(track);
16689
+ if (!track) {
16690
+ break;
16691
+ }
14377
16692
  if (!track.sampleTable) {
14378
16693
  break;
14379
16694
  }
@@ -14394,7 +16709,9 @@ ${cue.notes ?? ""}`;
14394
16709
  case "stz2":
14395
16710
  {
14396
16711
  const track = this.currentTrack;
14397
- assert(track);
16712
+ if (!track) {
16713
+ break;
16714
+ }
14398
16715
  if (!track.sampleTable) {
14399
16716
  break;
14400
16717
  }
@@ -14414,7 +16731,9 @@ ${cue.notes ?? ""}`;
14414
16731
  case "stss":
14415
16732
  {
14416
16733
  const track = this.currentTrack;
14417
- assert(track);
16734
+ if (!track) {
16735
+ break;
16736
+ }
14418
16737
  if (!track.sampleTable) {
14419
16738
  break;
14420
16739
  }
@@ -14434,7 +16753,9 @@ ${cue.notes ?? ""}`;
14434
16753
  case "stsc":
14435
16754
  {
14436
16755
  const track = this.currentTrack;
14437
- assert(track);
16756
+ if (!track) {
16757
+ break;
16758
+ }
14438
16759
  if (!track.sampleTable) {
14439
16760
  break;
14440
16761
  }
@@ -14466,7 +16787,9 @@ ${cue.notes ?? ""}`;
14466
16787
  case "stco":
14467
16788
  {
14468
16789
  const track = this.currentTrack;
14469
- assert(track);
16790
+ if (!track) {
16791
+ break;
16792
+ }
14470
16793
  if (!track.sampleTable) {
14471
16794
  break;
14472
16795
  }
@@ -14482,7 +16805,9 @@ ${cue.notes ?? ""}`;
14482
16805
  case "co64":
14483
16806
  {
14484
16807
  const track = this.currentTrack;
14485
- assert(track);
16808
+ if (!track) {
16809
+ break;
16810
+ }
14486
16811
  if (!track.sampleTable) {
14487
16812
  break;
14488
16813
  }
@@ -14793,14 +17118,316 @@ ${cue.notes ?? ""}`;
14793
17118
  }
14794
17119
  ;
14795
17120
  break;
14796
- // These appear in udta:
14797
- case "\xA9nam":
14798
- case "name":
17121
+ // Metadata section
17122
+ // https://exiftool.org/TagNames/QuickTime.html
17123
+ // https://mp4workshop.com/about
17124
+ case "udta":
17125
+ {
17126
+ const iterator = this.iterateContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
17127
+ for (const { boxInfo: boxInfo2, slice: slice2 } of iterator) {
17128
+ if (boxInfo2.name !== "meta" && !this.currentTrack) {
17129
+ const startPos2 = slice2.filePos;
17130
+ this.metadataTags.raw ??= {};
17131
+ if (UDTA_STRING_KEYS.has(boxInfo2.name)) {
17132
+ this.metadataTags.raw[boxInfo2.name] ??= readMetadataStringShort(slice2);
17133
+ } else {
17134
+ this.metadataTags.raw[boxInfo2.name] ??= readBytes(slice2, boxInfo2.contentSize);
17135
+ }
17136
+ slice2.filePos = startPos2;
17137
+ }
17138
+ switch (boxInfo2.name) {
17139
+ case "meta":
17140
+ {
17141
+ slice2.skip(-boxInfo2.headerSize);
17142
+ this.traverseBox(slice2);
17143
+ }
17144
+ ;
17145
+ break;
17146
+ case "\xA9nam":
17147
+ case "name":
17148
+ {
17149
+ if (this.currentTrack) {
17150
+ this.currentTrack.name = textDecoder.decode(readBytes(slice2, boxInfo2.contentSize));
17151
+ } else {
17152
+ this.metadataTags.title ??= readMetadataStringShort(slice2);
17153
+ }
17154
+ }
17155
+ ;
17156
+ break;
17157
+ case "\xA9des":
17158
+ {
17159
+ if (!this.currentTrack) {
17160
+ this.metadataTags.description ??= readMetadataStringShort(slice2);
17161
+ }
17162
+ }
17163
+ ;
17164
+ break;
17165
+ case "\xA9ART":
17166
+ {
17167
+ if (!this.currentTrack) {
17168
+ this.metadataTags.artist ??= readMetadataStringShort(slice2);
17169
+ }
17170
+ }
17171
+ ;
17172
+ break;
17173
+ case "\xA9alb":
17174
+ {
17175
+ if (!this.currentTrack) {
17176
+ this.metadataTags.album ??= readMetadataStringShort(slice2);
17177
+ }
17178
+ }
17179
+ ;
17180
+ break;
17181
+ case "albr":
17182
+ {
17183
+ if (!this.currentTrack) {
17184
+ this.metadataTags.albumArtist ??= readMetadataStringShort(slice2);
17185
+ }
17186
+ }
17187
+ ;
17188
+ break;
17189
+ case "\xA9gen":
17190
+ {
17191
+ if (!this.currentTrack) {
17192
+ this.metadataTags.genre ??= readMetadataStringShort(slice2);
17193
+ }
17194
+ }
17195
+ ;
17196
+ break;
17197
+ case "\xA9day":
17198
+ {
17199
+ if (!this.currentTrack) {
17200
+ const date = new Date(readMetadataStringShort(slice2));
17201
+ if (!Number.isNaN(date.getTime())) {
17202
+ this.metadataTags.date ??= date;
17203
+ }
17204
+ }
17205
+ }
17206
+ ;
17207
+ break;
17208
+ case "\xA9cmt":
17209
+ {
17210
+ if (!this.currentTrack) {
17211
+ this.metadataTags.comment ??= readMetadataStringShort(slice2);
17212
+ }
17213
+ }
17214
+ ;
17215
+ break;
17216
+ case "\xA9lyr":
17217
+ {
17218
+ if (!this.currentTrack) {
17219
+ this.metadataTags.lyrics ??= readMetadataStringShort(slice2);
17220
+ }
17221
+ }
17222
+ ;
17223
+ break;
17224
+ }
17225
+ }
17226
+ }
17227
+ ;
17228
+ break;
17229
+ case "meta":
17230
+ {
17231
+ if (this.currentTrack) {
17232
+ break;
17233
+ }
17234
+ const word = readU32Be(slice);
17235
+ const isQuickTime = word !== 0;
17236
+ this.currentMetadataKeys = /* @__PURE__ */ new Map();
17237
+ if (isQuickTime) {
17238
+ this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
17239
+ } else {
17240
+ this.readContiguousBoxes(slice.slice(contentStartPos + 4, boxInfo.contentSize - 4));
17241
+ }
17242
+ this.currentMetadataKeys = null;
17243
+ }
17244
+ ;
17245
+ break;
17246
+ case "keys":
17247
+ {
17248
+ if (!this.currentMetadataKeys) {
17249
+ break;
17250
+ }
17251
+ slice.skip(4);
17252
+ const entryCount = readU32Be(slice);
17253
+ for (let i = 0; i < entryCount; i++) {
17254
+ const keySize = readU32Be(slice);
17255
+ slice.skip(4);
17256
+ const keyName = textDecoder.decode(readBytes(slice, keySize - 8));
17257
+ this.currentMetadataKeys.set(i + 1, keyName);
17258
+ }
17259
+ }
17260
+ ;
17261
+ break;
17262
+ case "ilst":
14799
17263
  {
14800
- if (!this.currentTrack) {
17264
+ if (!this.currentMetadataKeys) {
14801
17265
  break;
14802
17266
  }
14803
- this.currentTrack.name = textDecoder.decode(readBytes(slice, boxInfo.contentSize));
17267
+ const iterator = this.iterateContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
17268
+ for (const { boxInfo: boxInfo2, slice: slice2 } of iterator) {
17269
+ let metadataKey = boxInfo2.name;
17270
+ const nameAsNumber = (metadataKey.charCodeAt(0) << 24) + (metadataKey.charCodeAt(1) << 16) + (metadataKey.charCodeAt(2) << 8) + metadataKey.charCodeAt(3);
17271
+ if (this.currentMetadataKeys.has(nameAsNumber)) {
17272
+ metadataKey = this.currentMetadataKeys.get(nameAsNumber);
17273
+ }
17274
+ const data = readDataBox(slice2);
17275
+ this.metadataTags.raw ??= {};
17276
+ this.metadataTags.raw[metadataKey] ??= data;
17277
+ switch (metadataKey) {
17278
+ case "\xA9nam":
17279
+ case "titl":
17280
+ case "com.apple.quicktime.title":
17281
+ case "title":
17282
+ {
17283
+ if (typeof data === "string") {
17284
+ this.metadataTags.title ??= data;
17285
+ }
17286
+ }
17287
+ ;
17288
+ break;
17289
+ case "\xA9des":
17290
+ case "desc":
17291
+ case "dscp":
17292
+ case "com.apple.quicktime.description":
17293
+ case "description":
17294
+ {
17295
+ if (typeof data === "string") {
17296
+ this.metadataTags.description ??= data;
17297
+ }
17298
+ }
17299
+ ;
17300
+ break;
17301
+ case "\xA9ART":
17302
+ case "com.apple.quicktime.artist":
17303
+ case "artist":
17304
+ {
17305
+ if (typeof data === "string") {
17306
+ this.metadataTags.artist ??= data;
17307
+ }
17308
+ }
17309
+ ;
17310
+ break;
17311
+ case "\xA9alb":
17312
+ case "albm":
17313
+ case "com.apple.quicktime.album":
17314
+ case "album":
17315
+ {
17316
+ if (typeof data === "string") {
17317
+ this.metadataTags.album ??= data;
17318
+ }
17319
+ }
17320
+ ;
17321
+ break;
17322
+ case "aART":
17323
+ case "album_artist":
17324
+ {
17325
+ if (typeof data === "string") {
17326
+ this.metadataTags.albumArtist ??= data;
17327
+ }
17328
+ }
17329
+ ;
17330
+ break;
17331
+ case "\xA9cmt":
17332
+ case "com.apple.quicktime.comment":
17333
+ case "comment":
17334
+ {
17335
+ if (typeof data === "string") {
17336
+ this.metadataTags.comment ??= data;
17337
+ }
17338
+ }
17339
+ ;
17340
+ break;
17341
+ case "\xA9gen":
17342
+ case "gnre":
17343
+ case "com.apple.quicktime.genre":
17344
+ case "genre":
17345
+ {
17346
+ if (typeof data === "string") {
17347
+ this.metadataTags.genre ??= data;
17348
+ }
17349
+ }
17350
+ ;
17351
+ break;
17352
+ case "\xA9lyr":
17353
+ case "lyrics":
17354
+ {
17355
+ if (typeof data === "string") {
17356
+ this.metadataTags.lyrics ??= data;
17357
+ }
17358
+ }
17359
+ ;
17360
+ break;
17361
+ case "\xA9day":
17362
+ case "rldt":
17363
+ case "com.apple.quicktime.creationdate":
17364
+ case "date":
17365
+ {
17366
+ if (typeof data === "string") {
17367
+ const date = new Date(data);
17368
+ if (!Number.isNaN(date.getTime())) {
17369
+ this.metadataTags.date ??= date;
17370
+ }
17371
+ }
17372
+ }
17373
+ ;
17374
+ break;
17375
+ case "covr":
17376
+ case "com.apple.quicktime.artwork":
17377
+ {
17378
+ if (data instanceof RichImageData) {
17379
+ this.metadataTags.images ??= [];
17380
+ this.metadataTags.images.push({
17381
+ data: data.data,
17382
+ kind: "coverFront",
17383
+ mimeType: data.mimeType
17384
+ });
17385
+ } else if (data instanceof Uint8Array) {
17386
+ this.metadataTags.images ??= [];
17387
+ this.metadataTags.images.push({
17388
+ data,
17389
+ kind: "coverFront",
17390
+ mimeType: "image/*"
17391
+ });
17392
+ }
17393
+ }
17394
+ ;
17395
+ break;
17396
+ case "trkn":
17397
+ {
17398
+ if (data instanceof Uint8Array) {
17399
+ const view2 = toDataView(data);
17400
+ const trackNumber = view2.getUint16(2, false);
17401
+ const tracksTotal = view2.getUint16(4, false);
17402
+ if (trackNumber > 0) {
17403
+ this.metadataTags.trackNumber ??= trackNumber;
17404
+ }
17405
+ if (tracksTotal > 0) {
17406
+ this.metadataTags.tracksTotal ??= tracksTotal;
17407
+ }
17408
+ }
17409
+ }
17410
+ ;
17411
+ break;
17412
+ case "disc":
17413
+ case "disk":
17414
+ {
17415
+ if (data instanceof Uint8Array) {
17416
+ const view2 = toDataView(data);
17417
+ const discNumber = view2.getUint16(2, false);
17418
+ const discNumberMax = view2.getUint16(4, false);
17419
+ if (discNumber > 0) {
17420
+ this.metadataTags.discNumber ??= discNumber;
17421
+ }
17422
+ if (discNumberMax > 0) {
17423
+ this.metadataTags.discsTotal ??= discNumberMax;
17424
+ }
17425
+ }
17426
+ }
17427
+ ;
17428
+ break;
17429
+ }
17430
+ }
14804
17431
  }
14805
17432
  ;
14806
17433
  break;
@@ -15459,6 +18086,9 @@ ${cue.notes ?? ""}`;
15459
18086
  this.currentCluster = null;
15460
18087
  this.currentBlock = null;
15461
18088
  this.currentCueTime = null;
18089
+ this.currentTagTargetIsMovie = true;
18090
+ this.currentSimpleTagName = null;
18091
+ this.currentAttachedFile = null;
15462
18092
  this.isWebM = false;
15463
18093
  this.reader = input._reader;
15464
18094
  }
@@ -15482,6 +18112,23 @@ ${cue.notes ?? ""}`;
15482
18112
  codecStrings: codecStrings.filter(Boolean)
15483
18113
  });
15484
18114
  }
18115
+ async getMetadataTags() {
18116
+ await this.readMetadata();
18117
+ for (const segment of this.segments) {
18118
+ if (!segment.metadataTagsCollected) {
18119
+ if (this.reader.fileSize !== null) {
18120
+ await this.loadSegmentMetadata(segment);
18121
+ } else {
18122
+ }
18123
+ segment.metadataTagsCollected = true;
18124
+ }
18125
+ }
18126
+ let metadataTags = {};
18127
+ for (const segment of this.segments) {
18128
+ metadataTags = { ...metadataTags, ...segment.metadataTags };
18129
+ }
18130
+ return metadataTags;
18131
+ }
15485
18132
  readMetadata() {
15486
18133
  return this.readMetadataPromise ??= (async () => {
15487
18134
  let currentPos = 0;
@@ -15539,6 +18186,8 @@ ${cue.notes ?? ""}`;
15539
18186
  infoSeen: false,
15540
18187
  tracksSeen: false,
15541
18188
  cuesSeen: false,
18189
+ tagsSeen: false,
18190
+ attachmentsSeen: false,
15542
18191
  timestampScale: -1,
15543
18192
  timestampFactor: -1,
15544
18193
  duration: -1,
@@ -15549,7 +18198,9 @@ ${cue.notes ?? ""}`;
15549
18198
  elementEndPos: dataSize === null ? null : segmentDataStart + dataSize,
15550
18199
  clusterSeekStartPos: segmentDataStart,
15551
18200
  clusters: [],
15552
- clusterLookupMutex: new AsyncMutex()
18201
+ clusterLookupMutex: new AsyncMutex(),
18202
+ metadataTags: {},
18203
+ metadataTagsCollected: false
15553
18204
  };
15554
18205
  this.segments.push(this.currentSegment);
15555
18206
  let currentPos = segmentDataStart;
@@ -15585,6 +18236,18 @@ ${cue.notes ?? ""}`;
15585
18236
  if (slice2) {
15586
18237
  this.readContiguousElements(slice2);
15587
18238
  }
18239
+ } else if (id === 307544935 /* Tags */ || id === 423732329 /* Attachments */) {
18240
+ if (id === 307544935 /* Tags */) {
18241
+ this.currentSegment.tagsSeen = true;
18242
+ } else {
18243
+ this.currentSegment.attachmentsSeen = true;
18244
+ }
18245
+ assertDefinedSize(size);
18246
+ let slice2 = this.reader.requestSlice(dataStartPos, size);
18247
+ if (slice2 instanceof Promise) slice2 = await slice2;
18248
+ if (slice2) {
18249
+ this.readContiguousElements(slice2);
18250
+ }
15588
18251
  } else if (id === 524531317 /* Cluster */) {
15589
18252
  this.currentSegment.clusterSeekStartPos = elementStartPos;
15590
18253
  break;
@@ -15595,8 +18258,8 @@ ${cue.notes ?? ""}`;
15595
18258
  currentPos = dataStartPos + size;
15596
18259
  }
15597
18260
  }
18261
+ this.currentSegment.seekEntries.sort((a, b) => a.segmentPosition - b.segmentPosition);
15598
18262
  if (this.reader.fileSize !== null) {
15599
- this.currentSegment.seekEntries.sort((a, b) => a.segmentPosition - b.segmentPosition);
15600
18263
  for (const seekEntry of this.currentSegment.seekEntries) {
15601
18264
  const target = METADATA_ELEMENTS.find((x) => x.id === seekEntry.id);
15602
18265
  if (!target) {
@@ -15850,6 +18513,39 @@ ${cue.notes ?? ""}`;
15850
18513
  blockIndex--;
15851
18514
  }
15852
18515
  }
18516
+ async loadSegmentMetadata(segment) {
18517
+ for (const seekEntry of segment.seekEntries) {
18518
+ if (seekEntry.id === 307544935 /* Tags */ && !segment.tagsSeen) {
18519
+ } else if (seekEntry.id === 423732329 /* Attachments */ && !segment.attachmentsSeen) {
18520
+ } else {
18521
+ continue;
18522
+ }
18523
+ let slice = this.reader.requestSliceRange(
18524
+ segment.dataStartPos + seekEntry.segmentPosition,
18525
+ MIN_HEADER_SIZE,
18526
+ MAX_HEADER_SIZE
18527
+ );
18528
+ if (slice instanceof Promise) slice = await slice;
18529
+ if (!slice) continue;
18530
+ const header = readElementHeader(slice);
18531
+ if (!header || header.id !== seekEntry.id) continue;
18532
+ const { size } = header;
18533
+ assertDefinedSize(size);
18534
+ assert(!this.currentSegment);
18535
+ this.currentSegment = segment;
18536
+ let dataSlice = this.reader.requestSlice(slice.filePos, size);
18537
+ if (dataSlice instanceof Promise) dataSlice = await dataSlice;
18538
+ if (dataSlice) {
18539
+ this.readContiguousElements(dataSlice);
18540
+ }
18541
+ this.currentSegment = null;
18542
+ if (seekEntry.id === 307544935 /* Tags */) {
18543
+ segment.tagsSeen = true;
18544
+ } else if (seekEntry.id === 423732329 /* Attachments */) {
18545
+ segment.attachmentsSeen = true;
18546
+ }
18547
+ }
18548
+ }
15853
18549
  readContiguousElements(slice) {
15854
18550
  const startIndex = slice.filePos;
15855
18551
  while (slice.filePos - startIndex <= slice.length - MIN_HEADER_SIZE) {
@@ -16356,10 +19052,230 @@ ${cue.notes ?? ""}`;
16356
19052
  }
16357
19053
  ;
16358
19054
  break;
19055
+ case 29555 /* Tag */:
19056
+ {
19057
+ this.currentTagTargetIsMovie = true;
19058
+ this.readContiguousElements(slice.slice(dataStartPos, size));
19059
+ }
19060
+ ;
19061
+ break;
19062
+ case 25536 /* Targets */:
19063
+ {
19064
+ this.readContiguousElements(slice.slice(dataStartPos, size));
19065
+ }
19066
+ ;
19067
+ break;
19068
+ case 26826 /* TargetTypeValue */:
19069
+ {
19070
+ const targetTypeValue = readUnsignedInt(slice, size);
19071
+ if (targetTypeValue !== 50) {
19072
+ this.currentTagTargetIsMovie = false;
19073
+ }
19074
+ }
19075
+ ;
19076
+ break;
19077
+ case 25541 /* TagTrackUID */:
19078
+ case 25545 /* TagEditionUID */:
19079
+ case 25540 /* TagChapterUID */:
19080
+ case 25542 /* TagAttachmentUID */:
19081
+ {
19082
+ this.currentTagTargetIsMovie = false;
19083
+ }
19084
+ ;
19085
+ break;
19086
+ case 26568 /* SimpleTag */:
19087
+ {
19088
+ if (!this.currentTagTargetIsMovie) break;
19089
+ this.currentSimpleTagName = null;
19090
+ this.readContiguousElements(slice.slice(dataStartPos, size));
19091
+ }
19092
+ ;
19093
+ break;
19094
+ case 17827 /* TagName */:
19095
+ {
19096
+ this.currentSimpleTagName = readUnicodeString(slice, size);
19097
+ }
19098
+ ;
19099
+ break;
19100
+ case 17543 /* TagString */:
19101
+ {
19102
+ if (!this.currentSimpleTagName) break;
19103
+ const value = readUnicodeString(slice, size);
19104
+ this.processTagValue(this.currentSimpleTagName, value);
19105
+ }
19106
+ ;
19107
+ break;
19108
+ case 17541 /* TagBinary */:
19109
+ {
19110
+ if (!this.currentSimpleTagName) break;
19111
+ const value = readBytes(slice, size);
19112
+ this.processTagValue(this.currentSimpleTagName, value);
19113
+ }
19114
+ ;
19115
+ break;
19116
+ case 24999 /* AttachedFile */:
19117
+ {
19118
+ if (!this.currentSegment) break;
19119
+ this.currentAttachedFile = {
19120
+ fileName: null,
19121
+ fileMediaType: null,
19122
+ fileData: null,
19123
+ fileDescription: null
19124
+ };
19125
+ this.readContiguousElements(slice.slice(dataStartPos, size));
19126
+ if (this.currentAttachedFile.fileMediaType?.startsWith("image/") && this.currentAttachedFile.fileData) {
19127
+ const fileName = this.currentAttachedFile.fileName;
19128
+ let kind = "unknown";
19129
+ if (fileName) {
19130
+ const lowerName = fileName.toLowerCase();
19131
+ if (lowerName.startsWith("cover.")) {
19132
+ kind = "coverFront";
19133
+ } else if (lowerName.startsWith("back.")) {
19134
+ kind = "coverBack";
19135
+ }
19136
+ }
19137
+ this.currentSegment.metadataTags.images ??= [];
19138
+ this.currentSegment.metadataTags.images.push({
19139
+ data: this.currentAttachedFile.fileData,
19140
+ mimeType: this.currentAttachedFile.fileMediaType,
19141
+ kind,
19142
+ name: this.currentAttachedFile.fileName ?? void 0,
19143
+ description: this.currentAttachedFile.fileDescription ?? void 0
19144
+ });
19145
+ }
19146
+ this.currentAttachedFile = null;
19147
+ }
19148
+ ;
19149
+ break;
19150
+ case 18030 /* FileName */:
19151
+ {
19152
+ if (!this.currentAttachedFile) break;
19153
+ this.currentAttachedFile.fileName = readUnicodeString(slice, size);
19154
+ }
19155
+ ;
19156
+ break;
19157
+ case 18016 /* FileMediaType */:
19158
+ {
19159
+ if (!this.currentAttachedFile) break;
19160
+ this.currentAttachedFile.fileMediaType = readAsciiString(slice, size);
19161
+ }
19162
+ ;
19163
+ break;
19164
+ case 18012 /* FileData */:
19165
+ {
19166
+ if (!this.currentAttachedFile) break;
19167
+ this.currentAttachedFile.fileData = readBytes(slice, size);
19168
+ }
19169
+ ;
19170
+ break;
19171
+ case 18046 /* FileDescription */:
19172
+ {
19173
+ if (!this.currentAttachedFile) break;
19174
+ this.currentAttachedFile.fileDescription = readUnicodeString(slice, size);
19175
+ }
19176
+ ;
19177
+ break;
16359
19178
  }
16360
19179
  slice.filePos = dataStartPos + size;
16361
19180
  return true;
16362
19181
  }
19182
+ processTagValue(name, value) {
19183
+ if (!this.currentSegment?.metadataTags) return;
19184
+ const metadataTags = this.currentSegment.metadataTags;
19185
+ metadataTags.raw ??= {};
19186
+ metadataTags.raw[name] ??= value;
19187
+ if (typeof value === "string") {
19188
+ switch (name.toLowerCase()) {
19189
+ case "title":
19190
+ {
19191
+ metadataTags.title ??= value;
19192
+ }
19193
+ ;
19194
+ break;
19195
+ case "description":
19196
+ {
19197
+ metadataTags.description ??= value;
19198
+ }
19199
+ ;
19200
+ break;
19201
+ case "artist":
19202
+ {
19203
+ metadataTags.artist ??= value;
19204
+ }
19205
+ ;
19206
+ break;
19207
+ case "album":
19208
+ {
19209
+ metadataTags.album ??= value;
19210
+ }
19211
+ ;
19212
+ break;
19213
+ case "album_artist":
19214
+ {
19215
+ metadataTags.albumArtist ??= value;
19216
+ }
19217
+ ;
19218
+ break;
19219
+ case "genre":
19220
+ {
19221
+ metadataTags.genre ??= value;
19222
+ }
19223
+ ;
19224
+ break;
19225
+ case "comment":
19226
+ {
19227
+ metadataTags.comment ??= value;
19228
+ }
19229
+ ;
19230
+ break;
19231
+ case "lyrics":
19232
+ {
19233
+ metadataTags.lyrics ??= value;
19234
+ }
19235
+ ;
19236
+ break;
19237
+ case "date":
19238
+ {
19239
+ const date = new Date(value);
19240
+ if (!Number.isNaN(date.getTime())) {
19241
+ metadataTags.date ??= date;
19242
+ }
19243
+ }
19244
+ ;
19245
+ break;
19246
+ case "track_number":
19247
+ case "part_number":
19248
+ {
19249
+ const parts = value.split("/");
19250
+ const trackNum = Number.parseInt(parts[0], 10);
19251
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
19252
+ if (Number.isInteger(trackNum) && trackNum > 0) {
19253
+ metadataTags.trackNumber ??= trackNum;
19254
+ }
19255
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
19256
+ metadataTags.tracksTotal ??= tracksTotal;
19257
+ }
19258
+ }
19259
+ ;
19260
+ break;
19261
+ case "disc_number":
19262
+ case "disc":
19263
+ {
19264
+ const discParts = value.split("/");
19265
+ const discNum = Number.parseInt(discParts[0], 10);
19266
+ const discsTotal = discParts[1] && Number.parseInt(discParts[1], 10);
19267
+ if (Number.isInteger(discNum) && discNum > 0) {
19268
+ metadataTags.discNumber ??= discNum;
19269
+ }
19270
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
19271
+ metadataTags.discsTotal ??= discsTotal;
19272
+ }
19273
+ }
19274
+ ;
19275
+ break;
19276
+ }
19277
+ }
19278
+ }
16363
19279
  };
16364
19280
  var MatroskaTrackBacking = class {
16365
19281
  constructor(internalTrack) {
@@ -16881,43 +19797,6 @@ ${cue.notes ?? ""}`;
16881
19797
  return result;
16882
19798
  };
16883
19799
 
16884
- // src/mp3/mp3-reader.ts
16885
- var readId3 = (slice) => {
16886
- const tag = readAscii(slice, 3);
16887
- if (tag !== "ID3") {
16888
- slice.skip(-3);
16889
- return null;
16890
- }
16891
- slice.skip(3);
16892
- const size = decodeSynchsafe(readU32Be(slice));
16893
- return { size };
16894
- };
16895
- var readNextFrameHeader = async (reader, startPos, until) => {
16896
- let currentPos = startPos;
16897
- while (until === null || currentPos < until) {
16898
- let slice = reader.requestSlice(currentPos, FRAME_HEADER_SIZE);
16899
- if (slice instanceof Promise) slice = await slice;
16900
- if (!slice) break;
16901
- const word = readU32Be(slice);
16902
- const result = readFrameHeader(word, reader.fileSize !== null ? reader.fileSize - currentPos : null);
16903
- if (result.header) {
16904
- return { header: result.header, startPos: currentPos };
16905
- }
16906
- currentPos += result.bytesAdvanced;
16907
- }
16908
- return null;
16909
- };
16910
- var decodeSynchsafe = (synchsafed) => {
16911
- let mask = 2130706432;
16912
- let unsynchsafed = 0;
16913
- while (mask !== 0) {
16914
- unsynchsafed >>= 1;
16915
- unsynchsafed |= synchsafed & mask;
16916
- mask >>= 8;
16917
- }
16918
- return unsynchsafed;
16919
- };
16920
-
16921
19800
  // src/mp3/mp3-demuxer.ts
16922
19801
  var Mp3Demuxer = class extends Demuxer {
16923
19802
  constructor(input) {
@@ -16926,6 +19805,7 @@ ${cue.notes ?? ""}`;
16926
19805
  this.firstFrameHeader = null;
16927
19806
  this.loadedSamples = [];
16928
19807
  // All samples from the start of the file to lastLoadedPos
19808
+ this.metadataTags = null;
16929
19809
  this.tracks = [];
16930
19810
  this.readingMutex = new AsyncMutex();
16931
19811
  this.lastSampleLoaded = false;
@@ -16938,21 +19818,26 @@ ${cue.notes ?? ""}`;
16938
19818
  while (!this.firstFrameHeader && !this.lastSampleLoaded) {
16939
19819
  await this.advanceReader();
16940
19820
  }
16941
- assert(this.firstFrameHeader);
19821
+ if (!this.firstFrameHeader) {
19822
+ throw new Error("No valid MP3 frame found.");
19823
+ }
16942
19824
  this.tracks = [new InputAudioTrack(new Mp3AudioTrackBacking(this))];
16943
19825
  })();
16944
19826
  }
16945
19827
  async advanceReader() {
16946
19828
  if (this.lastLoadedPos === 0) {
16947
- let slice2 = this.reader.requestSlice(0, 10);
16948
- if (slice2 instanceof Promise) slice2 = await slice2;
16949
- if (!slice2) {
16950
- this.lastSampleLoaded = true;
16951
- return;
16952
- }
16953
- const id3Tag = readId3(slice2);
16954
- if (id3Tag) {
16955
- this.lastLoadedPos += 10 + id3Tag.size;
19829
+ while (true) {
19830
+ let slice2 = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
19831
+ if (slice2 instanceof Promise) slice2 = await slice2;
19832
+ if (!slice2) {
19833
+ this.lastSampleLoaded = true;
19834
+ return;
19835
+ }
19836
+ const id3V2Header = readId3V2Header(slice2);
19837
+ if (!id3V2Header) {
19838
+ break;
19839
+ }
19840
+ this.lastLoadedPos = slice2.filePos + id3V2Header.size;
16956
19841
  }
16957
19842
  }
16958
19843
  const result = await readNextFrameHeader(this.reader, this.lastLoadedPos, this.reader.fileSize);
@@ -16999,6 +19884,45 @@ ${cue.notes ?? ""}`;
16999
19884
  assert(track);
17000
19885
  return track.computeDuration();
17001
19886
  }
19887
+ async getMetadataTags() {
19888
+ const release = await this.readingMutex.acquire();
19889
+ try {
19890
+ await this.readMetadata();
19891
+ if (this.metadataTags) {
19892
+ return this.metadataTags;
19893
+ }
19894
+ this.metadataTags = {};
19895
+ let currentPos = 0;
19896
+ let id3V2HeaderFound = false;
19897
+ while (true) {
19898
+ let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
19899
+ if (headerSlice instanceof Promise) headerSlice = await headerSlice;
19900
+ if (!headerSlice) break;
19901
+ const id3V2Header = readId3V2Header(headerSlice);
19902
+ if (!id3V2Header) {
19903
+ break;
19904
+ }
19905
+ id3V2HeaderFound = true;
19906
+ let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
19907
+ if (contentSlice instanceof Promise) contentSlice = await contentSlice;
19908
+ if (!contentSlice) break;
19909
+ parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
19910
+ currentPos = headerSlice.filePos + id3V2Header.size;
19911
+ }
19912
+ if (!id3V2HeaderFound && this.reader.fileSize !== null && this.reader.fileSize >= ID3_V1_TAG_SIZE) {
19913
+ let slice = this.reader.requestSlice(this.reader.fileSize - ID3_V1_TAG_SIZE, ID3_V1_TAG_SIZE);
19914
+ if (slice instanceof Promise) slice = await slice;
19915
+ assert(slice);
19916
+ const tag = readAscii(slice, 3);
19917
+ if (tag === "TAG") {
19918
+ parseId3V1Tag(slice, this.metadataTags);
19919
+ }
19920
+ }
19921
+ return this.metadataTags;
19922
+ } finally {
19923
+ release();
19924
+ }
19925
+ }
17002
19926
  };
17003
19927
  var Mp3AudioTrackBacking = class {
17004
19928
  constructor(demuxer) {
@@ -17136,6 +20060,7 @@ ${cue.notes ?? ""}`;
17136
20060
  this.metadataPromise = null;
17137
20061
  this.bitstreams = [];
17138
20062
  this.tracks = [];
20063
+ this.metadataTags = {};
17139
20064
  this.reader = input._reader;
17140
20065
  }
17141
20066
  async readMetadata() {
@@ -17256,6 +20181,7 @@ ${cue.notes ?? ""}`;
17256
20181
  ],
17257
20182
  modeBlockflags: parseModesFromVorbisSetupPacket(thirdPacket.data).modeBlockflags
17258
20183
  };
20184
+ this.readVorbisComments(secondPacket.data.subarray(7));
17259
20185
  }
17260
20186
  async readOpusMetadata(firstPacket, bitstream) {
17261
20187
  const nextPacketPosition = await this.findNextPacketStart(firstPacket);
@@ -17278,6 +20204,172 @@ ${cue.notes ?? ""}`;
17278
20204
  bitstream.codecInfo.opusInfo = {
17279
20205
  preSkip: header.preSkip
17280
20206
  };
20207
+ this.readVorbisComments(secondPacket.data.subarray(8));
20208
+ }
20209
+ readVorbisComments(bytes2) {
20210
+ const commentView = toDataView(bytes2);
20211
+ let commentPos = 0;
20212
+ const vendorStringLength = commentView.getUint32(commentPos, true);
20213
+ commentPos += 4;
20214
+ const vendorString = textDecoder.decode(
20215
+ bytes2.subarray(commentPos, commentPos + vendorStringLength)
20216
+ );
20217
+ commentPos += vendorStringLength;
20218
+ if (vendorStringLength > 0) {
20219
+ this.metadataTags.raw ??= {};
20220
+ this.metadataTags.raw["vendor"] ??= vendorString;
20221
+ }
20222
+ const listLength = commentView.getUint32(commentPos, true);
20223
+ commentPos += 4;
20224
+ for (let i = 0; i < listLength; i++) {
20225
+ const stringLength = commentView.getUint32(commentPos, true);
20226
+ commentPos += 4;
20227
+ const string = textDecoder.decode(
20228
+ bytes2.subarray(commentPos, commentPos + stringLength)
20229
+ );
20230
+ commentPos += stringLength;
20231
+ const separatorIndex = string.indexOf("=");
20232
+ if (separatorIndex === -1) {
20233
+ continue;
20234
+ }
20235
+ const key = string.slice(0, separatorIndex).toUpperCase();
20236
+ const value = string.slice(separatorIndex + 1);
20237
+ this.metadataTags.raw ??= {};
20238
+ this.metadataTags.raw[key] ??= value;
20239
+ switch (key) {
20240
+ case "TITLE":
20241
+ {
20242
+ this.metadataTags.title ??= value;
20243
+ }
20244
+ ;
20245
+ break;
20246
+ case "DESCRIPTION":
20247
+ {
20248
+ this.metadataTags.description ??= value;
20249
+ }
20250
+ ;
20251
+ break;
20252
+ case "ARTIST":
20253
+ {
20254
+ this.metadataTags.artist ??= value;
20255
+ }
20256
+ ;
20257
+ break;
20258
+ case "ALBUM":
20259
+ {
20260
+ this.metadataTags.album ??= value;
20261
+ }
20262
+ ;
20263
+ break;
20264
+ case "ALBUMARTIST":
20265
+ {
20266
+ this.metadataTags.albumArtist ??= value;
20267
+ }
20268
+ ;
20269
+ break;
20270
+ case "COMMENT":
20271
+ {
20272
+ this.metadataTags.comment ??= value;
20273
+ }
20274
+ ;
20275
+ break;
20276
+ case "LYRICS":
20277
+ {
20278
+ this.metadataTags.lyrics ??= value;
20279
+ }
20280
+ ;
20281
+ break;
20282
+ case "TRACKNUMBER":
20283
+ {
20284
+ const parts = value.split("/");
20285
+ const trackNum = Number.parseInt(parts[0], 10);
20286
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
20287
+ if (Number.isInteger(trackNum) && trackNum > 0) {
20288
+ this.metadataTags.trackNumber ??= trackNum;
20289
+ }
20290
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
20291
+ this.metadataTags.tracksTotal ??= tracksTotal;
20292
+ }
20293
+ }
20294
+ ;
20295
+ break;
20296
+ case "TRACKTOTAL":
20297
+ {
20298
+ const tracksTotal = Number.parseInt(value, 10);
20299
+ if (Number.isInteger(tracksTotal) && tracksTotal > 0) {
20300
+ this.metadataTags.tracksTotal ??= tracksTotal;
20301
+ }
20302
+ }
20303
+ ;
20304
+ break;
20305
+ case "DISCNUMBER":
20306
+ {
20307
+ const parts = value.split("/");
20308
+ const discNum = Number.parseInt(parts[0], 10);
20309
+ const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
20310
+ if (Number.isInteger(discNum) && discNum > 0) {
20311
+ this.metadataTags.discNumber ??= discNum;
20312
+ }
20313
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
20314
+ this.metadataTags.discsTotal ??= discsTotal;
20315
+ }
20316
+ }
20317
+ ;
20318
+ break;
20319
+ case "DISCTOTAL":
20320
+ {
20321
+ const discsTotal = Number.parseInt(value, 10);
20322
+ if (Number.isInteger(discsTotal) && discsTotal > 0) {
20323
+ this.metadataTags.discsTotal ??= discsTotal;
20324
+ }
20325
+ }
20326
+ ;
20327
+ break;
20328
+ case "DATE":
20329
+ {
20330
+ const date = new Date(value);
20331
+ if (!Number.isNaN(date.getTime())) {
20332
+ this.metadataTags.date ??= date;
20333
+ }
20334
+ }
20335
+ ;
20336
+ break;
20337
+ case "GENRE":
20338
+ {
20339
+ this.metadataTags.genre ??= value;
20340
+ }
20341
+ ;
20342
+ break;
20343
+ case "METADATA_BLOCK_PICTURE":
20344
+ {
20345
+ const decoded = base64ToBytes(value);
20346
+ const view2 = toDataView(decoded);
20347
+ const pictureType = view2.getUint32(0, false);
20348
+ const mediaTypeLength = view2.getUint32(4, false);
20349
+ const mediaType = String.fromCharCode(...decoded.subarray(8, 8 + mediaTypeLength));
20350
+ const descriptionLength = view2.getUint32(8 + mediaTypeLength, false);
20351
+ const description = textDecoder.decode(decoded.subarray(
20352
+ 12 + mediaTypeLength,
20353
+ 12 + mediaTypeLength + descriptionLength
20354
+ ));
20355
+ const dataLength = view2.getUint32(mediaTypeLength + descriptionLength + 28);
20356
+ const data = decoded.subarray(
20357
+ mediaTypeLength + descriptionLength + 32,
20358
+ mediaTypeLength + descriptionLength + 32 + dataLength
20359
+ );
20360
+ this.metadataTags.images ??= [];
20361
+ this.metadataTags.images.push({
20362
+ data,
20363
+ mimeType: mediaType,
20364
+ kind: pictureType === 3 ? "coverFront" : pictureType === 4 ? "coverBack" : "unknown",
20365
+ name: void 0,
20366
+ description: description || void 0
20367
+ });
20368
+ }
20369
+ ;
20370
+ break;
20371
+ }
20372
+ }
17281
20373
  }
17282
20374
  async readPacket(startPage, startSegmentIndex) {
17283
20375
  assert(startSegmentIndex < startPage.lacingValues.length);
@@ -17384,6 +20476,10 @@ ${cue.notes ?? ""}`;
17384
20476
  const trackDurations = await Promise.all(tracks.map((x) => x.computeDuration()));
17385
20477
  return Math.max(0, ...trackDurations);
17386
20478
  }
20479
+ async getMetadataTags() {
20480
+ await this.readMetadata();
20481
+ return this.metadataTags;
20482
+ }
17387
20483
  };
17388
20484
  var OggAudioTrackBacking = class {
17389
20485
  constructor(bitstream, demuxer) {
@@ -17973,6 +21069,9 @@ ${cue.notes ?? ""}`;
17973
21069
  assert(track);
17974
21070
  return track.computeDuration();
17975
21071
  }
21072
+ async getMetadataTags() {
21073
+ return {};
21074
+ }
17976
21075
  };
17977
21076
  var AdtsAudioTrackBacking = class {
17978
21077
  constructor(demuxer) {
@@ -18274,15 +21373,23 @@ ${cue.notes ?? ""}`;
18274
21373
  if (slice instanceof Promise) slice = await slice;
18275
21374
  if (!slice) return false;
18276
21375
  let currentPos = 0;
18277
- const id3Tag = readId3(slice);
18278
- if (id3Tag) {
18279
- currentPos = slice.filePos + id3Tag.size;
21376
+ let id3V2HeaderFound = false;
21377
+ while (true) {
21378
+ let slice2 = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
21379
+ if (slice2 instanceof Promise) slice2 = await slice2;
21380
+ if (!slice2) break;
21381
+ const id3V2Header = readId3V2Header(slice2);
21382
+ if (!id3V2Header) {
21383
+ break;
21384
+ }
21385
+ id3V2HeaderFound = true;
21386
+ currentPos = slice2.filePos + id3V2Header.size;
18280
21387
  }
18281
21388
  const firstResult = await readNextFrameHeader(input._reader, currentPos, currentPos + 4096);
18282
21389
  if (!firstResult) {
18283
21390
  return false;
18284
21391
  }
18285
- if (id3Tag) {
21392
+ if (id3V2HeaderFound) {
18286
21393
  return true;
18287
21394
  }
18288
21395
  currentPos = firstResult.startPos += firstResult.header.totalSize;
@@ -18485,6 +21592,11 @@ ${cue.notes ?? ""}`;
18485
21592
  const demuxer = await this._getDemuxer();
18486
21593
  return demuxer.getMimeType();
18487
21594
  }
21595
+ /** Returns descriptive metadata tags about the media file, such as title, author, date, or cover art. */
21596
+ async getMetadataTags() {
21597
+ const demuxer = await this._getDemuxer();
21598
+ return demuxer.getMetadataTags();
21599
+ }
18488
21600
  };
18489
21601
 
18490
21602
  // src/conversion.ts
@@ -18626,6 +21738,9 @@ ${cue.notes ?? ""}`;
18626
21738
  if (options.trim?.start !== void 0 && options.trim.end !== void 0 && options.trim.start >= options.trim.end) {
18627
21739
  throw new TypeError("options.trim.start must be less than options.trim.end.");
18628
21740
  }
21741
+ if (options.tags !== void 0 && typeof options.tags !== "function") {
21742
+ throw new TypeError("options.tags, when provided, must be a function.");
21743
+ }
18629
21744
  this._options = options;
18630
21745
  this.input = options.input;
18631
21746
  this.output = options.output;
@@ -18703,6 +21818,21 @@ ${cue.notes ?? ""}`;
18703
21818
  if (unintentionallyDiscardedTracks.length > 0) {
18704
21819
  console.warn("Some tracks had to be discarded from the conversion:", unintentionallyDiscardedTracks);
18705
21820
  }
21821
+ const inputTags = await this.input.getMetadataTags();
21822
+ let outputTags;
21823
+ if (this._options.tags) {
21824
+ const result = await this._options.tags(inputTags);
21825
+ validateMetadataTags(result);
21826
+ outputTags = result;
21827
+ } else {
21828
+ outputTags = inputTags;
21829
+ }
21830
+ const inputAndOutputFormatMatch = (await this.input.getFormat()).mimeType === this.output.format.mimeType;
21831
+ const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
21832
+ if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
21833
+ delete outputTags.raw;
21834
+ }
21835
+ this.output.setMetadataTags(outputTags);
18706
21836
  }
18707
21837
  /** Executes the conversion process. Resolves once conversion is complete. */
18708
21838
  async execute() {
@@ -18788,7 +21918,7 @@ ${cue.notes ?? ""}`;
18788
21918
  await this._started;
18789
21919
  const sink = new EncodedPacketSink(track);
18790
21920
  const decoderConfig = await track.getDecoderConfig();
18791
- const meta = { decoderConfig: decoderConfig ?? void 0 };
21921
+ const meta2 = { decoderConfig: decoderConfig ?? void 0 };
18792
21922
  const endPacket = Number.isFinite(this._endTimestamp) ? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? void 0 : void 0;
18793
21923
  for await (const packet of sink.packets(void 0, endPacket, { verifyKeyPackets: true })) {
18794
21924
  if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
@@ -18797,7 +21927,7 @@ ${cue.notes ?? ""}`;
18797
21927
  if (this._canceled) {
18798
21928
  return;
18799
21929
  }
18800
- await source.add(packet, meta);
21930
+ await source.add(packet, meta2);
18801
21931
  this._reportProgress(track.id, packet.timestamp + packet.duration);
18802
21932
  }
18803
21933
  source.close();
@@ -19027,7 +22157,7 @@ ${cue.notes ?? ""}`;
19027
22157
  await this._started;
19028
22158
  const sink = new EncodedPacketSink(track);
19029
22159
  const decoderConfig = await track.getDecoderConfig();
19030
- const meta = { decoderConfig: decoderConfig ?? void 0 };
22160
+ const meta2 = { decoderConfig: decoderConfig ?? void 0 };
19031
22161
  const endPacket = Number.isFinite(this._endTimestamp) ? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? void 0 : void 0;
19032
22162
  for await (const packet of sink.packets(void 0, endPacket)) {
19033
22163
  if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
@@ -19036,7 +22166,7 @@ ${cue.notes ?? ""}`;
19036
22166
  if (this._canceled) {
19037
22167
  return;
19038
22168
  }
19039
- await source.add(packet, meta);
22169
+ await source.add(packet, meta2);
19040
22170
  this._reportProgress(track.id, packet.timestamp + packet.duration);
19041
22171
  }
19042
22172
  source.close();