mediabunny 1.46.0 → 1.48.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 (86) hide show
  1. package/dist/bundles/mediabunny.cjs +340 -215
  2. package/dist/bundles/mediabunny.min.cjs +11 -11
  3. package/dist/bundles/mediabunny.min.mjs +11 -11
  4. package/dist/bundles/mediabunny.mjs +340 -215
  5. package/dist/bundles/mediabunny.node.cjs +340 -215
  6. package/dist/mediabunny.d.ts +82 -4
  7. package/dist/modules/src/adts/adts-demuxer.d.ts +1 -0
  8. package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
  9. package/dist/modules/src/adts/adts-demuxer.js +3 -0
  10. package/dist/modules/src/conversion.d.ts.map +1 -1
  11. package/dist/modules/src/conversion.js +91 -163
  12. package/dist/modules/src/encode.d.ts +4 -0
  13. package/dist/modules/src/encode.d.ts.map +1 -1
  14. package/dist/modules/src/encode.js +6 -0
  15. package/dist/modules/src/flac/flac-demuxer.d.ts +1 -0
  16. package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
  17. package/dist/modules/src/flac/flac-demuxer.js +3 -0
  18. package/dist/modules/src/hls/hls-demuxer.d.ts.map +1 -1
  19. package/dist/modules/src/hls/hls-demuxer.js +6 -1
  20. package/dist/modules/src/hls/hls-segmented-input.d.ts +1 -0
  21. package/dist/modules/src/hls/hls-segmented-input.d.ts.map +1 -1
  22. package/dist/modules/src/hls/hls-segmented-input.js +28 -11
  23. package/dist/modules/src/index.d.ts +2 -2
  24. package/dist/modules/src/index.d.ts.map +1 -1
  25. package/dist/modules/src/input-format.d.ts +21 -0
  26. package/dist/modules/src/input-format.d.ts.map +1 -1
  27. package/dist/modules/src/input-format.js +9 -0
  28. package/dist/modules/src/input-track.d.ts +17 -0
  29. package/dist/modules/src/input-track.d.ts.map +1 -1
  30. package/dist/modules/src/input-track.js +20 -0
  31. package/dist/modules/src/input.d.ts.map +1 -1
  32. package/dist/modules/src/input.js +6 -2
  33. package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
  34. package/dist/modules/src/isobmff/isobmff-demuxer.js +11 -7
  35. package/dist/modules/src/matroska/matroska-demuxer.js +3 -0
  36. package/dist/modules/src/media-sink.d.ts +20 -1
  37. package/dist/modules/src/media-sink.d.ts.map +1 -1
  38. package/dist/modules/src/media-sink.js +26 -3
  39. package/dist/modules/src/media-source.d.ts.map +1 -1
  40. package/dist/modules/src/media-source.js +14 -2
  41. package/dist/modules/src/mp3/mp3-demuxer.d.ts +1 -0
  42. package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
  43. package/dist/modules/src/mp3/mp3-demuxer.js +3 -0
  44. package/dist/modules/src/mpeg-ts/mpeg-ts-demuxer.js +3 -0
  45. package/dist/modules/src/ogg/ogg-demuxer.d.ts +1 -0
  46. package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
  47. package/dist/modules/src/ogg/ogg-demuxer.js +3 -0
  48. package/dist/modules/src/resample.d.ts +1 -4
  49. package/dist/modules/src/resample.d.ts.map +1 -1
  50. package/dist/modules/src/resample.js +9 -9
  51. package/dist/modules/src/sample.d.ts +6 -0
  52. package/dist/modules/src/sample.d.ts.map +1 -1
  53. package/dist/modules/src/sample.js +62 -0
  54. package/dist/modules/src/segmented-input.d.ts +7 -1
  55. package/dist/modules/src/segmented-input.d.ts.map +1 -1
  56. package/dist/modules/src/segmented-input.js +13 -1
  57. package/dist/modules/src/source.d.ts +13 -3
  58. package/dist/modules/src/source.d.ts.map +1 -1
  59. package/dist/modules/src/source.js +19 -3
  60. package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
  61. package/dist/modules/src/wave/wave-demuxer.d.ts +1 -0
  62. package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
  63. package/dist/modules/src/wave/wave-demuxer.js +3 -0
  64. package/package.json +1 -1
  65. package/src/adts/adts-demuxer.ts +4 -0
  66. package/src/conversion.ts +108 -215
  67. package/src/encode.ts +10 -0
  68. package/src/flac/flac-demuxer.ts +4 -0
  69. package/src/hls/hls-demuxer.ts +8 -1
  70. package/src/hls/hls-segmented-input.ts +32 -12
  71. package/src/index.ts +2 -0
  72. package/src/input-format.ts +33 -0
  73. package/src/input-track.ts +23 -0
  74. package/src/input.ts +6 -2
  75. package/src/isobmff/isobmff-demuxer.ts +13 -7
  76. package/src/matroska/matroska-demuxer.ts +4 -0
  77. package/src/media-sink.ts +54 -3
  78. package/src/media-source.ts +20 -2
  79. package/src/mp3/mp3-demuxer.ts +4 -0
  80. package/src/mpeg-ts/mpeg-ts-demuxer.ts +4 -0
  81. package/src/ogg/ogg-demuxer.ts +4 -0
  82. package/src/resample.ts +10 -15
  83. package/src/sample.ts +68 -0
  84. package/src/segmented-input.ts +23 -2
  85. package/src/source.ts +26 -5
  86. package/src/wave/wave-demuxer.ts +4 -0
@@ -6890,35 +6890,39 @@ var Mediabunny = (() => {
6890
6890
  this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize));
6891
6891
  if (this.currentTrack) {
6892
6892
  const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
6893
- if (trackData) {
6894
- this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
6895
- trackData.presentationTimestamps = trackData.samples.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i })).sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
6896
- for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
6897
- const currentEntry = trackData.presentationTimestamps[i];
6898
- const currentSample = trackData.samples[currentEntry.sampleIndex];
6899
- if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
6900
- trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
6893
+ cond:
6894
+ if (trackData) {
6895
+ if (trackData.samples.length === 0) {
6896
+ this.currentFragment.trackData.delete(this.currentTrack.id);
6897
+ break cond;
6901
6898
  }
6902
- if (i < trackData.presentationTimestamps.length - 1) {
6903
- const nextEntry = trackData.presentationTimestamps[i + 1];
6904
- const duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
6905
- currentSample.duration = duration;
6899
+ trackData.presentationTimestamps = trackData.samples.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i })).sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
6900
+ for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
6901
+ const currentEntry = trackData.presentationTimestamps[i];
6902
+ const currentSample = trackData.samples[currentEntry.sampleIndex];
6903
+ if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
6904
+ trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
6905
+ }
6906
+ if (i < trackData.presentationTimestamps.length - 1) {
6907
+ const nextEntry = trackData.presentationTimestamps[i + 1];
6908
+ const duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
6909
+ currentSample.duration = duration;
6910
+ }
6911
+ }
6912
+ const firstSample = trackData.samples[trackData.presentationTimestamps[0].sampleIndex];
6913
+ const lastSample = trackData.samples[last(trackData.presentationTimestamps).sampleIndex];
6914
+ trackData.startTimestamp = firstSample.presentationTimestamp;
6915
+ trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
6916
+ const { currentFragmentState } = this.currentTrack;
6917
+ assert(currentFragmentState);
6918
+ if (currentFragmentState.startTimestamp !== null) {
6919
+ offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp);
6920
+ trackData.startTimestampIsFinal = true;
6921
+ }
6922
+ if (currentFragmentState.encryptionAuxInfo && !trackData.samples[0].encryption) {
6923
+ trackData.encryptionAuxInfo = currentFragmentState.encryptionAuxInfo;
6906
6924
  }
