mediabunny 1.55.6 → 1.56.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 (71) hide show
  1. package/dist/bundles/mediabunny.cjs +535 -180
  2. package/dist/bundles/mediabunny.min.cjs +12 -12
  3. package/dist/bundles/mediabunny.min.mjs +12 -12
  4. package/dist/bundles/mediabunny.mjs +535 -180
  5. package/dist/bundles/mediabunny.node.cjs +535 -180
  6. package/dist/mediabunny.d.ts +106 -19
  7. package/dist/modules/src/conversion.d.ts +57 -12
  8. package/dist/modules/src/conversion.d.ts.map +1 -1
  9. package/dist/modules/src/conversion.js +353 -119
  10. package/dist/modules/src/index.d.ts +2 -2
  11. package/dist/modules/src/index.d.ts.map +1 -1
  12. package/dist/modules/src/isobmff/isobmff-boxes.d.ts +1 -1
  13. package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
  14. package/dist/modules/src/isobmff/isobmff-boxes.js +59 -29
  15. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  16. package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -4
  17. package/dist/modules/src/isobmff/isobmff-muxer.d.ts +1 -1
  18. package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
  19. package/dist/modules/src/isobmff/isobmff-muxer.js +14 -6
  20. package/dist/modules/src/matroska/matroska-demuxer.d.ts +1 -0
  21. package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
  22. package/dist/modules/src/matroska/matroska-demuxer.js +20 -8
  23. package/dist/modules/src/matroska/matroska-muxer.d.ts +1 -0
  24. package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
  25. package/dist/modules/src/matroska/matroska-muxer.js +10 -1
  26. package/dist/modules/src/media-sink.d.ts.map +1 -1
  27. package/dist/modules/src/media-sink.js +12 -0
  28. package/dist/modules/src/media-source.d.ts +17 -5
  29. package/dist/modules/src/media-source.d.ts.map +1 -1
  30. package/dist/modules/src/media-source.js +14 -9
  31. package/dist/modules/src/misc.d.ts +2 -0
  32. package/dist/modules/src/misc.d.ts.map +1 -1
  33. package/dist/modules/src/misc.js +6 -0
  34. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts +5 -0
  35. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.d.ts.map +1 -1
  36. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +37 -16
  37. package/dist/modules/src/mpeg-ts/mpeg-ts-misc.d.ts +1 -0
  38. package/dist/modules/src/mpeg-ts/mpeg-ts-misc.d.ts.map +1 -1
  39. package/dist/modules/src/mpeg-ts/mpeg-ts-misc.js +1 -0
  40. package/dist/modules/src/mpeg-ts/mpeg-ts-muxer.d.ts.map +1 -1
  41. package/dist/modules/src/mpeg-ts/mpeg-ts-muxer.js +14 -10
  42. package/dist/modules/src/muxer.d.ts.map +1 -1
  43. package/dist/modules/src/muxer.js +0 -3
  44. package/dist/modules/src/output-format.d.ts +17 -0
  45. package/dist/modules/src/output-format.d.ts.map +1 -1
  46. package/dist/modules/src/output-format.js +39 -1
  47. package/dist/modules/src/sample.d.ts +2 -2
  48. package/dist/modules/src/sample.d.ts.map +1 -1
  49. package/dist/modules/src/sample.js +16 -16
  50. package/dist/modules/src/source.d.ts +10 -0
  51. package/dist/modules/src/source.d.ts.map +1 -1
  52. package/dist/modules/src/source.js +38 -3
  53. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/src/conversion.ts +460 -99
  56. package/src/index.ts +2 -0
  57. package/src/isobmff/isobmff-boxes.ts +76 -32
  58. package/src/isobmff/isobmff-demuxer.ts +0 -5
  59. package/src/isobmff/isobmff-muxer.ts +22 -12
  60. package/src/matroska/matroska-demuxer.ts +24 -11
  61. package/src/matroska/matroska-muxer.ts +14 -1
  62. package/src/media-sink.ts +12 -0
  63. package/src/media-source.ts +28 -8
  64. package/src/misc.ts +8 -0
  65. package/src/mpeg-ts/mpeg-ts-demuxer.ts +52 -14
  66. package/src/mpeg-ts/mpeg-ts-misc.ts +1 -0
  67. package/src/mpeg-ts/mpeg-ts-muxer.ts +20 -10
  68. package/src/muxer.ts +0 -4
  69. package/src/output-format.ts +58 -1
  70. package/src/sample.ts +16 -16
  71. package/src/source.ts +55 -3
