mediabunny 1.45.3 → 1.45.4

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 (37) hide show
  1. package/dist/bundles/mediabunny.cjs +84 -50
  2. package/dist/bundles/mediabunny.min.cjs +11 -11
  3. package/dist/bundles/mediabunny.min.mjs +11 -11
  4. package/dist/bundles/mediabunny.mjs +84 -50
  5. package/dist/bundles/mediabunny.node.cjs +84 -50
  6. package/dist/mediabunny.d.ts +59 -36
  7. package/dist/modules/shared/mp3-misc.d.ts +2 -1
  8. package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
  9. package/dist/modules/shared/mp3-misc.js +5 -2
  10. package/dist/modules/src/id3.js +2 -2
  11. package/dist/modules/src/index.d.ts +1 -1
  12. package/dist/modules/src/index.d.ts.map +1 -1
  13. package/dist/modules/src/index.js +3 -1
  14. package/dist/modules/src/input-format.d.ts.map +1 -1
  15. package/dist/modules/src/input-format.js +2 -2
  16. package/dist/modules/src/input.d.ts.map +1 -1
  17. package/dist/modules/src/input.js +0 -12
  18. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  19. package/dist/modules/src/mp3/mp3-demuxer.js +4 -8
  20. package/dist/modules/src/mp3/mp3-reader.d.ts +1 -1
  21. package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
  22. package/dist/modules/src/mp3/mp3-reader.js +13 -6
  23. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts.map +1 -1
  24. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +2 -2
  25. package/dist/modules/src/source.d.ts +27 -6
  26. package/dist/modules/src/source.d.ts.map +1 -1
  27. package/dist/modules/src/source.js +64 -19
  28. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/id3.ts +2 -2
  31. package/src/index.ts +4 -0
  32. package/src/input-format.ts +6 -2
  33. package/src/input.ts +0 -14
  34. package/src/mp3/mp3-demuxer.ts +9 -10
  35. package/src/mp3/mp3-reader.ts +21 -6
  36. package/src/mpeg-ts/mpeg-ts-demuxer.ts +6 -2
  37. package/src/source.ts +93 -24