6907
6925
  }
6908
- const firstSample = trackData.samples[trackData.presentationTimestamps[0].sampleIndex];
6909
- const lastSample = trackData.samples[last(trackData.presentationTimestamps).sampleIndex];
6910
- trackData.startTimestamp = firstSample.presentationTimestamp;
6911
- trackData.endTimestamp = lastSample.presentationTimestamp + lastSample.duration;
6912
- const { currentFragmentState } = this.currentTrack;
6913
- assert(currentFragmentState);
6914
- if (currentFragmentState.startTimestamp !== null) {
6915
- offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp);
6916
- trackData.startTimestampIsFinal = true;
6917
- }
6918
- if (currentFragmentState.encryptionAuxInfo && !trackData.samples[0].encryption) {
6919
- trackData.encryptionAuxInfo = currentFragmentState.encryptionAuxInfo;
6920
- }
6921
- }
6922
6926
  this.currentTrack.currentFragmentState = null;
6923
6927
  this.currentTrack = null;
6924
6928
  }
@@ -7051,10 +7055,6 @@ var Mediabunny = (() => {
7051
7055
  };
7052
7056
  this.currentFragment.trackData.set(track.id, trackData);
7053
7057
  }
7054
- if (sampleCount === 0) {
7055
- this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
7056
- break;
7057
- }
7058
7058
  for (let i = 0; i < sampleCount; i++) {
7059
7059
  let sampleDuration;
7060
7060
  if (sampleDurationPresent) {
@@ -7100,6 +7100,7 @@ var Mediabunny = (() => {
7100
7100
  trackData.currentOffset += sampleSize;
7101
7101
  trackData.currentTimestamp += sampleDuration;
7102
7102
  }
7103
+ this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
7103
7104
  }
7104
7105
  ;
7105
7106
  break;
@@ -7577,6 +7578,9 @@ var Mediabunny = (() => {
7577
7578
  isRelativeToUnixEpoch() {
7578
7579
  return false;
7579
7580
  }
7581
+ getUnixTimeForTimestamp() {
7582
+ return null;
7583
+ }
7580
7584
  getDisposition() {
7581
7585
  return this.internalTrack.disposition;
7582
7586
  }
@@ -10454,6 +10458,9 @@ var Mediabunny = (() => {
10454
10458
  isRelativeToUnixEpoch() {
10455
10459
  return false;
10456
10460
  }
10461
+ getUnixTimeForTimestamp() {
10462
+ return null;
10463
+ }
10457
10464
  getDisposition() {
10458
10465
  return this.internalTrack.disposition;
10459
10466
  }
@@ -11074,6 +11081,9 @@ var Mediabunny = (() => {
11074
11081
  isRelativeToUnixEpoch() {
11075
11082
  return false;
11076
11083
  }
11084
+ getUnixTimeForTimestamp() {
11085
+ return null;
11086
+ }
11077
11087
  getPairingMask() {
11078
11088
  return 1n;
11079
11089
  }
@@ -11642,6 +11652,9 @@ var Mediabunny = (() => {
11642
11652
  isRelativeToUnixEpoch() {
11643
11653
  return false;
11644
11654
  }
11655
+ getUnixTimeForTimestamp() {
11656
+ return null;
11657
+ }
11645
11658
  getPairingMask() {
11646
11659
  return 1n;
11647
11660
  }
@@ -12414,6 +12427,9 @@ var Mediabunny = (() => {
12414
12427
  isRelativeToUnixEpoch() {
12415
12428
  return false;
12416
12429
  }
12430
+ getUnixTimeForTimestamp() {
12431
+ return null;
12432
+ }
12417
12433
  getPairingMask() {
12418
12434
  return 1n;
12419
12435
  }
@@ -12712,6 +12728,9 @@ var Mediabunny = (() => {
12712
12728
  isRelativeToUnixEpoch() {
12713
12729
  return false;
12714
12730
  }
12731
+ getUnixTimeForTimestamp() {
12732
+ return null;
12733
+ }
12715
12734
  getPairingMask() {
12716
12735
  return 1n;
12717
12736
  }
@@ -13332,6 +13351,9 @@ var Mediabunny = (() => {
13332
13351
  isRelativeToUnixEpoch() {
13333
13352
  return false;
13334
13353
  }
13354
+ getUnixTimeForTimestamp() {
13355
+ return null;
13356
+ }
13335
13357
  getPairingMask() {
13336
13358
  return 1n;
13337
13359
  }
@@ -14200,6 +14222,9 @@ var Mediabunny = (() => {
14200
14222
  isRelativeToUnixEpoch() {
14201
14223
  return false;
14202
14224
  }
14225
+ getUnixTimeForTimestamp() {
14226
+ return null;
14227
+ }
14203
14228
  getPairingMask() {
14204
14229
  return 1n;
14205
14230
  }
@@ -15238,6 +15263,15 @@ var Mediabunny = (() => {
15238
15263
  }
15239
15264
  return lastSegment.timestamp + lastSegment.duration;
15240
15265
  }
15266
+ async getUnixTimeForTimestamp(timestamp) {
15267
+ let segment = await this.getSegmentAt(timestamp, {});
15268
+ segment ??= await this.getFirstSegment({});
15269
+ if (!segment || segment.unixEpochTimestamp === null) {
15270
+ return null;
15271
+ }
15272
+ const elapsed = timestamp - segment.timestamp;
15273
+ return segment.unixEpochTimestamp + elapsed;
15274
+ }
15241
15275
  async getTrackBackings() {
15242
15276
  return this.trackBackingsPromise ??= (async () => {
15243
15277
  const backings = [];
@@ -15395,7 +15429,10 @@ var Mediabunny = (() => {
15395
15429
  async isRelativeToUnixEpoch() {
15396
15430
  await this.hydrate();
15397
15431
  assert(this.segmentedInput.firstSegment);
15398
- return this.segmentedInput.firstSegment.relativeToUnixEpoch;
15432
+ return this.segmentedInput.firstSegment.unixEpochTimestamp === this.segmentedInput.firstSegment.timestamp;
15433
+ }
15434
+ getUnixTimeForTimestamp(timestamp) {
15435
+ return this.segmentedInput.getUnixTimeForTimestamp(timestamp);
15399
15436
  }
15400
15437
  getBitrate() {
15401
15438
  return this.delegate(() => this.firstInputTrack._backing.getBitrate());
@@ -15985,7 +16022,10 @@ var Mediabunny = (() => {
15985
16022
  throw new TypeError("options.fetchFn, when provided, must be a function.");
15986
16023
  }
15987
16024
  const urlString = url2 instanceof Request ? url2.url : url2 instanceof URL ? url2.href : url2;
15988
- super(urlString, (request) => new _UrlSource(request.path, this._options));
16025
+ super(
16026
+ urlString,
16027
+ (request) => new _UrlSource(request.path, this._options)
16028
+ );
15989
16029
  /** @internal */
15990
16030
  this._offset = 0;
15991
16031
  /** @internal */
@@ -16090,6 +16130,9 @@ var Mediabunny = (() => {
16090
16130
  if (!response.ok) {
16091
16131
  throw new Error(`Error fetching ${String(this._url)}: ${response.status} ${response.statusText}`);
16092
16132
  }
16133
+ if (response.redirected) {
16134
+ this.rootPath = response.url;
16135
+ }
16093
16136
  outer:
16094
16137
  if (this._orchestrator.fileSize === null) {
16095
16138
  const contentRange = response.headers.get("Content-Range");
@@ -16750,6 +16793,12 @@ var Mediabunny = (() => {
16750
16793
  offset: innerStart
16751
16794
  });
16752
16795
  } else {
16796
+ promise.catch((error) => {
16797
+ if (this.disposed) {
16798
+ return;
16799
+ }
16800
+ throw error;
16801
+ });
16753
16802
  }
16754
16803
  return result;
16755
16804
  }
@@ -16834,11 +16883,11 @@ var Mediabunny = (() => {
16834
16883
  if (worker.pendingSlices.length > 0) {
16835
16884
  worker.pendingSlices.forEach((x) => x.reject(error));
16836
16885
  worker.pendingSlices.length = 0;
16837
- } else {
16886
+ } else if (!worker.aborted && !this.disposed) {
16838
16887
  throw error;
16839
16888
  }
16840
16889
  }).finally(() => {
16841
- if (worker.running) {
16890
+ if (worker.running || this.workers.length >= this.options.maxWorkerCount) {
16842
16891
  return;
16843
16892
  }
16844
16893
  if (this.queuedReads.length > 0) {
@@ -17151,6 +17200,7 @@ var Mediabunny = (() => {
17151
17200
  this.streamHasEnded = false;
17152
17201
  this.lastSegmentUpdateTime = -Infinity;
17153
17202
  this.refreshInterval = 5;
17203
+ this.rootPath = path;
17154
17204
  this.demuxer = demuxer;
17155
17205
  this.nextLines = lines;
17156
17206
  }
@@ -17186,19 +17236,24 @@ var Mediabunny = (() => {
17186
17236
  if (!lines) {
17187
17237
  var _stack = [];
17188
17238
  try {
17189
- const ref = __using(_stack, await this.demuxer.input._getSourceUncached({ path: this.path, isRoot: false }));
17239
+ const ref = __using(_stack, await this.demuxer.input._getSourceUncached({ path: this.rootPath, isRoot: false }));
17190
17240
  const reader = new Reader12(ref.source);
17191
17241
  const slice = await reader.requestEntireFile();
17192
17242
  assert(slice);
17193
17243
  lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
17244
+ if (ref.source instanceof PathedSource) {
17245
+ this.rootPath = ref.source.rootPath;
17246
+ }
17194
17247
  } catch (_) {
17195
17248
  var _error = _, _hasError = true;
17196
17249
  } finally {
17197
17250
  __callDispose(_stack, _error, _hasError);
17198
17251
  }
17199
17252
  }
17253
+ const offsetTimestampsByDateTime = this.input._formatOptions.hls?.offsetTimestampsByDateTime !== false;
17200
17254
  let headerRead = false;
17201
17255
  let accumulatedTime = 0;
17256
+ let accumulatedUnixTime = null;
17202
17257
  let nextSegmentDuration = null;
17203
17258
  let currentKey = null;
17204
17259
  let nextSequenceNumber = 0;
@@ -17234,6 +17289,7 @@ var Mediabunny = (() => {
17234
17289
  currentFirstSegment = prevLastSegment.firstSegment;
17235
17290
  currentInitSegment = prevLastSegment.initSegment;
17236
17291
  lastProgramDateTimeSeconds = prevLastSegment.lastProgramDateTimeSeconds;
17292
+ accumulatedUnixTime = prevLastSegment.unixEpochTimestamp !== null ? prevLastSegment.unixEpochTimestamp + prevLastSegment.duration : null;
17237
17293
  prevLastSegment = null;
17238
17294
  }
17239
17295
  }
@@ -17260,7 +17316,7 @@ var Mediabunny = (() => {
17260
17316
  view2.setUint32(12, nextSequenceNumber);
17261
17317
  key = { ...key, iv };
17262
17318
  }
17263
- const fullPath = joinPaths(this.path, line);
17319
+ const fullPath = joinPaths(this.rootPath, line);
17264
17320
  const location = {
17265
17321
  path: fullPath,
17266
17322
  offset: nextByteRange?.offset ?? 0,
@@ -17268,7 +17324,7 @@ var Mediabunny = (() => {
17268
17324
  };
17269
17325
  const segment = {
17270
17326
  timestamp: accumulatedTime,
17271
- relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
17327
+ unixEpochTimestamp: accumulatedUnixTime,
17272
17328
  firstSegment: currentFirstSegment,
17273
17329
  sequenceNumber: nextSequenceNumber,
17274
17330
  location,
@@ -17279,6 +17335,9 @@ var Mediabunny = (() => {
17279
17335
  };
17280
17336
  currentFirstSegment ??= segment;
17281
17337
  accumulatedTime += nextSegmentDuration;
17338
+ if (accumulatedUnixTime !== null) {
17339
+ accumulatedUnixTime += nextSegmentDuration;
17340
+ }
17282
17341
  this.segments.push(segment);
17283
17342
  } else {
17284
17343
  }
@@ -17324,7 +17383,7 @@ var Mediabunny = (() => {
17324
17383
  throw new Error("Invalid #EXT-X-MAP tag; BYTERANGE attribute must have a specified offset.");
17325
17384
  }
17326
17385
  if (!prevLastSegment) {
17327
- const fullPath = joinPaths(this.path, uri);
17386
+ const fullPath = joinPaths(this.rootPath, uri);
17328
17387
  const location = {
17329
17388
  path: fullPath,
17330
17389
  offset: parsedByteRange?.offset ?? 0,
@@ -17335,7 +17394,7 @@ var Mediabunny = (() => {
17335
17394
  }
17336
17395
  const segment = {
17337
17396
  timestamp: accumulatedTime,
17338
- relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
17397
+ unixEpochTimestamp: accumulatedUnixTime,
17339
17398
  firstSegment: null,
17340
17399
  sequenceNumber: null,
17341
17400
  location,
@@ -17385,7 +17444,7 @@ var Mediabunny = (() => {
17385
17444
  }
17386
17445
  currentKey = {
17387
17446
  method: "AES-128",
17388
- keyUri: joinPaths(this.path, uri),
17447
+ keyUri: joinPaths(this.rootPath, uri),
17389
17448
  iv,
17390
17449
  keyFormat
17391
17450
  };
@@ -17455,13 +17514,17 @@ var Mediabunny = (() => {
17455
17514
  const lastSegmentEnd = lastSegment.timestamp + lastSegment.duration;
17456
17515
  const offset = dateTimeSeconds - lastSegmentEnd;
17457
17516
  for (const segment of this.segments) {
17458
- segment.timestamp += offset;
17459
- segment.relativeToUnixEpoch = true;
17517
+ segment.unixEpochTimestamp = segment.timestamp + offset;
17518
+ if (offsetTimestampsByDateTime) {
17519
+ segment.timestamp = segment.unixEpochTimestamp;
17520
+ }
17460
17521
  }
17461
- accumulatedTime += offset;
17462
17522
  }
17463
17523
  lastProgramDateTimeSeconds = dateTimeSeconds;
17464
- accumulatedTime = dateTimeSeconds;
17524
+ accumulatedUnixTime = dateTimeSeconds;
17525
+ if (offsetTimestampsByDateTime) {
17526
+ accumulatedTime = dateTimeSeconds;
17527
+ }
17465
17528
  } else if (line === TAG_DISCONTINUITY) {
17466
17529
  currentFirstSegment = null;
17467
17530
  } else if (line.startsWith(TAG_TARGETDURATION)) {
@@ -17678,10 +17741,10 @@ var Mediabunny = (() => {
17678
17741
  readMetadata() {
17679
17742
  return this.metadataPromise ??= (async () => {
17680
17743
  assert(this.input._rootSource instanceof PathedSource);
17681
- const { rootPath } = this.input._rootSource;
17682
17744
  const slice = await this.input._reader.requestEntireFile();
17683
17745
  assert(slice);
17684
17746
  const lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
17747
+ const { rootPath } = this.input._rootSource;
17685
17748
  const variantStreams = [];
17686
17749
  const mediaTags = [];
17687
17750
  for (let i = 1; i < lines.length; i++) {
@@ -18196,6 +18259,9 @@ var Mediabunny = (() => {
18196
18259
  isRelativeToUnixEpoch() {
18197
18260
  return this.delegate(() => this.internalTrack.backingTrack.isRelativeToUnixEpoch());
18198
18261
  }
18262
+ getUnixTimeForTimestamp(timestamp) {
18263
+ return this.delegate(() => this.internalTrack.backingTrack.getUnixTimeForTimestamp(timestamp));
18264
+ }
18199
18265
  getBitrate() {
18200
18266
  return this.internalTrack.peakBitrate;
18201
18267
  }
@@ -18784,6 +18850,14 @@ var Mediabunny = (() => {
18784
18850
  throw new TypeError(`${prefix}.isobmff.resolveKeyId, when provided, must be a function.`);
18785
18851
  }
18786
18852
  }
18853
+ if (options.hls !== void 0) {
18854
+ if (!options.hls || typeof options.hls !== "object") {
18855
+ throw new TypeError(`${prefix}.hls, when provided, must be an object.`);
18856
+ }
18857
+ if (options.hls.offsetTimestampsByDateTime !== void 0 && typeof options.hls.offsetTimestampsByDateTime !== "boolean") {
18858
+ throw new TypeError(`${prefix}.hls.offsetTimestampsByDateTime, when provided, must be a boolean.`);
18859
+ }
18860
+ }
18787
18861
  };
18788
18862
 
18789
18863
  // src/decode.ts
@@ -20577,6 +20651,65 @@ var Mediabunny = (() => {
20577
20651
  });
20578
20652
  }
20579
20653
  }
20654
+ /**
20655
+ * Returns a new {@link AudioSample} containing only the frames in the range [startSample, endSample). Both bounds
20656
+ * must lie within this sample's range of frames. The returned sample's timestamp is shifted to match the start of
20657
+ * the trimmed section.
20658
+ */
20659
+ trim(startSample, endSample = this.numberOfFrames) {
20660
+ if (!Number.isInteger(startSample) || startSample < 0) {
20661
+ throw new TypeError("startSample must be a non-negative integer.");
20662
+ }
20663
+ if (!Number.isInteger(endSample) || endSample < 0) {
20664
+ throw new TypeError("endSample must be a non-negative integer.");
20665
+ }
20666
+ if (startSample > this.numberOfFrames) {
20667
+ throw new RangeError("startSample out of range.");
20668
+ }
20669
+ if (endSample > this.numberOfFrames) {
20670
+ throw new RangeError("endSample out of range.");
20671
+ }
20672
+ if (endSample < startSample) {
20673
+ throw new RangeError("endSample must not be less than startSample.");
20674
+ }
20675
+ if (this._closed) {
20676
+ throw new Error("AudioSample is closed.");
20677
+ }
20678
+ const frameCount = endSample - startSample;
20679
+ const bytesPerSample = getBytesPerSample(this.format);
20680
+ let data;
20681
+ if (formatIsPlanar(this.format)) {
20682
+ const planeSize = frameCount * bytesPerSample;
20683
+ data = new Uint8Array(planeSize * this.numberOfChannels);
20684
+ if (frameCount > 0) {
20685
+ for (let i = 0; i < this.numberOfChannels; i++) {
20686
+ this.copyTo(data.subarray(i * planeSize, (i + 1) * planeSize), {
20687
+ planeIndex: i,
20688
+ format: this.format,
20689
+ frameOffset: startSample,
20690
+ frameCount
20691
+ });
20692
+ }
20693
+ }
20694
+ } else {
20695
+ data = new Uint8Array(frameCount * this.numberOfChannels * bytesPerSample);
20696
+ if (frameCount > 0) {
20697
+ this.copyTo(data, {
20698
+ planeIndex: 0,
20699
+ format: this.format,
20700
+ frameOffset: startSample,
20701
+ frameCount
20702
+ });
20703
+ }
20704
+ }
20705
+ return new _AudioSample({
20706
+ data,
20707
+ format: this.format,
20708
+ sampleRate: this.sampleRate,
20709
+ numberOfChannels: this.numberOfChannels,
20710
+ timestamp: this.timestamp + startSample / this.sampleRate
20711
+ });
20712
+ }
20580
20713
  /**
20581
20714
  * Closes this audio sample, releasing held resources. Audio samples should be closed as soon as they are not
20582
20715
  * needed anymore.
@@ -20995,6 +21128,9 @@ var Mediabunny = (() => {
20995
21128
  if (config.onEncoderConfig !== void 0 && typeof config.onEncoderConfig !== "function") {
20996
21129
  throw new TypeError("config.onEncoderConfig, when provided, must be a function.");
20997
21130
  }
21131
+ if (config.onEncodedSample !== void 0 && typeof config.onEncodedSample !== "function") {
21132
+ throw new TypeError("config.onEncodedSample, when provided, must be a function.");
21133
+ }
20998
21134
  validateVideoEncodingAdditionalOptions(config.codec, config);
20999
21135
  };
21000
21136
  var validateVideoEncodingAdditionalOptions = (codec, options) => {
@@ -21090,6 +21226,9 @@ var Mediabunny = (() => {
21090
21226
  if (config.onEncoderConfig !== void 0 && typeof config.onEncoderConfig !== "function") {
21091
21227
  throw new TypeError("config.onEncoderConfig, when provided, must be a function.");
21092
21228
  }
21229
+ if (config.onEncodedSample !== void 0 && typeof config.onEncodedSample !== "function") {
21230
+ throw new TypeError("config.onEncodedSample, when provided, must be a function.");
21231
+ }
21093
21232
  validateAudioEncodingAdditionalOptions(config.codec, config);
21094
21233
  };
21095
21234
  var validateAudioEncodingAdditionalOptions = (codec, options) => {
@@ -22779,14 +22918,29 @@ var Mediabunny = (() => {
22779
22918
  }
22780
22919
  };
22781
22920
  };
22921
+ var validateVideoSinkDecoderOptions = (decoderOptions) => {
22922
+ if (!decoderOptions || typeof decoderOptions !== "object") {
22923
+ throw new TypeError("decoderOptions must be an object.");
22924
+ }
22925
+ if (decoderOptions.hardwareAcceleration !== void 0 && !["no-preference", "prefer-hardware", "prefer-software"].includes(decoderOptions.hardwareAcceleration)) {
22926
+ throw new TypeError(
22927
+ "decoderOptions.hardwareAcceleration, when provided, must be 'no-preference', 'prefer-hardware' or 'prefer-software'."
22928
+ );
22929
+ }
22930
+ if (decoderOptions.optimizeForLatency !== void 0 && typeof decoderOptions.optimizeForLatency !== "boolean") {
22931
+ throw new TypeError("decoderOptions.optimizeForLatency, when provided, must be a boolean.");
22932
+ }
22933
+ };
22782
22934
  var VideoSampleSink = class extends BaseMediaSampleSink {
22783
22935
  /** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
22784
- constructor(videoTrack) {
22936
+ constructor(videoTrack, decoderOptions = {}) {
22785
22937
  if (!(videoTrack instanceof InputVideoTrack)) {
22786
22938
  throw new TypeError("videoTrack must be an InputVideoTrack.");
22787
22939
  }
22940
+ validateVideoSinkDecoderOptions(decoderOptions);
22788
22941
  super();
22789
22942
  this._track = videoTrack;
22943
+ this._decoderOptions = decoderOptions;
22790
22944
  }
22791
22945
  /** @internal */
22792
22946
  async _createDecoder(onSample, onError) {
@@ -22797,9 +22951,14 @@ var Mediabunny = (() => {
22797
22951
  }
22798
22952
  const codec = await this._track.getCodec();
22799
22953
  const rotation = await this._track.getRotation();
22800
- const decoderConfig = await this._track.getDecoderConfig();
22954
+ let decoderConfig = await this._track.getDecoderConfig();
22801
22955
  const timeResolution = await this._track.getTimeResolution();
22802
22956
  assert(codec && decoderConfig);
22957
+ decoderConfig = {
22958
+ ...decoderConfig,
22959
+ hardwareAcceleration: this._decoderOptions.hardwareAcceleration,
22960
+ optimizeForLatency: this._decoderOptions.optimizeForLatency
22961
+ };
22803
22962
  return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution);
22804
22963
  }
22805
22964
  /** @internal */
@@ -22889,11 +23048,14 @@ var Mediabunny = (() => {
22889
23048
  if (options.poolSize !== void 0 && (typeof options.poolSize !== "number" || !Number.isInteger(options.poolSize) || options.poolSize < 0)) {
22890
23049
  throw new TypeError("poolSize must be a non-negative integer.");
22891
23050
  }
23051
+ if (options.decoderOptions !== void 0) {
23052
+ validateVideoSinkDecoderOptions(options.decoderOptions);
23053
+ }
22892
23054
  this._videoTrack = videoTrack;
22893
23055
  this._alpha = options.alpha ?? false;
22894
23056
  this._options = options;
22895
23057
  this._fit = options.fit ?? "fill";
22896
- this._videoSampleSink = new VideoSampleSink(videoTrack);
23058
+ this._videoSampleSink = new VideoSampleSink(videoTrack, options.decoderOptions);
22897
23059
  this._canvasPool = Array.from({ length: options.poolSize ?? 0 }, () => null);
22898
23060
  }
22899
23061
  /** @internal */
@@ -23536,6 +23698,26 @@ var Mediabunny = (() => {
23536
23698
  async isRelativeToUnixEpoch() {
23537
23699
  return this._backing.isRelativeToUnixEpoch();
23538
23700
  }
23701
+ /**
23702
+ * Returns the Unix time (in seconds since January 1, 1970 00:00:00 UTC) that the given track timestamp (in seconds)
23703
+ * maps to, or `null` if there is no such mapping. This provides a piecewise-continuous mapping from this track's
23704
+ * timestamp space into wall-clock time. Such mapping exists, for example, for HLS playlists with
23705
+ * `#EXT-X-PROGRAM-DATE-TIME` tags present.
23706
+ *
23707
+ * This mapping can be available even when {@link InputTrack.isRelativeToUnixEpoch} is `false`, for example for HLS
23708
+ * streams with program date time information but with {@link HlsInputFormatOptions.offsetTimestampsByDateTime}
23709
+ * set to `false`.
23710
+ */
23711
+ async getUnixTimeForTimestamp(timestamp) {
23712
+ return this._backing.getUnixTimeForTimestamp(timestamp);
23713
+ }
23714
+ /**
23715
+ * Whether the track's timestamps can be mapped to Unix wall clock time via
23716
+ * {@link InputTrack.getUnixTimeForTimestamp}.
23717
+ */
23718
+ async hasUnixTimeMapping() {
23719
+ return await this._backing.getUnixTimeForTimestamp(await this.getFirstTimestamp()) !== null;
23720
+ }
23539
23721
  /** Returns the track's disposition, i.e. information about its intended usage. */
23540
23722
  async getDisposition() {
23541
23723
  return this._backing.getDisposition();
@@ -24537,7 +24719,10 @@ var Mediabunny = (() => {
24537
24719
  ref.free();
24538
24720
  }
24539
24721
  this._sourceRefs.length = 0;
24540
- void this._demuxerPromise?.then((demuxer) => demuxer.dispose());
24722
+ if (this._demuxerPromise) {
24723
+ void this._demuxerPromise.then((demuxer) => demuxer.dispose()).catch(() => {
24724
+ });
24725
+ }
24541
24726
  }
24542
24727
  /**
24543
24728
  * Calls `.dispose()` on the input, implementing the `Disposable` interface for use with
@@ -31990,17 +32175,17 @@ ${cue.notes ?? ""}`;
31990
32175
  constructor(options) {
31991
32176
  this.sourceSampleRate = null;
31992
32177
  this.sourceNumberOfChannels = null;
32178
+ this.startTime = null;
32179
+ /** Start frame of current buffer */
32180
+ this.bufferStartFrame = 0;
31993
32181
  /** The highest index written to in the current buffer */
31994
32182
  this.maxWrittenFrame = null;
31995
32183
  this.targetSampleRate = options.targetSampleRate;
31996
32184
  this.targetNumberOfChannels = options.targetNumberOfChannels;
31997
- this.endTime = options.endTime;
31998
32185
  this.onSample = options.onSample;
31999
32186
  this.bufferSizeInFrames = Math.floor(this.targetSampleRate * 5);
32000
32187
  this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels;
32001
32188
  this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
32002
- this.bufferStartFrame = Math.floor(options.startTime * this.targetSampleRate);
32003
- this.timestampOffset = options.startTime - this.bufferStartFrame / this.targetSampleRate;
32004
32189
  }
32005
32190
  /**
32006
32191
  * Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
@@ -32090,16 +32275,18 @@ ${cue.notes ?? ""}`;
32090
32275
  if (this.sourceSampleRate === null) {
32091
32276
  this.sourceSampleRate = audioSample.sampleRate;
32092
32277
  this.sourceNumberOfChannels = audioSample.numberOfChannels;
32278
+ this.startTime = audioSample.timestamp;
32093
32279
  this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
32094
32280
  this.doChannelMixerSetup();
32095
32281
  }
32282
+ assert(this.startTime !== null);
32096
32283
  const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
32097
32284
  this.ensureTempBufferSize(requiredSamples);
32098
32285
  const sourceDataSize = audioSample.allocationSize({ planeIndex: 0, format: "f32" });
32099
32286
  const sourceView = new Float32Array(this.tempSourceBuffer.buffer, 0, sourceDataSize / 4);
32100
32287
  audioSample.copyTo(sourceView, { planeIndex: 0, format: "f32" });
32101
- const inputStartTime = audioSample.timestamp;
32102
- const inputEndTime = Math.min(audioSample.timestamp + audioSample.duration, this.endTime);
32288
+ const inputStartTime = audioSample.timestamp - this.startTime;
32289
+ const inputEndTime = inputStartTime + audioSample.duration;
32103
32290
  const outputStartFrame = Math.floor(inputStartTime * this.targetSampleRate);
32104
32291
  const outputEndFrame = Math.ceil(inputEndTime * this.targetSampleRate);
32105
32292
  for (let outputFrame = outputStartFrame; outputFrame < outputEndFrame; outputFrame++) {
@@ -32142,15 +32329,15 @@ ${cue.notes ?? ""}`;
32142
32329
  if (this.maxWrittenFrame === null) {
32143
32330
  return;
32144
32331
  }
32332
+ assert(this.startTime !== null);
32145
32333
  const samplesWritten = (this.maxWrittenFrame + 1) * this.targetNumberOfChannels;
32146
32334
  const outputData = new Float32Array(samplesWritten);
32147
32335
  outputData.set(this.outputBuffer.subarray(0, samplesWritten));
32148
- const timestampSeconds = this.bufferStartFrame / this.targetSampleRate;
32149
32336
  const audioSample = new AudioSample({
32150
32337
  format: "f32",
32151
32338
  sampleRate: this.targetSampleRate,
32152
32339
  numberOfChannels: this.targetNumberOfChannels,
32153
- timestamp: timestampSeconds + this.timestampOffset,
32340
+ timestamp: this.startTime + this.bufferStartFrame / this.targetSampleRate,
32154
32341
  data: outputData
32155
32342
  });
32156
32343
  await this.onSample(audioSample);
@@ -32314,6 +32501,7 @@ ${cue.notes ?? ""}`;
32314
32501
  * So, we keep track of the encoder error and throw it as soon as we get the chance.
32315
32502
  */
32316
32503
  this.error = null;
32504
+ this.closed = false;
32317
32505
  this.lastMuxerPromise = Promise.resolve();
32318
32506
  }
32319
32507
  async add(videoSample, shouldClose, encodeOptions) {
@@ -32450,6 +32638,9 @@ ${cue.notes ?? ""}`;
32450
32638
  }
32451
32639
  }
32452
32640
  assert(this.encoderInitialized);
32641
+ if (this.closed) {
32642
+ break;
32643
+ }
32453
32644
  const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
32454
32645
  const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
32455
32646
  const mergedEncodeOptions = { ...sampleToEncode.encodeOptions, ...encodeOptions };
@@ -32458,6 +32649,7 @@ ${cue.notes ?? ""}`;
32458
32649
  keyFrame: mergedEncodeOptions.keyFrame !== void 0 ? mergedEncodeOptions.keyFrame : keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval
32459
32650
  };
32460
32651
  this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval;
32652
+ this.encodingConfig.onEncodedSample?.(sampleToEncode);
32461
32653
  if (this.customEncoder) {
32462
32654
  this.customEncoderQueueSize++;
32463
32655
  const clonedSample = sampleToEncode.clone();
@@ -32712,6 +32904,7 @@ ${cue.notes ?? ""}`;
32712
32904
  const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp, frameRate);
32713
32905
  await this.padFrameRate(alignedEnd);
32714
32906
  }
32907
+ this.closed = true;
32715
32908
  this.frameRateLastSample?.close();
32716
32909
  this.frameRateLastSample = null;
32717
32910
  if (this.customEncoder) {
@@ -33585,6 +33778,7 @@ ${cue.notes ?? ""}`;
33585
33778
  */
33586
33779
  this.error = null;
33587
33780
  this.lastMuxerPromise = Promise.resolve();
33781
+ this.closed = false;
33588
33782
  }
33589
33783
  async add(audioSample, shouldClose) {
33590
33784
  try {
@@ -33607,8 +33801,6 @@ ${cue.notes ?? ""}`;
33607
33801
  this.resampler = new AudioResampler({
33608
33802
  targetNumberOfChannels: config.transform.numberOfChannels ?? audioSample.numberOfChannels,
33609
33803
  targetSampleRate: config.transform.sampleRate ?? audioSample.sampleRate,
33610
- startTime: audioSample.timestamp,
33611
- endTime: Infinity,
33612
33804
  onSample: async (sample) => {
33613
33805
  await this.processAndEncode(sample, true);
33614
33806
  }
@@ -33677,6 +33869,9 @@ ${cue.notes ?? ""}`;
33677
33869
  }
33678
33870
  }
33679
33871
  assert(this.encoderInitialized);
33872
+ if (this.closed) {
33873
+ return;
33874
+ }
33680
33875
  {
33681
33876
  const startSampleIndex = Math.round(
33682
33877
  audioSample.timestamp * audioSample.sampleRate
@@ -33702,6 +33897,7 @@ ${cue.notes ?? ""}`;
33702
33897
  this.lastEndSampleIndex += audioSample.numberOfFrames;
33703
33898
  }
33704
33899
  }
33900
+ this.encodingConfig.onEncodedSample?.(audioSample);
33705
33901
  if (this.customEncoder) {
33706
33902
  this.customEncoderQueueSize++;
33707
33903
  const clonedSample = audioSample.clone();
@@ -33974,6 +34170,7 @@ ${cue.notes ?? ""}`;
33974
34170
  await this.resampler.finalize();
33975
34171
  }
33976
34172
  this.resampler = null;
34173
+ this.closed = true;
33977
34174
  if (this.customEncoder) {
33978
34175
  if (!forceClose) {
33979
34176
  void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
@@ -37336,6 +37533,9 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37336
37533
  if (trackOptions.frameRate) {
37337
37534
  encodingConfig.transform.frameRate = trackOptions.frameRate;
37338
37535
  }
37536
+ if (trackOptions.process) {
37537
+ encodingConfig.transform.process = trackOptions.process;
37538
+ }
37339
37539
  if (needsRerender) {
37340
37540
  outputTrackRotation = 0;
37341
37541
  encodingConfig.transform.width = width;
@@ -37345,6 +37545,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37345
37545
  encodingConfig.transform.crop = crop;
37346
37546
  encodingConfig.transform.alpha = alpha;
37347
37547
  }
37548
+ let lastSampleTimestamp = null;
37549
+ encodingConfig.onEncodedSample = (sample) => {
37550
+ lastSampleTimestamp = sample.timestamp;
37551
+ };
37348
37552
  const source = new VideoSampleSource(encodingConfig);
37349
37553
  videoSource = source;
37350
37554
  this._trackPromises.push((async () => {
@@ -37357,7 +37561,13 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37357
37561
  }
37358
37562
  const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
37359
37563
  sample.setTimestamp(adjustedSampleTimestamp);
37360
- await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
37564
+ this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37565
+ await source.add(sample);
37566
+ if (lastSampleTimestamp !== null) {
37567
+ if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37568
+ await this._synchronizer.wait(lastSampleTimestamp);
37569
+ }
37570
+ }
37361
37571
  sample.close();
37362
37572
  }
37363
37573
  source.close();
@@ -37385,52 +37595,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37385
37595
  this._outputOwnTrackGroups.push(ownGroup);
37386
37596
  }
37387
37597
  /** @internal */
37388
- async _registerVideoSample(trackOptions, outputTrackId, source, sample) {
37389
- if (this._canceled) {
37390
- return;
37391
- }
37392
- this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37393
- let finalSamples;
37394
- if (!trackOptions.process) {
37395
- finalSamples = [sample];
37396
- } else {
37397
- let processed = trackOptions.process(sample);
37398
- if (processed instanceof Promise) processed = await processed;
37399
- if (!Array.isArray(processed)) {
37400
- processed = processed === null ? [] : [processed];
37401
- }
37402
- finalSamples = processed.map((x) => {
37403
- if (x instanceof VideoSample) {
37404
- return x;
37405
- }
37406
- if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
37407
- return new VideoSample(x);
37408
- }
37409
- return new VideoSample(x, {
37410
- timestamp: sample.timestamp,
37411
- duration: sample.duration
37412
- });
37413
- });
37414
- }
37415
- try {
37416
- for (const finalSample of finalSamples) {
37417
- if (this._canceled) {
37418
- break;
37419
- }
37420
- await source.add(finalSample);
37421
- if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
37422
- await this._synchronizer.wait(finalSample.timestamp);
37423
- }
37424
- }
37425
- } finally {
37426
- for (const finalSample of finalSamples) {
37427
- if (finalSample !== sample) {
37428
- finalSample.close();
37429
- }
37430
- }
37431
- }
37432
- }
37433
- /** @internal */
37434
37598
  async _processAudioTrack(track, trackOptions, outputTrackId) {
37435
37599
  const sourceCodec = await track.getCodec();
37436
37600
  if (!sourceCodec) {
@@ -37447,9 +37611,10 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37447
37611
  const firstTimestamp = await track.getFirstTimestamp();
37448
37612
  let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
37449
37613
  let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
37450
- let needsResample = numberOfChannels !== originalNumberOfChannels || sampleRate !== originalSampleRate || firstTimestamp < this._startTimestamp || firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
37614
+ const needsTrimming = firstTimestamp < this._startTimestamp;
37615
+ const needsPadding = firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
37451
37616
  let audioCodecs = this.output.format.getSupportedAudioCodecs();
37452
- if (!trackOptions.forceTranscode && !trackOptions.bitrate && !needsResample && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
37617
+ if (!trackOptions.forceTranscode && !trackOptions.bitrate && numberOfChannels === originalNumberOfChannels && sampleRate === originalSampleRate && !needsTrimming && !needsPadding && audioCodecs.includes(sourceCodec) && (!trackOptions.codec || trackOptions.codec === sourceCodec) && !trackOptions.process && trackOptions.sampleFormat === void 0) {
37453
37618
  const source = new EncodedAudioPacketSource(sourceCodec);
37454
37619
  audioSource = source;
37455
37620
  this._trackPromises.push((async () => {
@@ -37505,7 +37670,6 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37505
37670
  });
37506
37671
  const nonPcmCodec = encodableCodecsWithDefaultParams.find((codec) => NON_PCM_AUDIO_CODECS.includes(codec));
37507
37672
  if (nonPcmCodec) {
37508
- needsResample = true;
37509
37673
  codecOfChoice = nonPcmCodec;
37510
37674
  numberOfChannels = FALLBACK_NUMBER_OF_CHANNELS;
37511
37675
  sampleRate = FALLBACK_SAMPLE_RATE;
@@ -37521,38 +37685,70 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37521
37685
  });
37522
37686
  return;
37523
37687
  }
37524
- if (needsResample) {
37525
- audioSource = this._resampleAudio(
37526
- track,
37527
- trackOptions,
37528
- outputTrackId,
37529
- codecOfChoice,
37530
- numberOfChannels,
37531
- sampleRate,
37532
- bitrate
37533
- );
37534
- } else {
37535
- const source = new AudioSampleSource({
37536
- codec: codecOfChoice,
37537
- bitrate
37538
- });
37539
- audioSource = source;
37540
- this._trackPromises.push((async () => {
37541
- await this._started;
37542
- const sink = new AudioSampleSink(track);
37543
- for await (const sample of sink.samples(void 0, this._endTimestamp)) {
37544
- if (this._canceled) {
37688
+ const encodingConfig = {
37689
+ codec: codecOfChoice,
37690
+ bitrate,
37691
+ transform: {
37692
+ sampleFormat: trackOptions.sampleFormat,
37693
+ process: trackOptions.process
37694
+ }
37695
+ };
37696
+ assert(encodingConfig.transform);
37697
+ if (numberOfChannels !== originalNumberOfChannels) {
37698
+ encodingConfig.transform.numberOfChannels = numberOfChannels;
37699
+ }
37700
+ if (sampleRate !== originalSampleRate) {
37701
+ encodingConfig.transform.sampleRate = sampleRate;
37702
+ }
37703
+ let lastSampleTimestamp = null;
37704
+ encodingConfig.onEncodedSample = (sample) => {
37705
+ lastSampleTimestamp = sample.timestamp;
37706
+ };
37707
+ const source = new AudioSampleSource(encodingConfig);
37708
+ audioSource = source;
37709
+ this._trackPromises.push((async () => {
37710
+ await this._started;
37711
+ if (needsPadding) {
37712
+ const paddingLength = firstTimestamp - this._startTimestamp;
37713
+ const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
37714
+ const silentSample = new AudioSample({
37715
+ data: new Float32Array(paddingLengthSamples * originalNumberOfChannels),
37716
+ format: "f32-planar",
37717
+ numberOfChannels: originalNumberOfChannels,
37718
+ sampleRate: originalSampleRate,
37719
+ timestamp: 0
37720
+ });
37721
+ await this._registerAudioSample(silentSample, source, outputTrackId, () => lastSampleTimestamp);
37722
+ }
37723
+ const sink = new AudioSampleSink(track);
37724
+ for await (let sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
37725
+ if (this._canceled) {
37726
+ sample.close();
37727
+ return;
37728
+ }
37729
+ let startFrame = 0;
37730
+ let endFrame = sample.numberOfFrames;
37731
+ if (sample.timestamp < this._startTimestamp) {
37732
+ startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
37733
+ }
37734
+ if (sample.timestamp + sample.duration > this._endTimestamp) {
37735
+ endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
37736
+ }
37737
+ if (startFrame > 0 || endFrame < sample.numberOfFrames) {
37738
+ const trimmedSample = sample.trim(startFrame, endFrame);
37739
+ sample.close();
37740
+ sample = trimmedSample;
37741
+ if (sample.numberOfFrames === 0) {
37545
37742
  sample.close();
37546
- return;
37743
+ continue;
37547
37744
  }
37548
- sample.setTimestamp(sample.timestamp - this._startTimestamp);
37549
- await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
37550
- sample.close();
37551
37745
  }
37552
- source.close();
37553
- this._synchronizer.closeTrack(outputTrackId);
37554
- })());
37555
- }
37746
+ sample.setTimestamp(sample.timestamp - this._startTimestamp);
37747
+ await this._registerAudioSample(sample, source, outputTrackId, () => lastSampleTimestamp);
37748
+ }
37749
+ source.close();
37750
+ this._synchronizer.closeTrack(outputTrackId);
37751
+ })());
37556
37752
  }
37557
37753
  let ownGroup = null;
37558
37754
  if (!trackOptions.group) {
@@ -37573,89 +37769,18 @@ The @mediabunny/mp3-encoder extension package provides support for encoding MP3.
37573
37769
  this._outputOwnTrackGroups.push(ownGroup);
37574
37770
  }
37575
37771
  /** @internal */
37576
- async _registerAudioSample(trackOptions, outputTrackId, source, inputSample) {
37577
- if (this._canceled) {
37578
- return;
37579
- }
37580
- let sample = inputSample;
37581
- if (trackOptions.sampleFormat !== void 0 && toInterleavedAudioFormat(sample.format) !== trackOptions.sampleFormat) {
37582
- sample = audioSampleToInterleavedFormat(sample, trackOptions.sampleFormat);
37583
- }
37772
+ async _registerAudioSample(sample, source, outputTrackId, getLastSampleTimestamp) {
37584
37773
  this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
37585
- let finalSamples;
37586
- if (!trackOptions.process) {
37587
- finalSamples = [sample];
37588
- } else {
37589
- let processed = trackOptions.process(sample);
37590
- if (processed instanceof Promise) processed = await processed;
37591
- if (!Array.isArray(processed)) {
37592
- processed = processed === null ? [] : [processed];
37593
- }
37594
- if (!processed.every((x) => x instanceof AudioSample)) {
37595
- throw new TypeError(
37596
- "The audio process function must return an AudioSample, null, or an array of AudioSamples."
37597
- );
37598
- }
37599
- finalSamples = processed;
37600
- }
37601
- try {
37602
- for (const finalSample of finalSamples) {
37603
- if (this._canceled) {
37604
- break;
37605
- }
37606
- await source.add(finalSample);
37607
- if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
37608
- await this._synchronizer.wait(finalSample.timestamp);
37609
- }
37610
- }
37611
- } finally {
37612
- if (sample !== inputSample) {
37613
- sample.close();
37614
- }
37615
- for (const finalSample of finalSamples) {
37616
- if (finalSample !== inputSample) {
37617
- finalSample.close();
37618
- }
37774
+ await source.add(sample);
37775
+ sample.close();
37776
+ const lastSampleTimestamp = getLastSampleTimestamp();
37777
+ if (lastSampleTimestamp !== null) {
37778
+ if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
37779
+ await this._synchronizer.wait(lastSampleTimestamp);
37619
37780
  }
37620
37781
  }
37621
37782
  }
37622
37783
  /** @internal */
37623
- _resampleAudio(track, trackOptions, outputTrackId, codec, targetNumberOfChannels, targetSampleRate, bitrate) {
37624
- const source = new AudioSampleSource({
37625
- codec,
37626
- bitrate
37627
- });
37628
- this._trackPromises.push((async () => {
37629
- await this._started;
37630
- const resampler = new AudioResampler({
37631
- targetNumberOfChannels,
37632
- targetSampleRate,
37633
- startTime: this._startTimestamp,
37634
- endTime: this._endTimestamp,
37635
- onSample: async (sample) => {
37636
- assert(sample.timestamp >= this._startTimestamp);
37637
- sample.setTimestamp(sample.timestamp - this._startTimestamp);
37638
- await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
37639
- sample.close();
37640
- }
37641
- });
37642
- const sink = new AudioSampleSink(track);
37643
- const iterator = sink.samples(this._startTimestamp, this._endTimestamp);
37644
- for await (const sample of iterator) {
37645
- if (this._canceled) {
37646
- sample.close();
37647
- return;
37648
- }
37649
- await resampler.add(sample);
37650
- sample.close();
37651
- }
37652
- await resampler.finalize();
37653
- source.close();
37654
- this._synchronizer.closeTrack(outputTrackId);
37655
- })());
37656
- return source;
37657
- }
37658
- /** @internal */
37659
37784
  _reportProgress(trackId, endTimestamp) {
37660
37785
  if (!this._computeProgress) {
37661
37786
  return;