@@ -259,6 +259,9 @@ var Mediabunny = (() => {
259
259
  var isU32 = (value) => {
260
260
  return value >= 0 && value < 2 ** 32;
261
261
  };
262
+ var isI32 = (value) => {
263
+ return value >= -(2 ** 31) && value < 2 ** 31;
264
+ };
262
265
  var readExpGolomb = (bitstream) => {
263
266
  let leadingZeroBits = 0;
264
267
  while (bitstream.readBits(1) === 0 && leadingZeroBits < 32) {
@@ -570,6 +573,9 @@ var Mediabunny = (() => {
570
573
  var lerp = (from, to, t) => {
571
574
  return from + (to - from) * t;
572
575
  };
576
+ var modEuclid = (value, modulus) => {
577
+ return value - Math.floor(value / modulus) * modulus;
578
+ };
573
579
  var UNDETERMINED_LANGUAGE = "und";
574
580
  var roundIfAlmostInteger = (value) => {
575
581
  const rounded = Math.round(value);
@@ -6657,9 +6663,6 @@ var Mediabunny = (() => {
6657
6663
  const segmentDuration = version === 1 ? readU64Be(slice) : readU32Be(slice);
6658
6664
  const mediaTime = version === 1 ? readI64Be(slice) : readI32Be(slice);
6659
6665
  const mediaRate = readFixed_16_16(slice);
6660
- if (segmentDuration === 0) {
6661
- continue;
6662
- }
6663
6666
  if (relevantEntryFound) {
6664
6667
  Logging._warn(
6665
6668
  "Unsupported edit list: multiple edits are not currently supported. Only using first edit."
@@ -11321,6 +11324,22 @@ var Mediabunny = (() => {
11321
11324
  }
11322
11325
  }
11323
11326
  }
11327
+ async getDurationFromMetadata(segment) {
11328
+ if (segment.duration <= 0) {
11329
+ return null;
11330
+ }
11331
+ let minTimestamp = null;
11332
+ for (const track of segment.tracks) {
11333
+ assert(track.trackBacking);
11334
+ const firstPacket = await track.trackBacking.getFirstPacket({ metadataOnly: true });
11335
+ if (firstPacket) {
11336
+ minTimestamp = Math.min(minTimestamp ?? Infinity, firstPacket.timestamp);
11337
+ }
11338
+ }
11339
+ let endTimestamp = segment.duration / segment.timestampFactor;
11340
+ endTimestamp += minTimestamp ?? 0;
11341
+ return endTimestamp;
11342
+ }
11324
11343
  };
11325
11344
  var MatroskaTrackBacking = class {
11326
11345
  constructor(internalTrack) {
@@ -11380,14 +11399,7 @@ var Mediabunny = (() => {
11380
11399
  return null;
11381
11400
  }
11382
11401
  async getDurationFromMetadata() {
11383
- const segment = this.internalTrack.segment;
11384
- if (segment.duration <= 0) {
11385
- return null;
11386
- }
11387
- let endTimestamp = segment.duration / segment.timestampFactor;
11388
- const firstPacket = await this.getFirstPacket({ metadataOnly: true });
11389
- endTimestamp += firstPacket?.timestamp ?? 0;
11390
- return endTimestamp;
11402
+ return this.internalTrack.demuxer.getDurationFromMetadata(this.internalTrack.segment);
11391
11403
  }
11392
11404
  async getLiveRefreshInterval() {
11393
11405
  return null;
@@ -14474,6 +14486,7 @@ var Mediabunny = (() => {
14474
14486
 
14475
14487
  // src/mpeg-ts/mpeg-ts-misc.ts
14476
14488
  var TIMESCALE = 9e4;
14489
+ var TIMESTAMP_MODULUS = 2 ** 33;
14477
14490
  var TS_PACKET_SIZE = 188;
14478
14491
  var buildMpegTsMimeType = (codecStrings) => {
14479
14492
  let string = "video/MP2T";
@@ -14508,6 +14521,7 @@ var Mediabunny = (() => {
14508
14521
  this.seekChunkSize = 5 * 1024 * 1024;
14509
14522
  // 5 MiB, picked because most HLS segments are below this size
14510
14523
  this.minReferencePointByteDistance = -1;
14524
+ this.timestampWrapInfo = null;
14511
14525
  this.reader = input._reader;
14512
14526
  }
14513
14527
  async readMetadata() {
@@ -14765,7 +14779,7 @@ var Mediabunny = (() => {
14765
14779
  const elementaryStream = this.elementaryStreams.find((x) => x.pid === section.pid);
14766
14780
  outer:
14767
14781
  if (elementaryStream && !elementaryStream.initialized) {
14768
- const pesPacket = readPesPacket(section, true);
14782
+ const pesPacket = readPesPacket(this, section, true);
14769
14783
  if (!pesPacket) {
14770
14784
  throw new Error(
14771
14785
  `Couldn't read first PES packet for Elementary Stream with PID ${elementaryStream.pid}`
@@ -15160,8 +15174,22 @@ var Mediabunny = (() => {
15160
15174
  body: bytes2.subarray(4)
15161
15175
  };
15162
15176
  }
15177
+ normalizeTimestamp(timestamp) {
15178
+ if (!this.timestampWrapInfo) {
15179
+ const tolerance = 60 * TIMESCALE;
15180
+ this.timestampWrapInfo = {
15181
+ reference: timestamp - tolerance,
15182
+ offset: timestamp >= TIMESTAMP_MODULUS - tolerance ? -TIMESTAMP_MODULUS : TIMESTAMP_MODULUS
15183
+ };
15184
+ }
15185
+ const { reference, offset } = this.timestampWrapInfo;
15186
+ if (offset < 0 && timestamp >= reference || offset > 0 && timestamp < reference) {
15187
+ return timestamp + offset;
15188
+ }
15189
+ return timestamp;
15190
+ }
15163
15191
  };
15164
- var readPesPacketHeader = (section, expectPts) => {
15192
+ var readPesPacketHeader = (demuxer, section, expectPts) => {
15165
15193
  if (section.payload.byteLength < 3) {
15166
15194
  return null;
15167
15195
  }
@@ -15187,6 +15215,7 @@ var Mediabunny = (() => {
15187
15215
  pts += bitstream.readBits(15) * (1 << 15);
15188
15216
  bitstream.skipBits(1);
15189
15217
  pts += bitstream.readBits(15);
15218
+ pts = demuxer.normalizeTimestamp(pts);
15190
15219
  } else {
15191
15220
  if (expectPts) {
15192
15221
  throw new Error(MISSING_PTS_ERROR_MESSAGE);
@@ -15199,9 +15228,9 @@ var Mediabunny = (() => {
15199
15228
  randomAccessIndicator: section.randomAccessIndicator
15200
15229
  };
15201
15230
  };
15202
- var readPesPacket = (section, expectPts) => {
15231
+ var readPesPacket = (demuxer, section, expectPts) => {
15203
15232
  assert(section.endPos !== null);
15204
- const header = readPesPacketHeader(section, expectPts);
15233
+ const header = readPesPacketHeader(demuxer, section, expectPts);
15205
15234
  if (!header) {
15206
15235
  return null;
15207
15236
  }
@@ -15313,7 +15342,7 @@ var Mediabunny = (() => {
15313
15342
  async getFirstPacket(options) {
15314
15343
  const section = this.elementaryStream.firstSection;
15315
15344
  assert(section);
15316
- const pesPacket = readPesPacket(section, true);
15345
+ const pesPacket = readPesPacket(this.elementaryStream.demuxer, section, true);
15317
15346
  assert(pesPacket);
15318
15347
  const context = new PacketReadingContext(this.elementaryStream, pesPacket);
15319
15348
  const buffer = new PacketBuffer(this, context);
@@ -15346,7 +15375,7 @@ var Mediabunny = (() => {
15346
15375
  const demuxer = this.elementaryStream.demuxer;
15347
15376
  const section = await demuxer.readSection(sectionStartPos, true);
15348
15377
  assert(section);
15349
- const pesPacket = readPesPacket(section, true);
15378
+ const pesPacket = readPesPacket(demuxer, section, true);
15350
15379
  assert(pesPacket);
15351
15380
  const context = new PacketReadingContext(this.elementaryStream, pesPacket);
15352
15381
  buffer = new PacketBuffer(this, context);
@@ -15404,7 +15433,7 @@ var Mediabunny = (() => {
15404
15433
  if (!section) {
15405
15434
  return null;
15406
15435
  }
15407
- const pesPacketHeader = readPesPacketHeader(section, false);
15436
+ const pesPacketHeader = readPesPacketHeader(demuxer, section, false);
15408
15437
  if (pesPacketHeader && pesPacketHeader.pts !== null) {
15409
15438
  return {
15410
15439
  pesPacketHeader,
@@ -15418,7 +15447,7 @@ var Mediabunny = (() => {
15418
15447
  };
15419
15448
  const firstSection = this.elementaryStream.firstSection;
15420
15449
  assert(firstSection);
15421
- const firstPesPacketHeader = readPesPacketHeader(firstSection, true);
15450
+ const firstPesPacketHeader = readPesPacketHeader(demuxer, firstSection, true);
15422
15451
  assert(firstPesPacketHeader);
15423
15452
  if (searchPts < firstPesPacketHeader.pts) {
15424
15453
  return null;
@@ -15473,7 +15502,7 @@ var Mediabunny = (() => {
15473
15502
  const retrieveEncodedPacket = async (sectionStartPos, predicate) => {
15474
15503
  const section = await demuxer.readSection(sectionStartPos, true);
15475
15504
  assert(section);
15476
- const pesPacket = readPesPacket(section, true);
15505
+ const pesPacket = readPesPacket(demuxer, section, true);
15477
15506
  assert(pesPacket);
15478
15507
  const context = new PacketReadingContext(this.elementaryStream, pesPacket);
15479
15508
  const buffer = new PacketBuffer(this, context);
@@ -15516,7 +15545,7 @@ var Mediabunny = (() => {
15516
15545
  if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
15517
15546
  const section = await demuxer.readSection(currentPos, false);
15518
15547
  if (section) {
15519
- const nextPesHeader = readPesPacketHeader(section, false);
15548
+ const nextPesHeader = readPesPacketHeader(demuxer, section, false);
15520
15549
  if (nextPesHeader && nextPesHeader.pts !== null) {
15521
15550
  if (nextPesHeader.pts > searchPts) {
15522
15551
  break outer;
@@ -15541,7 +15570,7 @@ var Mediabunny = (() => {
15541
15570
  if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
15542
15571
  const section = await demuxer.readSection(pos, false);
15543
15572
  if (section) {
15544
- const header = readPesPacketHeader(section, false);
15573
+ const header = readPesPacketHeader(demuxer, section, false);
15545
15574
  if (header && header.pts !== null) {
15546
15575
  currentPesHeader = header;
15547
15576
  break;
@@ -15586,7 +15615,7 @@ var Mediabunny = (() => {
15586
15615
  isKeyPacket = pesHeader.randomAccessIndicator === 1;
15587
15616
  } else {
15588
15617
  assert(pesHeaderSection);
15589
- const pesPacket = readPesPacket(pesHeaderSection, true);
15618
+ const pesPacket = readPesPacket(demuxer, pesHeaderSection, true);
15590
15619
  assert(pesPacket);
15591
15620
  const context = new PacketReadingContext(this.elementaryStream, pesPacket);
15592
15621
  await context.markNextPacket();
@@ -15614,7 +15643,7 @@ var Mediabunny = (() => {
15614
15643
  if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
15615
15644
  const section = await demuxer.readSection(currentPos, readSectionsInFull);
15616
15645
  if (section) {
15617
- const nextPesHeader = readPesPacketHeader(section, false);
15646
+ const nextPesHeader = readPesPacketHeader(demuxer, section, false);
15618
15647
  if (nextPesHeader && nextPesHeader.pts !== null) {
15619
15648
  pesHeader = nextPesHeader;
15620
15649
  pesHeaderSection = section;
@@ -15640,7 +15669,7 @@ var Mediabunny = (() => {
15640
15669
  if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
15641
15670
  const section = await demuxer.readSection(pos, readSectionsInFull);
15642
15671
  if (section) {
15643
- const header = readPesPacketHeader(section, false);
15672
+ const header = readPesPacketHeader(demuxer, section, false);
15644
15673
  if (header && header.pts !== null) {
15645
15674
  startPesHeader = header;
15646
15675
  break;
@@ -15812,7 +15841,7 @@ var Mediabunny = (() => {
15812
15841
  if (!nextSection) {
15813
15842
  return;
15814
15843
  }
15815
- const nextPesPacket = readPesPacket(nextSection, false);
15844
+ const nextPesPacket = readPesPacket(this.demuxer, nextSection, false);
15816
15845
  if (nextPesPacket) {
15817
15846
  pesPacket = nextPesPacket;
15818
15847
  break;
@@ -17084,6 +17113,9 @@ var Mediabunny = (() => {
17084
17113
  if (options.useStreamReader !== void 0 && typeof options.useStreamReader !== "boolean") {
17085
17114
  throw new TypeError("options.useStreamReader, when provided, must be a boolean.");
17086
17115
  }
17116
+ if (options.handleUnhandledError !== void 0 && typeof options.handleUnhandledError !== "function") {
17117
+ throw new TypeError("options.handleUnhandledError, when provided, must be a function.");
17118
+ }
17087
17119
  super();
17088
17120
  /** @internal */
17089
17121
  this._readers = /* @__PURE__ */ new WeakMap();
@@ -17102,7 +17134,8 @@ var Mediabunny = (() => {
17102
17134
  });
17103
17135
  }
17104
17136
  },
17105
- prefetchProfile: PREFETCH_PROFILES.fileSystem
17137
+ prefetchProfile: PREFETCH_PROFILES.fileSystem,
17138
+ handleUnhandledError: options.handleUnhandledError
17106
17139
  });
17107
17140
  this._orchestrator.fileSize = blob.size;
17108
17141
  }
@@ -17210,6 +17243,9 @@ var Mediabunny = (() => {
17210
17243
  if (options.fetchFn !== void 0 && typeof options.fetchFn !== "function") {
17211
17244
  throw new TypeError("options.fetchFn, when provided, must be a function.");
17212
17245
  }
17246
+ if (options.handleUnhandledError !== void 0 && typeof options.handleUnhandledError !== "function") {
17247
+ throw new TypeError("options.handleUnhandledError, when provided, must be a function.");
17248
+ }
17213
17249
  const urlString = url2 instanceof Request ? url2.url : url2 instanceof URL ? url2.href : url2;
17214
17250
  super(
17215
17251
  urlString,
@@ -17266,7 +17302,8 @@ var Mediabunny = (() => {
17266
17302
  maxCacheSize: options.maxCacheSize ?? 64 * 2 ** 20,
17267
17303
  maxWorkerCount: options.parallelism ?? DEFAULT_PARALLELISM,
17268
17304
  runWorker: this._runWorker.bind(this),
17269
- prefetchProfile: PREFETCH_PROFILES.network
17305
+ prefetchProfile: PREFETCH_PROFILES.network,
17306
+ handleUnhandledError: options.handleUnhandledError
17270
17307
  });
17271
17308
  }
17272
17309
  /** @internal */
@@ -17329,7 +17366,7 @@ var Mediabunny = (() => {
17329
17366
  this.rootPath = response.url;
17330
17367
  }
17331
17368
  outer:
17332
- if (this._orchestrator.fileSize === null) {
17369
+ if (this._orchestrator.fileSize === null && (response.status === 206 || response.type === "basic" && !response.headers.has("Content-Encoding"))) {
17333
17370
  const contentRange = response.headers.get("Content-Range");
17334
17371
  if (contentRange) {
17335
17372
  const match = /\/(\d+)/.exec(contentRange);
@@ -17485,7 +17522,8 @@ var Mediabunny = (() => {
17485
17522
  cancel: () => currentReader.cancel()
17486
17523
  });
17487
17524
  const backing = new ReadableStreamSource(wrappedStream, {
17488
- maxCacheSize: this._orchestrator.options.maxCacheSize
17525
+ maxCacheSize: this._orchestrator.options.maxCacheSize,
17526
+ handleUnhandledError: this._options.handleUnhandledError
17489
17527
  });
17490
17528
  backing._endIndex = this._orchestrator.fileSize;
17491
17529
  backing._cacheMissErrorMessage = "Attempted to read data from an already-evicted part of the cache. Because the HTTP server did not honor the range request, data can only be read sequentially, with old data being evicted from the cache. To fix this issue, either ensure your server responds to range requests with 206 Partial Content, or set maxCacheSize to Infinity in the UrlSource options. Note that the latter will store the entire file in the cache if needed, no matter how large.";
@@ -17585,7 +17623,8 @@ var Mediabunny = (() => {
17585
17623
  return buffer;
17586
17624
  },
17587
17625
  maxCacheSize: options.maxCacheSize,
17588
- prefetchProfile: "fileSystem"
17626
+ prefetchProfile: "fileSystem",
17627
+ handleUnhandledError: options.handleUnhandledError
17589
17628
  });
17590
17629
  }
17591
17630
  /** @internal */
@@ -17621,6 +17660,9 @@ var Mediabunny = (() => {
17621
17660
  if (options.dispose !== void 0 && typeof options.dispose !== "function") {
17622
17661
  throw new TypeError("options.dispose, when provided, must be a function.");
17623
17662
  }
17663
+ if (options.handleUnhandledError !== void 0 && typeof options.handleUnhandledError !== "function") {
17664
+ throw new TypeError("options.handleUnhandledError, when provided, must be a function.");
17665
+ }
17624
17666
  if (options.maxCacheSize !== void 0 && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0)) {
17625
17667
  throw new TypeError("options.maxCacheSize, when provided, must be a non-negative number.");
17626
17668
  }
@@ -17636,7 +17678,8 @@ var Mediabunny = (() => {
17636
17678
  maxWorkerCount: 2,
17637
17679
  // Fixed for now, *should* be fine
17638
17680
  prefetchProfile: PREFETCH_PROFILES[options.prefetchProfile ?? "none"],
17639
- runWorker: this._runWorker.bind(this)
17681
+ runWorker: this._runWorker.bind(this),
17682
+ handleUnhandledError: options.handleUnhandledError
17640
17683
  });
17641
17684
  }
17642
17685
  /** @internal */
@@ -17728,6 +17771,9 @@ var Mediabunny = (() => {
17728
17771
  if (!options || typeof options !== "object") {
17729
17772
  throw new TypeError("options must be an object.");
17730
17773
  }
17774
+ if (options.handleUnhandledError !== void 0 && typeof options.handleUnhandledError !== "function") {
17775
+ throw new TypeError("options.handleUnhandledError, when provided, must be a function.");
17776
+ }
17731
17777
  if (options.maxCacheSize !== void 0 && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0)) {
17732
17778
  throw new TypeError("options.maxCacheSize, when provided, must be a non-negative number.");
17733
17779
  }
@@ -17755,6 +17801,7 @@ var Mediabunny = (() => {
17755
17801
  this._cacheMissErrorMessage = "Attempted to read data from an already-evicted part of the cache. With ReadableStreamSource, you must access the data more sequentially or increase the size of its cache.";
17756
17802
  this._stream = stream;
17757
17803
  this._maxCacheSize = options.maxCacheSize ?? 32 * 2 ** 20;
17804
+ this._handleUnhandledError = options.handleUnhandledError;
17758
17805
  }
17759
17806
  /** @internal */
17760
17807
  _getFileSize() {
@@ -17823,6 +17870,8 @@ var Mediabunny = (() => {
17823
17870
  if (this._pendingSlices.length > 0) {
17824
17871
  this._pendingSlices.forEach((x) => x.reject(error));
17825
17872
  this._pendingSlices.length = 0;
17873
+ } else if (this._handleUnhandledError) {
17874
+ this._handleUnhandledError(error);
17826
17875
  } else {
17827
17876
  throw error;
17828
17877
  }
@@ -18106,7 +18155,11 @@ var Mediabunny = (() => {
18106
18155
  if (this.disposed) {
18107
18156
  return;
18108
18157
  }
18109
- throw error;
18158
+ if (this.options.handleUnhandledError) {
18159
+ this.options.handleUnhandledError(error);
18160
+ } else {
18161
+ throw error;
18162
+ }
18110
18163
  });
18111
18164
  }
18112
18165
  return result;
@@ -18194,7 +18247,11 @@ var Mediabunny = (() => {
18194
18247
  worker.pendingSlices.forEach((x) => x.reject(error));
18195
18248
  worker.pendingSlices.length = 0;
18196
18249
  } else if (!worker.aborted && !this.disposed) {
18197
- throw error;
18250
+ if (this.options.handleUnhandledError) {
18251
+ this.options.handleUnhandledError(error);
18252
+ } else {
18253
+ throw error;
18254
+ }
18198
18255
  }
18199
18256
  }).finally(() => {
18200
18257
  if (worker.running) {
@@ -22098,30 +22155,30 @@ var Mediabunny = (() => {
22098
22155
  }
22099
22156
  }
22100
22157
  /**
22101
- * Returns a new {@link AudioSample} containing only the frames in the range [startSample, endSample). Both bounds
22158
+ * Returns a new {@link AudioSample} containing only the frames in the range [startFrame, endFrame). Both bounds
22102
22159
  * must lie within this sample's range of frames. The returned sample's timestamp is shifted to match the start of
22103
22160
  * the trimmed section.
22104
22161
  */
22105
- trim(startSample, endSample = this.numberOfFrames) {
22106
- if (!Number.isInteger(startSample) || startSample < 0) {
22107
- throw new TypeError("startSample must be a non-negative integer.");
22162
+ trim(startFrame, endFrame = this.numberOfFrames) {
22163
+ if (!Number.isInteger(startFrame) || startFrame < 0) {
22164
+ throw new TypeError("startFrame must be a non-negative integer.");
22108
22165
  }
22109
- if (!Number.isInteger(endSample) || endSample < 0) {
22110
- throw new TypeError("endSample must be a non-negative integer.");
22166
+ if (!Number.isInteger(endFrame) || endFrame < 0) {
22167
+ throw new TypeError("endFrame must be a non-negative integer.");
22111
22168
  }
22112
- if (startSample > this.numberOfFrames) {
22113
- throw new RangeError("startSample out of range.");
22169
+ if (startFrame > this.numberOfFrames) {
22170
+ throw new RangeError("startFrame out of range.");
22114
22171
  }
22115
- if (endSample > this.numberOfFrames) {
22116
- throw new RangeError("endSample out of range.");
22172
+ if (endFrame > this.numberOfFrames) {
22173
+ throw new RangeError("endFrame out of range.");
22117
22174
  }
22118
- if (endSample < startSample) {
22119
- throw new RangeError("endSample must not be less than startSample.");
22175
+ if (endFrame < startFrame) {
22176
+ throw new RangeError("endFrame must not be less than startFrame.");
22120
22177
  }
22121
22178
  if (this._closed) {
22122
22179
  throw new Error("AudioSample is closed.");
22123
22180
  }
22124
- const frameCount = endSample - startSample;
22181
+ const frameCount = endFrame - startFrame;
22125
22182
  const bytesPerSample = getBytesPerSample(this.format);
22126
22183
  let data;
22127
22184
  if (formatIsPlanar(this.format)) {
@@ -22132,7 +22189,7 @@ var Mediabunny = (() => {
22132
22189
  this.copyTo(data.subarray(i * planeSize, (i + 1) * planeSize), {
22133
22190
  planeIndex: i,
22134
22191
  format: this.format,
22135
- frameOffset: startSample,
22192
+ frameOffset: startFrame,
22136
22193
  frameCount
22137
22194
  });
22138
22195
  }
@@ -22143,7 +22200,7 @@ var Mediabunny = (() => {
22143
22200
  this.copyTo(data, {
22144
22201
  planeIndex: 0,
22145
22202
  format: this.format,
22146
- frameOffset: startSample,
22203
+ frameOffset: startFrame,
22147
22204
  frameCount
22148
22205
  });
22149
22206
  }
@@ -22153,7 +22210,7 @@ var Mediabunny = (() => {
22153
22210
  format: this.format,
22154
22211
  sampleRate: this.sampleRate,
22155
22212
  numberOfChannels: this.numberOfChannels,
22156
- timestamp: this.timestamp + startSample / this.sampleRate
22213
+ timestamp: this.timestamp + startFrame / this.sampleRate
22157
22214
  });
22158
22215
  }
22159
22216
  /**
@@ -23736,11 +23793,15 @@ var Mediabunny = (() => {
23736
23793
  async next() {
23737
23794
  while (true) {
23738
23795
  if (track.input._disposed) {
23796
+ terminated = true;
23797
+ ended = true;
23739
23798
  closeSamples();
23740
23799
  throw new InputDisposedError();
23741
23800
  } else if (terminated) {
23742
23801
  return { value: void 0, done: true };
23743
23802
  } else if (hasOutOfBandError) {
23803
+ terminated = true;
23804
+ ended = true;
23744
23805
  closeSamples();
23745
23806
  throw outOfBandError;
23746
23807
  } else if (sampleQueue.length > 0) {
@@ -23904,11 +23965,13 @@ var Mediabunny = (() => {
23904
23965
  async next() {
23905
23966
  while (true) {
23906
23967
  if (track.input._disposed) {
23968
+ terminated = true;
23907
23969
  closeSamples();
23908
23970
  throw new InputDisposedError();
23909
23971
  } else if (terminated) {
23910
23972
  return { value: void 0, done: true };
23911
23973
  } else if (hasOutOfBandError) {
23974
+ terminated = true;
23912
23975
  closeSamples();
23913
23976
  throw outOfBandError;
23914
23977
  } else if (sampleQueue.length > 0) {
@@ -27880,9 +27943,6 @@ var Mediabunny = (() => {
27880
27943
  onTrackClose(track) {
27881
27944
  }
27882
27945
  validateTimestamp(track, timestampInSeconds, isKeyPacket) {
27883
- if (timestampInSeconds < 0) {
27884
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
27885
- }
27886
27946
  let timestampInfo = this.trackTimestampInfo.get(track);
27887
27947
  if (!timestampInfo) {
27888
27948
  if (!isKeyPacket) {
@@ -28558,7 +28618,8 @@ var Mediabunny = (() => {
28558
28618
  ascii("dash")
28559
28619
  ]);
28560
28620
  var sidx = (muxer, referencedSize) => {
28561
- let duration = muxer.maxWrittenEndTimestamp - muxer.minWrittenTimestamp;
28621
+ const earliestPresentationTime = Math.max(0, muxer.minWrittenTimestamp);
28622
+ let duration = Math.max(0, muxer.maxWrittenEndTimestamp - earliestPresentationTime);
28562
28623
  if (!Number.isFinite(duration)) {
28563
28624
  duration = 0;
28564
28625
  }
@@ -28567,7 +28628,7 @@ var Mediabunny = (() => {
28567
28628
  // Reference ID
28568
28629
  u32(GLOBAL_TIMESCALE),
28569
28630
  // Timescale
28570
- u64(intoTimescale(muxer.minWrittenTimestamp, GLOBAL_TIMESCALE)),
28631
+ u64(intoTimescale(earliestPresentationTime, GLOBAL_TIMESCALE)),
28571
28632
  // Earliest presentation time
28572
28633
  u64(0),
28573
28634
  // First offset
@@ -28596,7 +28657,13 @@ var Mediabunny = (() => {
28596
28657
  var mvhd = (creationTime, trackDatas) => {
28597
28658
  const duration = Math.max(
28598
28659
  0,
28599
- ...trackDatas.map((trackData) => intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE))
28660
+ ...trackDatas.map((trackData) => (
28661
+ // Round separately to match the edit list
28662
+ Math.max(
28663
+ 0,
28664
+ intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE)
28665
+ )
28666
+ ))
28600
28667
  );
28601
28668
  const nextTrackId = Math.max(0, ...trackDatas.map((x) => x.track.id)) + 1;
28602
28669
  const needsU64 = !isU32(creationTime) || !isU32(duration);
@@ -28646,10 +28713,10 @@ var Mediabunny = (() => {
28646
28713
  };
28647
28714
  var trak = (trackData, creationTime) => {
28648
28715
  const trackMetadata = getTrackMetadata(trackData);
28649
- const needsEditList = trackData.startTimestampOffset !== null && trackData.startTimestampOffset > 0;
28716
+ const needsEditList = trackData.startTimestampOffset !== null && trackData.startTimestampOffset !== 0;
28650
28717
  return box("trak", void 0, [
28651
28718
  tkhd(trackData, creationTime),
28652
- needsEditList ? edts(trackData, trackData.startTimestampOffset) : null,
28719
+ needsEditList ? edts(trackData) : null,
28653
28720
  mdia(trackData, creationTime),
28654
28721
  trackMetadata.name !== void 0 ? box("udta", void 0, [
28655
28722
  box("name", [
@@ -28660,7 +28727,10 @@ var Mediabunny = (() => {
28660
28727
  ]);
28661
28728
  };
28662
28729
  var tkhd = (trackData, creationTime) => {
28663
- const durationInGlobalTimescale = intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE);
28730
+ const durationInGlobalTimescale = Math.max(
28731
+ 0,
28732
+ intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE)
28733
+ );
28664
28734
  const needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
28665
28735
  const u32OrU64 = needsU64 ? u64 : u32;
28666
28736
  let matrix;
@@ -28704,32 +28774,58 @@ var Mediabunny = (() => {
28704
28774
  // Track height
28705
28775
  ]);
28706
28776
  };
28707
- var edts = (trackData, offset) => {
28708
- const startOffset = intoTimescale(offset, GLOBAL_TIMESCALE);
28709
- const mediaDuration = intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE);
28710
- const needs64Bits = !isU32(startOffset) || !isU32(mediaDuration);
28711
- const u32OrU64 = needs64Bits ? u64 : u32;
28712
- const i32OrI64 = needs64Bits ? i64 : i32;
28713
- return box("edts", void 0, [
28714
- fullBox("elst", needs64Bits ? 1 : 0, 0, [
28715
- u32(2),
28716
- // Entry count
28717
- // #1
28718
- u32OrU64(startOffset),
28719
- // Segment duration
28720
- i32OrI64(-1),
28721
- // Media time
28722
- fixed_16_16(1),
28723
- // Media rate
28724
- // #2
28725
- u32OrU64(mediaDuration),
28726
- // Segment duration
28727
- i32OrI64(0),
28728
- // Media time
28729
- fixed_16_16(1)
28730
- // Media rate
28731
- ])
28732
- ]);
28777
+ var edts = (trackData) => {
28778
+ const offset = trackData.startTimestampOffset;
28779
+ assert(offset !== null);
28780
+ if (offset > 0) {
28781
+ const startOffset = intoTimescale(offset, GLOBAL_TIMESCALE);
28782
+ const mediaDuration = intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE);
28783
+ const needs64Bits = !isU32(startOffset) || !isU32(mediaDuration);
28784
+ const u32OrU64 = needs64Bits ? u64 : u32;
28785
+ const i32OrI64 = needs64Bits ? i64 : i32;
28786
+ return box("edts", void 0, [
28787
+ fullBox("elst", needs64Bits ? 1 : 0, 0, [
28788
+ u32(2),
28789
+ // Entry count
28790
+ // #1
28791
+ u32OrU64(startOffset),
28792
+ // Segment duration
28793
+ i32OrI64(-1),
28794
+ // Media time
28795
+ fixed_16_16(1),
28796
+ // Media rate
28797
+ // #2
28798
+ u32OrU64(mediaDuration),
28799
+ // Segment duration
28800
+ i32OrI64(0),
28801
+ // Media time
28802
+ fixed_16_16(1)
28803
+ // Media rate
28804
+ ])
28805
+ ]);
28806
+ } else {
28807
+ const mediaTime = intoTimescale(-offset, trackData.timescale);
28808
+ const mediaDuration = Math.max(
28809
+ 0,
28810
+ intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(offset, GLOBAL_TIMESCALE)
28811
+ );
28812
+ const needs64Bits = !isI32(mediaTime) || !isU32(mediaDuration);
28813
+ const u32OrU64 = needs64Bits ? u64 : u32;
28814
+ const i32OrI64 = needs64Bits ? i64 : i32;
28815
+ return box("edts", void 0, [
28816
+ fullBox("elst", needs64Bits ? 1 : 0, 0, [
28817
+ u32(1),
28818
+ // Entry count
28819
+ // #1
28820
+ u32OrU64(mediaDuration),
28821
+ // Segment duration
28822
+ i32OrI64(mediaTime),
28823
+ // Media time
28824
+ fixed_16_16(1)
28825
+ // Media rate
28826
+ ])
28827
+ ]);
28828
+ }
28733
28829
  };
28734
28830
  var mdia = (trackData, creationTime) => box("mdia", void 0, [
28735
28831
  mdhd(trackData, creationTime),
@@ -31038,7 +31134,7 @@ var Mediabunny = (() => {
31038
31134
  currentChunk: null,
31039
31135
  compactlyCodedChunkTable: [],
31040
31136
  closed: false,
31041
- lastCueEndTimestamp: 0,
31137
+ lastCueEndTimestamp: null,
31042
31138
  cueQueue: [],
31043
31139
  nextSourceId: 0,
31044
31140
  cueToSourceId: /* @__PURE__ */ new WeakMap()
@@ -31170,6 +31266,7 @@ var Mediabunny = (() => {
31170
31266
  }
31171
31267
  async processWebVTTCues(trackData, until) {
31172
31268
  while (trackData.cueQueue.length > 0) {
31269
+ trackData.lastCueEndTimestamp ??= Math.min(0, trackData.cueQueue[0].timestamp);
31173
31270
  const timestamps = /* @__PURE__ */ new Set([]);
31174
31271
  for (const cue of trackData.cueQueue) {
31175
31272
  assert(cue.timestamp <= until);
@@ -31253,9 +31350,8 @@ var Mediabunny = (() => {
31253
31350
  return;
31254
31351
  }
31255
31352
  if (trackData.type === "audio" && trackData.info.requiresPcmTransformation) {
31256
- if (!this.isFragmented) {
31257
- trackData.startTimestampOffset ??= trackData.timestampProcessingQueue[0].timestamp;
31258
- }
31353
+ assert(!this.isFragmented);
31354
+ trackData.startTimestampOffset ??= trackData.timestampProcessingQueue[0].timestamp;
31259
31355
  let totalDuration = 0;
31260
31356
  for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
31261
31357
  const sample = trackData.timestampProcessingQueue[i];
@@ -31275,7 +31371,9 @@ var Mediabunny = (() => {
31275
31371
  return;
31276
31372
  }
31277
31373
  const sortedTimestamps = trackData.timestampProcessingQueue.map((x) => x.timestamp).sort((a, b) => a - b);
31278
- if (!this.isFragmented) {
31374
+ if (this.isFragmented) {
31375
+ trackData.startTimestampOffset ??= Math.min(sortedTimestamps[0], 0);
31376
+ } else {
31279
31377
  trackData.startTimestampOffset ??= sortedTimestamps[0];
31280
31378
  }
31281
31379
  for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
@@ -31526,11 +31624,16 @@ var Mediabunny = (() => {
31526
31624
  let fragmentStartTimestamp = Infinity;
31527
31625
  for (let i = 0; i < tracksInFragment.length; i++) {
31528
31626
  const trackData = tracksInFragment[i];
31627
+ assert(trackData.currentChunk);
31628
+ assert(trackData.startTimestampOffset !== null);
31529
31629
  trackData.currentChunk.offset = currentPos;
31530
31630
  trackData.currentChunk.moofOffset = moofOffset;
31531
31631
  trackData.currentChunk.trafIndex = i;
31632
+ trackData.currentChunk.startTimestamp -= trackData.startTimestampOffset;
31532
31633
  for (const sample of trackData.currentChunk.samples) {
31533
31634
  currentPos += sample.size;
31635
+ sample.timestamp -= trackData.startTimestampOffset;
31636
+ sample.decodeTimestamp -= trackData.startTimestampOffset;
31534
31637
  }
31535
31638
  fragmentStartTimestamp = Math.min(fragmentStartTimestamp, trackData.currentChunk.startTimestamp);
31536
31639
  }
@@ -31832,6 +31935,7 @@ var Mediabunny = (() => {
31832
31935
  this.trackDatasInCurrentCluster = /* @__PURE__ */ new Map();
31833
31936
  this.startTimestamp = Infinity;
31834
31937
  this.endTimestamp = -Infinity;
31938
+ this.warnedAboutTooNegativeTimestamp = false;
31835
31939
  this.format = format;
31836
31940
  }
31837
31941
  async start() {
@@ -32629,6 +32733,13 @@ ${cue.notes ?? ""}`;
32629
32733
  }
32630
32734
  const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp;
32631
32735
  if (relativeTimestamp < MIN_CLUSTER_TIMESTAMP_MS) {
32736
+ if (!this.warnedAboutTooNegativeTimestamp) {
32737
+ const formatName = this.format instanceof WebMOutputFormat ? "WebM" : "Matroska";
32738
+ Logging._warn(
32739
+ `Packets had to be discarded because their timestamp is too negative to represent in ${formatName}.`
32740
+ );
32741
+ this.warnedAboutTooNegativeTimestamp = true;
32742
+ }
32632
32743
  return;
32633
32744
  }
32634
32745
  const prelude = new Uint8Array(4);
@@ -32677,6 +32788,7 @@ ${cue.notes ?? ""}`;
32677
32788
  }
32678
32789
  /** Creates a new Cluster element to contain media chunks. */
32679
32790
  createNewCluster(msTimestamp) {
32791
+ msTimestamp = Math.max(0, msTimestamp);
32680
32792
  if (this.currentCluster) {
32681
32793
  this.finalizeCurrentCluster();
32682
32794
  }
@@ -32721,7 +32833,8 @@ ${cue.notes ?? ""}`;
32721
32833
  for (const [msTimestamp, trackDatas] of groupedAndSortedByTimestamp) {
32722
32834
  assert(this.cues);
32723
32835
  this.cues.data.push({ id: 187 /* CuePoint */, data: [
32724
- { id: 179 /* CueTime */, data: msTimestamp },
32836
+ { id: 179 /* CueTime */, data: Math.max(0, msTimestamp) },
32837
+ // CueTime is unsigned
32725
32838
  // Create CueTrackPositions for each track that starts at this timestamp
32726
32839
  ...trackDatas.map((trackData) => {
32727
32840
  return { id: 183 /* CueTrackPositions */, data: [
@@ -33780,24 +33893,26 @@ ${cue.notes ?? ""}`;
33780
33893
  pesView.setUint8(6, 132);
33781
33894
  pesView.setUint8(7, includeDts ? 192 : 128);
33782
33895
  pesView.setUint8(8, headerDataLength);
33783
- const pts = Math.round(queuedPacket.presentationTimestamp * TIMESCALE);
33896
+ const unwrappedPts = Math.round(queuedPacket.presentationTimestamp * TIMESCALE);
33897
+ const pts = modEuclid(unwrappedPts, TIMESTAMP_MODULUS);
33784
33898
  ptsDtsBitstream.pos = 0;
33785
33899
  ptsDtsBitstream.writeBits(4, includeDts ? 3 : 2);
33786
- ptsDtsBitstream.writeBits(3, pts >>> 30 & 7);
33900
+ ptsDtsBitstream.writeBits(3, Math.floor(pts / 2 ** 30));
33787
33901
  ptsDtsBitstream.writeBits(1, 1);
33788
- ptsDtsBitstream.writeBits(15, pts >>> 15 & 32767);
33902
+ ptsDtsBitstream.writeBits(15, Math.floor(pts / 2 ** 15) % 2 ** 15);
33789
33903
  ptsDtsBitstream.writeBits(1, 1);
33790
- ptsDtsBitstream.writeBits(15, pts & 32767);
33904
+ ptsDtsBitstream.writeBits(15, pts % 2 ** 15);
33791
33905
  ptsDtsBitstream.writeBits(1, 1);
33792
33906
  if (includeDts) {
33793
33907
  assert(queuedPacket.decodeTimestamp !== null);
33794
- const dts = Math.round(queuedPacket.decodeTimestamp * TIMESCALE);
33908
+ const unwrappedDts = Math.round(queuedPacket.decodeTimestamp * TIMESCALE);
33909
+ const dts = modEuclid(unwrappedDts, TIMESTAMP_MODULUS);
33795
33910
  ptsDtsBitstream.writeBits(4, 1);
33796
- ptsDtsBitstream.writeBits(3, dts >>> 30 & 7);
33911
+ ptsDtsBitstream.writeBits(3, Math.floor(dts / 2 ** 30));
33797
33912
  ptsDtsBitstream.writeBits(1, 1);
33798
- ptsDtsBitstream.writeBits(15, dts >>> 15 & 32767);
33913
+ ptsDtsBitstream.writeBits(15, Math.floor(dts / 2 ** 15) % 2 ** 15);
33799
33914
  ptsDtsBitstream.writeBits(1, 1);
33800
- ptsDtsBitstream.writeBits(15, dts & 32767);
33915
+ ptsDtsBitstream.writeBits(15, dts % 2 ** 15);
33801
33916
  ptsDtsBitstream.writeBits(1, 1);
33802
33917
  }
33803
33918
  const totalLength = pesHeaderBuffer.length + queuedPacket.data.length;
@@ -35384,8 +35499,8 @@ ${cue.notes ?? ""}`;
35384
35499
  * to respect writer and encoder backpressure.
35385
35500
  */
35386
35501
  add(timestamp, duration = 0, encodeOptions) {
35387
- if (!Number.isFinite(timestamp) || timestamp < 0) {
35388
- throw new TypeError("timestamp must be a non-negative number.");
35502
+ if (!Number.isFinite(timestamp)) {
35503
+ throw new TypeError("timestamp must be a finite number.");
35389
35504
  }
35390
35505
  if (!Number.isFinite(duration) || duration < 0) {
35391
35506
  throw new TypeError("duration must be a non-negative number.");
@@ -36230,19 +36345,24 @@ ${cue.notes ?? ""}`;
36230
36345
  var AudioBufferSource = class extends AudioSource {
36231
36346
  /**
36232
36347
  * Creates a new {@link AudioBufferSource} whose `AudioBuffer` instances are encoded according to the specified
36233
- * {@link AudioEncodingConfig}.
36348
+ * {@link AudioEncodingConfig} and {@link AudioBufferSourceOptions}.
36234
36349
  */
36235
- constructor(encodingConfig) {
36350
+ constructor(encodingConfig, options = {}) {
36236
36351
  validateAudioEncodingConfig(encodingConfig);
36352
+ if (typeof options !== "object" || !options) {
36353
+ throw new TypeError("options must be an object.");
36354
+ }
36355
+ if (options.startTimestamp !== void 0 && !Number.isFinite(options.startTimestamp)) {
36356
+ throw new TypeError("options.startTimestamp, when provided, must be a finite number.");
36357
+ }
36237
36358
  super(encodingConfig.codec);
36238
- /** @internal */
36239
- this._accumulatedTime = 0;
36240
36359
  this._encoder = new AudioEncoderWrapper(this, encodingConfig);
36360
+ this._accumulatedTime = options.startTimestamp ?? 0;
36241
36361
  }
36242
36362
  /**
36243
- * Converts an AudioBuffer to audio samples, encodes them and adds them to the output. The first AudioBuffer will
36244
- * be played at timestamp 0, and any subsequent AudioBuffer will have a timestamp equal to the total duration of
36245
- * all previous AudioBuffers.
36363
+ * Converts an AudioBuffer to audio samples, encodes them and adds them to the output. The first `AudioBuffer` will
36364
+ * be played at the configured start timestamp (the default is 0), and each subsequent `AudioBuffer` will be placed
36365
+ * directly after the previous one.
36246
36366
  *
36247
36367
  * @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
36248
36368
  * to respect writer and encoder backpressure.
@@ -37824,6 +37944,9 @@ ${cue.notes ?? ""}`;
37824
37944
  get supportsTimestampedMediaData() {
37825
37945
  return true;
37826
37946
  }
37947
+ get negativeTimestampSupport() {
37948
+ return "full";
37949
+ }
37827
37950
  /** @internal */
37828
37951
  _createMuxer(output) {
37829
37952
  return new IsobmffMuxer2(output, this);
@@ -37998,6 +38121,9 @@ ${cue.notes ?? ""}`;
37998
38121
  get supportsTimestampedMediaData() {
37999
38122
  return true;
38000
38123
  }
38124
+ get negativeTimestampSupport() {
38125
+ return "prefer-non-negative";
38126
+ }
38001
38127
  };
38002
38128
  var WebMOutputFormat = class extends MkvOutputFormat2 {
38003
38129
  /** Creates a new {@link WebMOutputFormat} configured with the specified `options`. */
@@ -38075,6 +38201,9 @@ ${cue.notes ?? ""}`;
38075
38201
  get supportsTimestampedMediaData() {
38076
38202
  return false;
38077
38203
  }
38204
+ get negativeTimestampSupport() {
38205
+ return null;
38206
+ }
38078
38207
  };
38079
38208
  var WavOutputFormat = class extends OutputFormat {
38080
38209
  /** Creates a new {@link WavOutputFormat} configured with the specified `options`. */
@@ -38129,6 +38258,9 @@ ${cue.notes ?? ""}`;
38129
38258
  get supportsTimestampedMediaData() {
38130
38259
  return false;
38131
38260
  }
38261
+ get negativeTimestampSupport() {
38262
+ return null;
38263
+ }
38132
38264
  };
38133
38265
  var OggOutputFormat = class extends OutputFormat {
38134
38266
  /** Creates a new {@link OggOutputFormat} configured with the specified `options`. */
@@ -38179,6 +38311,9 @@ ${cue.notes ?? ""}`;
38179
38311
  get supportsTimestampedMediaData() {
38180
38312
  return false;
38181
38313
  }
38314
+ get negativeTimestampSupport() {
38315
+ return null;
38316
+ }
38182
38317
  };
38183
38318
  var AdtsOutputFormat = class extends OutputFormat {
38184
38319
  /** Creates a new {@link AdtsOutputFormat} configured with the specified `options`. */
@@ -38223,6 +38358,9 @@ ${cue.notes ?? ""}`;
38223
38358
  get supportsTimestampedMediaData() {
38224
38359
  return false;
38225
38360
  }
38361
+ get negativeTimestampSupport() {
38362
+ return null;
38363
+ }
38226
38364
  };
38227
38365
  var FlacOutputFormat = class extends OutputFormat {
38228
38366
  /** Creates a new {@link FlacOutputFormat} configured with the specified `options`. */
@@ -38267,6 +38405,9 @@ ${cue.notes ?? ""}`;
38267
38405
  get supportsTimestampedMediaData() {
38268
38406
  return false;
38269
38407
  }
38408
+ get negativeTimestampSupport() {
38409
+ return null;
38410
+ }
38270
38411
  };
38271
38412
  var MpegTsOutputFormat = class extends OutputFormat {
38272
38413
  /** Creates a new {@link MpegTsOutputFormat} configured with the specified `options`. */
@@ -38317,6 +38458,9 @@ ${cue.notes ?? ""}`;
38317
38458
  get supportsTimestampedMediaData() {
38318
38459
  return true;
38319
38460
  }
38461
+ get negativeTimestampSupport() {
38462
+ return "prefer-non-negative";
38463
+ }
38320
38464
  };
38321
38465
  var HlsOutputFormat = class extends OutputFormat {
38322
38466
  /** Creates a new {@link HlsOutputFormat} configured with the specified `options`. */
@@ -38410,6 +38554,19 @@ ${cue.notes ?? ""}`;
38410
38554
  get supportsTimestampedMediaData() {
38411
38555
  return true;
38412
38556
  }
38557
+ get negativeTimestampSupport() {
38558
+ const formats = toArray(this._options.segmentFormat);
38559
+ if (formats.some((format) => format.negativeTimestampSupport === "none")) {
38560
+ return "none";
38561
+ }
38562
+ if (formats.some((format) => format.negativeTimestampSupport === "prefer-non-negative")) {
38563
+ return "prefer-non-negative";
38564
+ }
38565
+ if (formats.some((format) => format.negativeTimestampSupport === "full")) {
38566
+ return "full";
38567
+ }
38568
+ return null;
38569
+ }
38413
38570
  /** @internal */
38414
38571
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
38415
38572
  _codecUnsupportedHint(codec) {
@@ -39148,15 +39305,16 @@ ${cue.notes ?? ""}`;
39148
39305
  var Conversion = class _Conversion {
39149
39306
  /** Creates a new Conversion instance (duh). */
39150
39307
  constructor(options) {
39151
- /**
39152
- * The current state of the conversion.
39153
- *
39154
- * - `'idle'`: The conversion is not currently executing and isn't done; `execute` can be called.
39155
- * - `'executing'`: A call to `execute` is currently running.
39156
- * - `'canceled'`: The conversion has been canceled and can no longer be executed.
39157
- * - `'done'`: The conversion has run to completion. Subsequent calls to `execute` do nothing.
39158
- */
39159
- this.state = "idle";
39308
+ /** @internal */
39309
+ this._state = "idle";
39310
+ /** @internal */
39311
+ this._timestampOffset = 0;
39312
+ /** @internal */
39313
+ this._timestampOffsetAdjusted = false;
39314
+ /** @internal */
39315
+ this._copyTimestampPossible = /* @__PURE__ */ new Map();
39316
+ /** @internal */
39317
+ this._copyStartPackets = /* @__PURE__ */ new Map();
39160
39318
  /** @internal */
39161
39319
  this._nextOutputTrackId = 0;
39162
39320
  /** @internal */
@@ -39227,6 +39385,22 @@ ${cue.notes ?? ""}`;
39227
39385
  if (options.composable !== void 0 && typeof options.composable !== "boolean") {
39228
39386
  throw new TypeError("options.composable, when provided, must be a boolean.");
39229
39387
  }
39388
+ if (options.copy !== void 0 && options.copy !== false) {
39389
+ if (!options.copy || typeof options.copy !== "object") {
39390
+ throw new TypeError("options.copy, when provided, must be an object or false.");
39391
+ }
39392
+ if (options.copy.mode !== void 0 && !["forced", "preferred"].includes(options.copy.mode)) {
39393
+ throw new TypeError("options.copy.mode, when provided, must be 'forced' or 'preferred'.");
39394
+ }
39395
+ if (options.copy.shiftTolerance !== void 0 && (!isNumber(options.copy.shiftTolerance) || options.copy.shiftTolerance < 0)) {
39396
+ throw new TypeError("options.copy.shiftTolerance, when provided, must be a non-negative number.");
39397
+ }
39398
+ if (options.copy.boundaryPolicy !== void 0 && !["expand", "shrink"].includes(options.copy.boundaryPolicy)) {
39399
+ throw new TypeError(
39400
+ "options.copy.boundaryPolicy, when provided, must be 'expand' or 'shrink'."
39401
+ );
39402
+ }
39403
+ }
39230
39404
  const composable = options.composable ?? false;
39231
39405
  if (!composable) {
39232
39406
  if (options.output.tracks.length > 0 || Object.keys(options.output._metadataTags).length > 0 || options.output.state !== "pending") {
@@ -39268,8 +39442,8 @@ ${cue.notes ?? ""}`;
39268
39442
  if (options.trim?.start !== void 0 && !Number.isFinite(options.trim.start)) {
39269
39443
  throw new TypeError("options.trim.start, when provided, must be a finite number.");
39270
39444
  }
39271
- if (options.trim?.end !== void 0 && !Number.isFinite(options.trim.end)) {
39272
- throw new TypeError("options.trim.end, when provided, must be a finite number.");
39445
+ if (options.trim?.end !== void 0 && !isNumber(options.trim.end)) {
39446
+ throw new TypeError("options.trim.end, when provided, must be a number.");
39273
39447
  }
39274
39448
  if (options.trim?.start !== void 0 && options.trim.end !== void 0 && options.trim.start >= options.trim.end) {
39275
39449
  throw new TypeError("options.trim.start must be less than options.trim.end.");
@@ -39284,10 +39458,24 @@ ${cue.notes ?? ""}`;
39284
39458
  throw new TypeError("options.showWarnings, when provided, must be a boolean.");
39285
39459
  }
39286
39460
  this._options = options;
39461
+ this._copyMode = options.copy === false ? false : options.copy?.mode ?? "preferred";
39462
+ this._copyTimestampShiftTolerance = options.copy === false ? 0 : options.copy?.shiftTolerance ?? 0;
39463
+ this._copyBoundaryPolicy = options.copy === false ? "expand" : options.copy?.boundaryPolicy ?? "expand";
39287
39464
  this._composable = composable;
39288
39465
  this.input = options.input;
39289
39466
  this.output = options.output;
39290
39467
  }
39468
+ /**
39469
+ * The current state of the conversion.
39470
+ *
39471
+ * - `'idle'`: The conversion is not currently executing and isn't done; `execute` can be called.
39472
+ * - `'executing'`: A call to `execute` is currently running.
39473
+ * - `'canceled'`: The conversion has been canceled and can no longer be executed.
39474
+ * - `'done'`: The conversion has run to completion. Subsequent calls to `execute` do nothing.
39475
+ */
39476
+ get state() {
39477
+ return this._state;
39478
+ }
39291
39479
  /** Initializes a new conversion process without starting the conversion. */
39292
39480
  static async init(options) {
39293
39481
  const conversion = new _Conversion(options);
@@ -39394,6 +39582,7 @@ ${cue.notes ?? ""}`;
39394
39582
  );
39395
39583
  }
39396
39584
  this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
39585
+ this._timestampOffset = -this._startTimestamp;
39397
39586
  for (let i = 0; i < filteredTracks.length; i++) {
39398
39587
  const track = filteredTracks[i];
39399
39588
  const options = filteredTrackOptions[i];
@@ -39572,13 +39761,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39572
39761
  "Cannot execute this conversion because its output configuration is invalid. Make sure to always check the isValid field before executing a conversion.\n" + this._getInvalidityExplanation().join("")
39573
39762
  );
39574
39763
  }
39575
- if (this.state === "executing") {
39764
+ if (this._state === "executing") {
39576
39765
  throw new Error("Cannot call execute() while a previous call to execute() is still running.");
39577
39766
  }
39578
- if (this.state === "canceled") {
39767
+ if (this._state === "canceled") {
39579
39768
  throw new ConversionCanceledError();
39580
39769
  }
39581
- if (this.state === "done") {
39770
+ if (this._state === "done") {
39582
39771
  return;
39583
39772
  }
39584
39773
  if (this._composable && this.output.state === "pending") {
@@ -39586,11 +39775,11 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39586
39775
  "A composable conversion requires the output to be started. Call start() on the output before executing the conversion."
39587
39776
  );
39588
39777
  }
39589
- this.state = "executing";
39778
+ this._state = "executing";
39590
39779
  this._executionUntil = options.until ?? Infinity;
39591
39780
  this._pauseRequested = options.pauseSignal?.aborted ?? false;
39592
39781
  const onPause = () => {
39593
- if (this.state !== "executing") {
39782
+ if (this._state !== "executing") {
39594
39783
  return;
39595
39784
  }
39596
39785
  this._pauseRequested = true;
@@ -39640,18 +39829,18 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39640
39829
  try {
39641
39830
  await Promise.all(this._trackPumps.map((x) => x.resolvers.promise));
39642
39831
  } catch (error) {
39643
- if (this.state !== "canceled") {
39832
+ if (this._state !== "canceled") {
39644
39833
  void this.cancel();
39645
39834
  }
39646
39835
  throw error;
39647
39836
  } finally {
39648
39837
  options.pauseSignal?.removeEventListener("abort", onPause);
39649
39838
  }
39650
- if (this.state === "canceled") {
39839
+ if (this._state === "canceled") {
39651
39840
  throw new ConversionCanceledError();
39652
39841
  }
39653
39842
  const isDone = this._trackPumps.every((x) => x.done);
39654
- this.state = isDone ? "done" : "idle";
39843
+ this._state = isDone ? "done" : "idle";
39655
39844
  if (isDone) {
39656
39845
  if (!this._composable) {
39657
39846
  await this.output.finalize();
@@ -39667,14 +39856,14 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39667
39856
  * Does nothing if the conversion is already complete.
39668
39857
  */
39669
39858
  async cancel() {
39670
- if (this.state === "done") {
39859
+ if (this._state === "done") {
39671
39860
  return;
39672
39861
  }
39673
- if (this.state === "canceled") {
39862
+ if (this._state === "canceled") {
39674
39863
  Logging._warn("Conversion already canceled.");
39675
39864
  return;
39676
39865
  }
39677
- this.state = "canceled";
39866
+ this._state = "canceled";
39678
39867
  for (const pump of this._trackPumps) {
39679
39868
  pump.wake?.();
39680
39869
  }
@@ -39720,10 +39909,63 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39720
39909
  width = ceilToMultipleOfTwo(trackOptions.width);
39721
39910
  height = ceilToMultipleOfTwo(trackOptions.height);
39722
39911
  }
39723
- const firstTimestamp = await track.getFirstTimestamp();
39724
39912
  let videoCodecs = this.output.format.getSupportedVideoCodecs();
39725
- const needsTranscode = !!trackOptions.forceTranscode || firstTimestamp < this._startTimestamp || !!trackOptions.frameRate || trackOptions.keyFrameInterval !== void 0 || trackOptions.process !== void 0 || trackOptions.quality !== void 0 || trackOptions.bitrate !== void 0 || !videoCodecs.includes(sourceCodec) || trackOptions.codec && trackOptions.codec !== sourceCodec || width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !canUseRotationMetadata || !!crop;
39726
39913
  const alpha = trackOptions.alpha ?? "discard";
39914
+ let needsTranscode = !this._copyMode || !!trackOptions.forceTranscode || !!trackOptions.frameRate || trackOptions.keyFrameInterval !== void 0 || trackOptions.process !== void 0 || trackOptions.quality !== void 0 || trackOptions.bitrate !== void 0 || !videoCodecs.includes(sourceCodec) || trackOptions.codec && trackOptions.codec !== sourceCodec || width !== originalWidth || height !== originalHeight || totalRotation !== 0 && !canUseRotationMetadata || !!crop;
39915
+ let copyStartPacket = null;
39916
+ if (!needsTranscode) {
39917
+ const sink = new EncodedPacketSink(track);
39918
+ let startPacket = await sink.getKeyPacket(this._startTimestamp, { verifyKeyPackets: true }) ?? await sink.getFirstKeyPacket({ verifyKeyPackets: true });
39919
+ if (startPacket && startPacket.timestamp < this._startTimestamp && startPacket.timestamp + startPacket.duration <= this._startTimestamp && this._copyBoundaryPolicy === "shrink") {
39920
+ startPacket = await sink.getNextKeyPacket(startPacket, { verifyKeyPackets: true });
39921
+ }
39922
+ copyStartPacket = startPacket;
39923
+ if (startPacket) {
39924
+ const effectiveStartTimestamp = this._copyBoundaryPolicy === "shrink" ? Math.max(startPacket.timestamp, this._startTimestamp) : startPacket.timestamp;
39925
+ if (!this.output.format.supportsTimestampedMediaData) {
39926
+ if (this._timestampOffsetAdjusted) {
39927
+ const isValid = effectiveStartTimestamp + this._timestampOffset === 0;
39928
+ if (!isValid) {
39929
+ needsTranscode = true;
39930
+ }
39931
+ } else {
39932
+ const correction = clamp(
39933
+ this._startTimestamp - effectiveStartTimestamp,
39934
+ -this._copyTimestampShiftTolerance,
39935
+ this._copyTimestampShiftTolerance
39936
+ );
39937
+ const shiftedStartTimestamp = effectiveStartTimestamp + correction;
39938
+ const isValid = shiftedStartTimestamp === this._startTimestamp;
39939
+ if (isValid) {
39940
+ this._timestampOffset = -this._startTimestamp + correction;
39941
+ this._timestampOffsetAdjusted = true;
39942
+ } else {
39943
+ needsTranscode = true;
39944
+ }
39945
+ }
39946
+ } else if (this.output.format.negativeTimestampSupport !== "full" && effectiveStartTimestamp < this._startTimestamp) {
39947
+ const correction = Math.min(
39948
+ this._startTimestamp - effectiveStartTimestamp,
39949
+ this._copyTimestampShiftTolerance
39950
+ );
39951
+ const shiftedStartTimestamp = effectiveStartTimestamp + correction;
39952
+ const isValid = shiftedStartTimestamp >= this._startTimestamp || this.output.format.negativeTimestampSupport === "prefer-non-negative" && this._copyMode === "forced";
39953
+ if (isValid) {
39954
+ this._timestampOffset = Math.max(this._timestampOffset, -this._startTimestamp + correction);
39955
+ } else {
39956
+ needsTranscode = true;
39957
+ }
39958
+ }
39959
+ }
39960
+ }
39961
+ if (needsTranscode && this._copyMode === "forced") {
39962
+ this.discardedTracks.push({
39963
+ track,
39964
+ reason: "cannot_copy",
39965
+ trackOptions
39966
+ });
39967
+ return;
39968
+ }
39727
39969
  if (!needsTranscode) {
39728
39970
  const source = new EncodedVideoPacketSource(sourceCodec);
39729
39971
  videoSource = source;
@@ -39731,18 +39973,51 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39731
39973
  const sink = new EncodedPacketSink(track);
39732
39974
  const decoderConfig = await track.getDecoderConfig();
39733
39975
  const meta = { decoderConfig: decoderConfig ?? void 0 };
39734
- for await (const packet of sink.packets(void 0, void 0, { verifyKeyPackets: true })) {
39735
- if (this.state === "canceled") {
39976
+ if (copyStartPacket) for await (const packet of sink.packets(
39977
+ copyStartPacket,
39978
+ void 0,
39979
+ { verifyKeyPackets: true }
39980
+ )) {
39981
+ if (this._state === "canceled") {
39736
39982
  break;
39737
39983
  }
39738
39984
  if (packet.timestamp >= this._endTimestamp) {
39739
- break;
39985
+ if (this._copyBoundaryPolicy === "shrink") {
39986
+ break;
39987
+ } else {
39988
+ let current = packet;
39989
+ let found = false;
39990
+ const lookahead = 6;
39991
+ for (let i = 0; i < lookahead; i++) {
39992
+ const next = await sink.getNextPacket(current, { metadataOnly: true });
39993
+ if (!next) {
39994
+ break;
39995
+ }
39996
+ if (next.timestamp < this._endTimestamp) {
39997
+ found = true;
39998
+ break;
39999
+ }
40000
+ current = next;
40001
+ }
40002
+ if (!found) {
40003
+ break;
40004
+ }
40005
+ }
39740
40006
  }
40007
+ let packetStartTimestamp = packet.timestamp;
40008
+ let packetEndTimestamp = packet.timestamp + packet.duration;
40009
+ if (this._copyBoundaryPolicy === "shrink") {
40010
+ packetStartTimestamp = Math.max(packetStartTimestamp, this._startTimestamp);
40011
+ packetEndTimestamp = Math.min(packetEndTimestamp, this._endTimestamp);
40012
+ packetEndTimestamp = Math.max(packetEndTimestamp, packetStartTimestamp);
40013
+ }
40014
+ packetStartTimestamp += this._timestampOffset;
40015
+ packetEndTimestamp += this._timestampOffset;
39741
40016
  const modifiedPacket = packet.clone({
39742
- timestamp: packet.timestamp - this._startTimestamp,
40017
+ timestamp: packetStartTimestamp,
40018
+ duration: packetEndTimestamp - packetStartTimestamp,
39743
40019
  sideData: alpha === "discard" ? {} : packet.sideData
39744
40020
  });
39745
- assert(modifiedPacket.timestamp >= 0);
39746
40021
  this._reportProgress(outputTrackId, modifiedPacket.timestamp + modifiedPacket.duration);
39747
40022
  await source.add(modifiedPacket, meta);
39748
40023
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
@@ -39803,7 +40078,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39803
40078
  tempOutput.addVideoTrack(tempSource);
39804
40079
  await tempOutput.start();
39805
40080
  const sink = new VideoSampleSink(track);
39806
- const firstSample = __using(_stack, await sink.getSample(firstTimestamp));
40081
+ const firstSample = __using(_stack, await sink.getSample(await track.getFirstTimestamp()));
39807
40082
  if (firstSample) {
39808
40083
  try {
39809
40084
  await tempSource.add(firstSample);
@@ -39854,11 +40129,16 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39854
40129
  var _stack2 = [];
39855
40130
  try {
39856
40131
  const sample = __using(_stack2, _sample);
39857
- if (this.state === "canceled") {
40132
+ if (this._state === "canceled") {
39858
40133
  break;
39859
40134
  }
39860
- const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
39861
- sample.setTimestamp(adjustedSampleTimestamp);
40135
+ const clampedStartTimestamp = Math.max(this._startTimestamp, sample.timestamp);
40136
+ const clampedEndTimestamp = Math.min(this._endTimestamp, sample.timestamp + sample.duration);
40137
+ if (clampedStartTimestamp >= clampedEndTimestamp) {
40138
+ continue;
40139
+ }
40140
+ sample.setTimestamp(clampedStartTimestamp + this._timestampOffset);
40141
+ sample.setDuration(clampedEndTimestamp - clampedStartTimestamp);
39862
40142
  this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
39863
40143
  await source.add(sample);
39864
40144
  sample.close();
@@ -39883,12 +40163,14 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39883
40163
  ownGroup = new OutputTrackGroup();
39884
40164
  }
39885
40165
  const videoTrackLanguageCode = await track.getLanguageCode();
40166
+ const trackName = await track.getName();
40167
+ const trackDisposition = await track.getDisposition();
39886
40168
  this.output.addVideoTrack(videoSource, {
39887
40169
  frameRate: trackOptions.frameRate,
39888
40170
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
39889
40171
  languageCode: isIso639Dash2LanguageCode(videoTrackLanguageCode) ? videoTrackLanguageCode : void 0,
39890
- name: await track.getName() ?? void 0,
39891
- disposition: await track.getDisposition(),
40172
+ name: trackName ?? void 0,
40173
+ disposition: trackDisposition,
39892
40174
  rotation: outputTrackRotation,
39893
40175
  group: ownGroup ?? trackOptions.group
39894
40176
  });
@@ -39910,30 +40192,93 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
39910
40192
  let audioSource;
39911
40193
  const originalNumberOfChannels = await track.getNumberOfChannels();
39912
40194
  const originalSampleRate = await track.getSampleRate();
39913
- const firstTimestamp = await track.getFirstTimestamp();
39914
40195
  let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
39915
40196
  let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
39916
- const needsTrimming = firstTimestamp < this._startTimestamp;
39917
- let needsPadding = firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
39918
40197
  let audioCodecs = this.output.format.getSupportedAudioCodecs();
39919
- if (!trackOptions.forceTranscode && !trackOptions.quality && !trackOptions.bitrate && numberOfChannels === originalNumberOfChannels && sampleRate === originalSampleRate && !needsTrimming && !needsPadding && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
40198
+ let needsTranscode = !this._copyMode || !!trackOptions.forceTranscode || !!trackOptions.quality || !!trackOptions.bitrate || numberOfChannels !== originalNumberOfChannels || sampleRate !== originalSampleRate || !audioCodecs.includes(sourceCodec) || !!trackOptions.codec && trackOptions.codec !== sourceCodec || trackOptions.process !== void 0 || trackOptions.sampleFormat !== void 0;
40199
+ let copyStartPacket = null;
40200
+ if (!needsTranscode) {
40201
+ const sink = new EncodedPacketSink(track);
40202
+ let startPacket = await sink.getKeyPacket(this._startTimestamp) ?? await sink.getFirstKeyPacket();
40203
+ if (startPacket && (this._copyBoundaryPolicy === "shrink" && startPacket.timestamp < this._startTimestamp || this._copyBoundaryPolicy === "expand" && startPacket.timestamp + startPacket.duration <= this._startTimestamp)) {
40204
+ startPacket = await sink.getNextKeyPacket(startPacket);
40205
+ }
40206
+ const hasDecoderWarmup = NON_PCM_AUDIO_CODECS.includes(sourceCodec) && sourceCodec !== "flac";
40207
+ if (startPacket && this._copyBoundaryPolicy === "expand" && hasDecoderWarmup) {
40208
+ const previousPacket = await sink.getKeyPacket(
40209
+ startPacket.timestamp - 1 / await track.getTimeResolution()
40210
+ );
40211
+ if (previousPacket) {
40212
+ startPacket = previousPacket;
40213
+ }
40214
+ }
40215
+ copyStartPacket = startPacket;
40216
+ if (startPacket) {
40217
+ if (!this.output.format.supportsTimestampedMediaData) {
40218
+ if (this._timestampOffsetAdjusted) {
40219
+ const isValid = startPacket.timestamp + this._timestampOffset === 0;
40220
+ if (!isValid) {
40221
+ needsTranscode = true;
40222
+ }
40223
+ } else {
40224
+ const correction = clamp(
40225
+ this._startTimestamp - startPacket.timestamp,
40226
+ -this._copyTimestampShiftTolerance,
40227
+ this._copyTimestampShiftTolerance
40228
+ );
40229
+ const shiftedStartTimestamp = startPacket.timestamp + correction;
40230
+ const isValid = shiftedStartTimestamp === this._startTimestamp;
40231
+ if (isValid) {
40232
+ this._timestampOffset = -this._startTimestamp + correction;
40233
+ this._timestampOffsetAdjusted = true;
40234
+ } else {
40235
+ needsTranscode = true;
40236
+ }
40237
+ }
40238
+ } else if (this.output.format.negativeTimestampSupport !== "full" && startPacket.timestamp < this._startTimestamp) {
40239
+ const correction = Math.min(
40240
+ this._startTimestamp - startPacket.timestamp,
40241
+ this._copyTimestampShiftTolerance
40242
+ );
40243
+ const shiftedStartTimestamp = startPacket.timestamp + correction;
40244
+ const isValid = shiftedStartTimestamp >= this._startTimestamp || this.output.format.negativeTimestampSupport === "prefer-non-negative" && this._copyMode === "forced";
40245
+ if (isValid) {
40246
+ this._timestampOffset = Math.max(this._timestampOffset, -this._startTimestamp + correction);
40247
+ } else {
40248
+ needsTranscode = true;
40249
+ }
40250
+ }
40251
+ }
40252
+ }
40253
+ if (needsTranscode && this._copyMode === "forced") {
40254
+ this.discardedTracks.push({
40255
+ track,
40256
+ reason: "cannot_copy",
40257
+ trackOptions
40258
+ });
40259
+ return;
40260
+ }
40261
+ if (!needsTranscode) {
39920
40262
  const source = new EncodedAudioPacketSource(sourceCodec);
39921
40263
  audioSource = source;
39922
40264
  this._registerTrackPump(async (pump) => {
39923
40265
  const sink = new EncodedPacketSink(track);
39924
40266
  const decoderConfig = await track.getDecoderConfig();
39925
40267
  const meta = { decoderConfig: decoderConfig ?? void 0 };
39926
- for await (const packet of sink.packets()) {
39927
- if (this.state === "canceled") {
40268
+ if (copyStartPacket) for await (const packet of sink.packets(copyStartPacket)) {
40269
+ if (this._state === "canceled") {
39928
40270
  break;
39929
40271
  }
39930
40272
  if (packet.timestamp >= this._endTimestamp) {
39931
40273
  break;
39932
40274
  }
40275
+ if (this._copyBoundaryPolicy === "shrink" && packet.timestamp + packet.duration > this._endTimestamp) {
40276
+ break;
40277
+ }
39933
40278
  const modifiedPacket = packet.clone({
39934
- timestamp: packet.timestamp - this._startTimestamp
40279
+ timestamp: packet.timestamp + this._timestampOffset,
40280
+ duration: packet.duration
39935
40281
  });
39936
- assert(modifiedPacket.timestamp >= 0);
39937
40282
  this._reportProgress(outputTrackId, modifiedPacket.timestamp + modifiedPacket.duration);
39938
40283
  await source.add(modifiedPacket, meta);
39939
40284
  if (this._synchronizer.shouldWait(outputTrackId, modifiedPacket.timestamp)) {
@@ -40009,6 +40354,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40009
40354
  const source = new AudioSampleSource(encodingConfig);
40010
40355
  audioSource = source;
40011
40356
  this._registerTrackPump(async (pump) => {
40357
+ let needsPadding = null;
40012
40358
  const sink = new AudioSampleSink(track);
40013
40359
  for await (var _sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
40014
40360
  var _stack3 = [];
@@ -40016,13 +40362,42 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40016
40362
  const sample = __using(_stack3, _sample);
40017
40363
  var _stack2 = [];
40018
40364
  try {
40019
- if (this.state === "canceled") {
40365
+ if (this._state === "canceled") {
40020
40366
  break;
40021
40367
  }
40368
+ let startFrame = 0;
40369
+ let endFrame = sample.numberOfFrames;
40370
+ if (sample.timestamp < this._startTimestamp) {
40371
+ startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
40372
+ }
40373
+ if (sample.timestamp + sample.duration > this._endTimestamp) {
40374
+ endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
40375
+ }
40376
+ if (startFrame >= endFrame) {
40377
+ sample.close();
40378
+ continue;
40379
+ }
40380
+ let finalSampleLet;
40381
+ if (startFrame > 0 || endFrame < sample.numberOfFrames) {
40382
+ const trimmedSample = sample.trim(startFrame, endFrame);
40383
+ sample.close();
40384
+ finalSampleLet = trimmedSample;
40385
+ if (trimmedSample.numberOfFrames === 0) {
40386
+ trimmedSample.close();
40387
+ continue;
40388
+ }
40389
+ } else {
40390
+ finalSampleLet = sample;
40391
+ }
40392
+ const finalSample = __using(_stack2, finalSampleLet);
40393
+ finalSample.setTimestamp(finalSample.timestamp + this._timestampOffset);
40394
+ if (needsPadding === null) {
40395
+ needsPadding = finalSample.timestamp > 0 && !this.output.format.supportsTimestampedMediaData;
40396
+ }
40022
40397
  if (needsPadding) {
40023
40398
  var _stack = [];
40024
40399
  try {
40025
- const paddingLength = firstTimestamp - this._startTimestamp;
40400
+ const paddingLength = finalSample.timestamp;
40026
40401
  const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
40027
40402
  const bytesPerSample = getBytesPerSample(sample.format);
40028
40403
  const data = new Uint8Array(bytesPerSample * paddingLengthSamples * originalNumberOfChannels);
@@ -40052,28 +40427,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40052
40427
  __callDispose(_stack, _error, _hasError);
40053
40428
  }
40054
40429
  }
40055
- let startFrame = 0;
40056
- let endFrame = sample.numberOfFrames;
40057
- if (sample.timestamp < this._startTimestamp) {
40058
- startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
40059
- }
40060
- if (sample.timestamp + sample.duration > this._endTimestamp) {
40061
- endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
40062
- }
40063
- let finalSampleLet;
40064
- if (startFrame > 0 || endFrame < sample.numberOfFrames) {
40065
- const trimmedSample = sample.trim(startFrame, endFrame);
40066
- sample.close();
40067
- finalSampleLet = trimmedSample;
40068
- if (trimmedSample.numberOfFrames === 0) {
40069
- trimmedSample.close();
40070
- continue;
40071
- }
40072
- } else {
40073
- finalSampleLet = sample;
40074
- }
40075
- const finalSample = __using(_stack2, finalSampleLet);
40076
- finalSample.setTimestamp(finalSample.timestamp - this._startTimestamp);
40077
40430
  await this._registerAudioSample(
40078
40431
  pump,
40079
40432
  finalSample,
@@ -40101,11 +40454,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40101
40454
  ownGroup = new OutputTrackGroup();
40102
40455
  }
40103
40456
  const audioTrackLanguageCode = await track.getLanguageCode();
40457
+ const trackName = await track.getName();
40458
+ const trackDisposition = await track.getDisposition();
40104
40459
  this.output.addAudioTrack(audioSource, {
40105
40460
  // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
40106
40461
  languageCode: isIso639Dash2LanguageCode(audioTrackLanguageCode) ? audioTrackLanguageCode : void 0,
40107
- name: await track.getName() ?? void 0,
40108
- disposition: await track.getDisposition(),
40462
+ name: trackName ?? void 0,
40463
+ disposition: trackDisposition,
40109
40464
  group: ownGroup ?? trackOptions.group
40110
40465
  });
40111
40466
  this.utilizedTracks.push(track);
@@ -40144,7 +40499,7 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40144
40499
  }
40145
40500
  /** @internal */
40146
40501
  async _checkpoint(pump, timestamp) {
40147
- while (this.state !== "canceled" && (timestamp >= this._executionUntil || this._pauseRequested)) {
40502
+ while (this._state !== "canceled" && (timestamp >= this._executionUntil || this._pauseRequested)) {
40148
40503
  pump.resolvers.resolve();
40149
40504
  const { promise, resolve } = promiseWithResolvers();
40150
40505
  pump.wake = resolve;
@@ -40185,14 +40540,14 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
40185
40540
  this.conversion = conversion;
40186
40541
  }
40187
40542
  declareTrack(trackId) {
40188
- this.maxTimestamps.set(trackId, 0);
40543
+ this.maxTimestamps.set(trackId, -Infinity);
40189
40544
  }
40190
40545
  shouldWait(trackId, timestamp) {
40191
40546
  const currentValue = this.maxTimestamps.get(trackId);
40192
40547
  assert(currentValue !== void 0);
40193
40548
  this.maxTimestamps.set(trackId, Math.max(timestamp, currentValue));
40194
40549
  const newMin = this.computeMinAndMaybeResolve();
40195
- if (this.conversion.state === "canceled" || this.conversion._pauseRequested || timestamp >= this.conversion._executionUntil) {
40550
+ if (this.conversion._state === "canceled" || this.conversion._pauseRequested || timestamp >= this.conversion._executionUntil) {
40196
40551
  return false;
40197
40552
  }
40198
40553
  return timestamp - newMin > MAX_TIMESTAMP_GAP;