@@ -120,6 +120,7 @@ var Mediabunny = (() => {
120
120
  CustomAudioDecoder: () => CustomAudioDecoder,
121
121
  CustomAudioEncoder: () => CustomAudioEncoder,
122
122
  CustomPathedSource: () => CustomPathedSource,
123
+ CustomSource: () => CustomSource,
123
124
  CustomVideoDecoder: () => CustomVideoDecoder,
124
125
  CustomVideoEncoder: () => CustomVideoEncoder,
125
126
  EncodedAudioPacketSource: () => EncodedAudioPacketSource,
@@ -2283,7 +2284,7 @@ var Mediabunny = (() => {
2283
2284
  };
2284
2285
 
2285
2286
  // shared/mp3-misc.ts
2286
- var FRAME_HEADER_SIZE = 4;
2287
+ var MP3_FRAME_HEADER_SIZE = 4;
2287
2288
  var SAMPLING_RATES = [44100, 48e3, 32e3];
2288
2289
  var KILOBIT_RATES = [
2289
2290
  // lowSamplingFrequency === 0
@@ -2551,6 +2552,9 @@ var Mediabunny = (() => {
2551
2552
  }
2552
2553
  return unsynchsafed;
2553
2554
  };
2555
+ var getMp3ChannelCount = (channel) => {
2556
+ return channel === 3 ? 1 : 2;
2557
+ };
2554
2558
 
2555
2559
  // shared/ac3-misc.ts
2556
2560
  var AC3_SAMPLE_RATES = [48e3, 44100, 32e3];
@@ -10880,20 +10884,22 @@ var Mediabunny = (() => {
10880
10884
  };
10881
10885
 
10882
10886
  // src/mp3/mp3-reader.ts
10883
- var readNextMp3FrameHeader = async (reader, startPos, until) => {
10887
+ var readNextMp3FrameHeader = async (reader, startPos, until, ref = null) => {
10884
10888
  const CHUNK_SIZE = 2 ** 16;
10885
10889
  let currentPos = startPos;
10886
10890
  while (until === null || currentPos < until) {
10887
10891
  const maxLength = until !== null ? Math.min(CHUNK_SIZE, until - currentPos) : CHUNK_SIZE;
10888
- let slice = reader.requestSliceRange(currentPos, FRAME_HEADER_SIZE, maxLength);
10892
+ let slice = reader.requestSliceRange(currentPos, MP3_FRAME_HEADER_SIZE, maxLength);
10889
10893
  if (slice instanceof Promise) slice = await slice;
10890
- if (!slice || slice.length < FRAME_HEADER_SIZE) break;
10891
- while (slice.remainingLength >= FRAME_HEADER_SIZE) {
10894
+ if (!slice || slice.length < MP3_FRAME_HEADER_SIZE) break;
10895
+ while (slice.remainingLength >= MP3_FRAME_HEADER_SIZE) {
10892
10896
  const posBeforeRead = slice.filePos;
10893
10897
  const word = readU32Be(slice);
10894
10898
  const remainingBytes = reader.fileSize !== null ? reader.fileSize - currentPos : null;
10895
10899
  const result = readMp3FrameHeader(word, remainingBytes);
10896
- if (result.header) {
10900
+ if (result.header && (!ref || // This condition helps us recover malformed streams
10901
+ // https://stackoverflow.com/a/20884944
10902
+ result.header.sampleRate === ref.sampleRate && result.header.mpegVersionId === ref.mpegVersionId && result.header.layer === ref.layer && getMp3ChannelCount(result.header.channel) === getMp3ChannelCount(ref.channel))) {
10897
10903
  return { header: result.header, startPos: currentPos };
10898
10904
  }
10899
10905
  slice.filePos = posBeforeRead + result.bytesAdvanced;
@@ -10948,7 +10954,12 @@ var Mediabunny = (() => {
10948
10954
  this.lastLoadedPos = slice2.filePos + id3V2Header.size;
10949
10955
  }
10950
10956
  }
10951
- const result = await readNextMp3FrameHeader(this.reader, this.lastLoadedPos, this.reader.fileSize);
10957
+ const result = await readNextMp3FrameHeader(
10958
+ this.reader,
10959
+ this.lastLoadedPos,
10960
+ this.reader.fileSize,
10961
+ this.firstFrameHeader
10962
+ );
10952
10963
  if (!result) {
10953
10964
  this.lastSampleLoaded = true;
10954
10965
  return;
@@ -10982,11 +10993,6 @@ var Mediabunny = (() => {
10982
10993
  this.firstFrameHeader = header;
10983
10994
  this.firstFrameHeaderPos = result.startPos;
10984
10995
  }
10985
- if (header.sampleRate !== this.firstFrameHeader.sampleRate) {
10986
- console.warn(
10987
- `MP3 changed sample rate mid-file: ${this.firstFrameHeader.sampleRate} Hz to ${header.sampleRate} Hz. Might be a bug, so please report this file.`
10988
- );
10989
- }
10990
10996
  const sampleDuration = header.audioSamplesInFrame / this.firstFrameHeader.sampleRate;
10991
10997
  const sample = {
10992
10998
  timestamp: this.nextTimestampInSamples / this.firstFrameHeader.sampleRate,
@@ -11113,7 +11119,7 @@ var Mediabunny = (() => {
11113
11119
  }
11114
11120
  getNumberOfChannels() {
11115
11121
  assert(this.demuxer.firstFrameHeader);
11116
- return this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2;
11122
+ return getMp3ChannelCount(this.demuxer.firstFrameHeader.channel);
11117
11123
  }
11118
11124
  getSampleRate() {
11119
11125
  assert(this.demuxer.firstFrameHeader);
@@ -11128,7 +11134,7 @@ var Mediabunny = (() => {
11128
11134
  assert(this.demuxer.firstFrameHeader);
11129
11135
  return {
11130
11136
  codec: "mp3",
11131
- numberOfChannels: this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2,
11137
+ numberOfChannels: getMp3ChannelCount(this.demuxer.firstFrameHeader.channel),
11132
11138
  sampleRate: this.demuxer.firstFrameHeader.sampleRate
11133
11139
  };
11134
11140
  }
@@ -13850,7 +13856,7 @@ var Mediabunny = (() => {
13850
13856
  "Invalid MP3 audio stream; could not read frame header from first packet."
13851
13857
  );
13852
13858
  }
13853
- elementaryStream.info.numberOfChannels = result.header.channel === 3 ? 1 : 2;
13859
+ elementaryStream.info.numberOfChannels = getMp3ChannelCount(result.header.channel);
13854
13860
  elementaryStream.info.sampleRate = result.header.sampleRate;
13855
13861
  } else if (elementaryStream.info.codec === "ac3") {
13856
13862
  const frameInfo = parseAc3SyncFrame(context.suppliedPacket.data);
@@ -14914,12 +14920,12 @@ var Mediabunny = (() => {
14914
14920
  }
14915
14921
  this.skip(-1);
14916
14922
  const possibleHeaderStartPos = this.currentPos;
14917
- let remaining2 = this.ensureBuffered(FRAME_HEADER_SIZE);
14923
+ let remaining2 = this.ensureBuffered(MP3_FRAME_HEADER_SIZE);
14918
14924
  if (remaining2 instanceof Promise) remaining2 = await remaining2;
14919
- if (remaining2 < FRAME_HEADER_SIZE) {
14925
+ if (remaining2 < MP3_FRAME_HEADER_SIZE) {
14920
14926
  return;
14921
14927
  }
14922
- const headerBytes = this.readBytes(FRAME_HEADER_SIZE);
14928
+ const headerBytes = this.readBytes(MP3_FRAME_HEADER_SIZE);
14923
14929
  const word = toDataView(headerBytes).getUint32(0);
14924
14930
  const result = readMp3FrameHeader(word, null);
14925
14931
  if (result.header) {
@@ -15562,9 +15568,15 @@ var Mediabunny = (() => {
15562
15568
  var node = typeof nodeAlias !== "undefined" ? nodeAlias : void 0;
15563
15569
  var DEFAULT_MIN_READ_POSITION = 0;
15564
15570
  var DEFAULT_MAX_READ_POSITION = Infinity;
15571
+ var sourceFinalizationRegistry = null;
15572
+ if (typeof FinalizationRegistry !== "undefined") {
15573
+ sourceFinalizationRegistry = new FinalizationRegistry((cleanup) => {
15574
+ cleanup();
15575
+ });
15576
+ }
15565
15577
  var Source = class extends EventEmitter {
15566
15578
  constructor() {
15567
- super(...arguments);
15579
+ super();
15568
15580
  /** @internal */
15569
15581
  this._disposed = false;
15570
15582
  /** @internal */
@@ -15574,6 +15586,13 @@ var Mediabunny = (() => {
15574
15586
  * @internal
15575
15587
  */
15576
15588
  this._usedForHls = false;
15589
+ /**
15590
+ * FinalizationRegistry for rogue refs to this source that didn't get freed. It lives on the Source itself so that
15591
+ * in case the Source transitively points back to itself and forms a cycle (for example through a custom
15592
+ * CustomSource callback) that we're not leaking memory.
15593
+ * @internal
15594
+ */
15595
+ this._refFinalizationRegistry = null;
15577
15596
  /** @internal */
15578
15597
  this._sizePromise = null;
15579
15598
  /**
@@ -15582,6 +15601,11 @@ var Mediabunny = (() => {
15582
15601
  * @deprecated Use `source.on('read', ({ start, end }) => ...)` instead.
15583
15602
  */
15584
15603
  this.onread = null;
15604
+ if (typeof FinalizationRegistry !== "undefined") {
15605
+ this._refFinalizationRegistry = new FinalizationRegistry((source) => {
15606
+ source._decrementRefCount();
15607
+ });
15608
+ }
15585
15609
  }
15586
15610
  /**
15587
15611
  * Resolves with the total size of the file in bytes. This function is memoized, meaning only the first call
@@ -15647,6 +15671,18 @@ var Mediabunny = (() => {
15647
15671
  ref() {
15648
15672
  return new SourceRef(this);
15649
15673
  }
15674
+ /** @internal */
15675
+ _incrementRefCount() {
15676
+ this._refCount++;
15677
+ }
15678
+ /** @internal */
15679
+ _decrementRefCount() {
15680
+ this._refCount--;
15681
+ if (this._refCount === 0) {
15682
+ this._dispose();
15683
+ this._disposed = true;
15684
+ }
15685
+ }
15650
15686
  };
15651
15687
  var SourceRef = class {
15652
15688
  /** @internal */
@@ -15656,7 +15692,8 @@ var Mediabunny = (() => {
15656
15692
  if (source._disposed) {
15657
15693
  throw new Error("Cannot ref a disposed source.");
15658
15694
  }
15659
- source._refCount++;
15695
+ source._incrementRefCount();
15696
+ source._refFinalizationRegistry?.register(this, source, this);
15660
15697
  this._source = source;
15661
15698
  }
15662
15699
  /** The {@link Source} this ref references. Accessing this field throws an error after having freed the ref. */
@@ -15680,11 +15717,8 @@ var Mediabunny = (() => {
15680
15717
  }
15681
15718
  const source = this.source;
15682
15719
  assert(source._refCount > 0);
15683
- source._refCount--;
15684
- if (source._refCount === 0) {
15685
- source._dispose();
15686
- source._disposed = true;
15687
- }
15720
+ source._decrementRefCount();
15721
+ source._refFinalizationRegistry?.unregister(this);
15688
15722
  this._freed = true;
15689
15723
  this._source = null;
15690
15724
  }
@@ -16110,10 +16144,14 @@ var Mediabunny = (() => {
16110
16144
  super(filePath, (request) => new _FilePathSource(request.path, options));
16111
16145
  /** @internal */
16112
16146
  this._fileHandle = null;
16113
- this._streamSource = new StreamSource({
16147
+ this._customSource = new CustomSource({
16114
16148
  getSize: async () => {
16115
- this._fileHandle = await node.fs.open(filePath, "r");
16116
- const stats = await this._fileHandle.stat();
16149
+ const fileHandle = await node.fs.open(filePath, "r");
16150
+ this._fileHandle = fileHandle;
16151
+ sourceFinalizationRegistry?.register(this, () => {
16152
+ void fileHandle.close();
16153
+ }, this);
16154
+ const stats = await fileHandle.stat();
16117
16155
  return stats.size;
16118
16156
  },
16119
16157
  read: async (start, end) => {
@@ -16128,21 +16166,24 @@ var Mediabunny = (() => {
16128
16166
  }
16129
16167
  /** @internal */
16130
16168
  _read(start, end, minReadPosition, maxReadPosition) {
16131
- return this._streamSource._read(start, end, minReadPosition, maxReadPosition);
16169
+ return this._customSource._read(start, end, minReadPosition, maxReadPosition);
16132
16170
  }
16133
16171
  /** @internal */
16134
16172
  _getFileSize() {
16135
- return this._streamSource._getFileSize();
16173
+ return this._customSource._getFileSize();
16136
16174
  }
16137
16175
  /** @internal */
16138
16176
  _dispose() {
16139
- this._streamSource._dispose();
16140
- void this._fileHandle?.close();
16141
- this._fileHandle = null;
16177
+ this._customSource._dispose();
16178
+ if (this._fileHandle) {
16179
+ void this._fileHandle.close();
16180
+ this._fileHandle = null;
16181
+ sourceFinalizationRegistry?.unregister(this);
16182
+ }
16142
16183
  }
16143
16184
  };
16144
- var StreamSource = class extends Source {
16145
- /** Creates a new {@link StreamSource} whose behavior is specified by `options`. */
16185
+ var CustomSource = class extends Source {
16186
+ /** Creates a new {@link CustomSource} whose behavior is specified by `options`. */
16146
16187
  constructor(options) {
16147
16188
  if (!options || typeof options !== "object") {
16148
16189
  throw new TypeError("options must be an object.");
@@ -16253,6 +16294,7 @@ var Mediabunny = (() => {
16253
16294
  this._options.dispose?.();
16254
16295
  }
16255
16296
  };
16297
+ var StreamSource = CustomSource;
16256
16298
  var ReadableStreamSource = class extends Source {
16257
16299
  /** Creates a new {@link ReadableStreamSource} backed by the specified `ReadableStream<Uint8Array>`. */
16258
16300
  constructor(stream, options = {}) {
@@ -18449,7 +18491,11 @@ var Mediabunny = (() => {
18449
18491
  return true;
18450
18492
  }
18451
18493
  currentPos = firstResult.startPos + firstResult.header.totalSize;
18452
- const secondResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
18494
+ const secondResult = await readNextMp3FrameHeader(
18495
+ input._reader,
18496
+ currentPos,
18497
+ currentPos + MP3_FRAME_HEADER_SIZE
18498
+ );
18453
18499
  if (!secondResult) {
18454
18500
  return false;
18455
18501
  }
@@ -24050,16 +24096,6 @@ var Mediabunny = (() => {
24050
24096
  polyfillSymbolDispose();
24051
24097
  var DEFAULT_SOURCE_CACHE_GROUP = 1;
24052
24098
  var ENCRYPTION_KEY_CACHE_GROUP = 2;
24053
- var inputFinalizationRegistry = null;
24054
- if (typeof FinalizationRegistry !== "undefined") {
24055
- inputFinalizationRegistry = new FinalizationRegistry((refs) => {
24056
- for (const ref of refs) {
24057
- if (!ref.freed) {
24058
- ref.free();
24059
- }
24060
- }
24061
- });
24062
- }
24063
24099
  var Input = class _Input extends EventEmitter {
24064
24100
  /**
24065
24101
  * Creates a new input file from the specified options. No reading operations will be performed until methods are
@@ -24114,7 +24150,6 @@ var Mediabunny = (() => {
24114
24150
  this._rootRef = options.source;
24115
24151
  }
24116
24152
  this._sourceRefs.push(this._rootRef);
24117
- inputFinalizationRegistry?.register(this, this._sourceRefs, this);
24118
24153
  }
24119
24154
  /** True if the input has been disposed. */
24120
24155
  get disposed() {
@@ -24402,7 +24437,6 @@ var Mediabunny = (() => {
24402
24437
  ref.free();
24403
24438
  }
24404
24439
  this._sourceRefs.length = 0;
24405
- inputFinalizationRegistry?.unregister(this);
24406
24440
  void this._demuxerPromise?.then((demuxer) => demuxer.dispose());
24407
24441
  }
24408
24442
  /**
@@ -24918,7 +24952,7 @@ var Mediabunny = (() => {
24918
24952
  const yearText = readId3V1String(slice, 4);
24919
24953
  const year = Number.parseInt(yearText, 10);
24920
24954
  if (Number.isInteger(year) && year > 0) {
24921
- tags.date ??= new Date(year, 0, 1);
24955
+ tags.date ??= new Date(String(year));
24922
24956
  }
24923
24957
  const commentBytes = readBytes(slice, 30);
24924
24958
  let comment;
@@ -25138,7 +25172,7 @@ var Mediabunny = (() => {
25138
25172
  const yearText = reader.readId3V2EncodingAndText(frameEndPos);
25139
25173
  const year = Number.parseInt(yearText, 10);
25140
25174
  if (Number.isInteger(year)) {
25141
- tags.date ??= new Date(year, 0, 1);
25175
+ tags.date ??= new Date(String(year));
25142
25176
  }
25143
25177
  }
25144
25178
  ;