mediabunny 1.53.0 → 1.54.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.
@@ -24778,6 +24778,100 @@ var Mediabunny = (() => {
24778
24778
  assert(decoderConfig);
24779
24779
  return determineVideoPacketType(codec, decoderConfig, packet.data);
24780
24780
  }
24781
+ /**
24782
+ * Computes frame rate metrics for this video track, i.e. estimates the video's frame rate. Frame rate is never
24783
+ * determined from file metadata (which is unreliable) but is always deduced directly from the actual frame
24784
+ * timestamps.
24785
+ */
24786
+ async computeFrameRateMetrics(options = {}) {
24787
+ if (!options || typeof options !== "object") {
24788
+ throw new TypeError("options must be an object.");
24789
+ }
24790
+ if (options.targetPacketCount !== void 0 && (!Number.isFinite(options.targetPacketCount) || options.targetPacketCount < 0)) {
24791
+ throw new TypeError("options.targetPacketCount must be a non-negative number.");
24792
+ }
24793
+ const timeResolution = await this.getTimeResolution();
24794
+ const targetPacketCount = options.targetPacketCount ?? 256;
24795
+ const sink = new EncodedPacketSink(this);
24796
+ const timestamps = [];
24797
+ let maxTimestamp = -Infinity;
24798
+ let probedPacketCount = 0;
24799
+ for await (const packet of sink.packets(void 0, void 0, { metadataOnly: true })) {
24800
+ if (timestamps.length >= targetPacketCount && packet.timestamp >= maxTimestamp) {
24801
+ break;
24802
+ }
24803
+ timestamps.push(packet.timestamp);
24804
+ maxTimestamp = Math.max(maxTimestamp, packet.timestamp);
24805
+ probedPacketCount++;
24806
+ }
24807
+ const ticks = new Float64Array(timestamps.length);
24808
+ for (let i = 0; i < timestamps.length; i++) {
24809
+ ticks[i] = Math.round(timestamps[i] * timeResolution);
24810
+ }
24811
+ ticks.sort();
24812
+ let n = 1;
24813
+ for (let i = 1; i < ticks.length; i++) {
24814
+ if (ticks[i] !== ticks[n - 1]) {
24815
+ ticks[n++] = ticks[i];
24816
+ }
24817
+ }
24818
+ if (n < 2) {
24819
+ return {
24820
+ underlyingFrameRate: null,
24821
+ bestGuessFrameRate: timeResolution,
24822
+ minFrameRate: timeResolution,
24823
+ maxFrameRate: timeResolution,
24824
+ averageFrameRate: timeResolution,
24825
+ medianFrameRate: timeResolution,
24826
+ frameRateIsConstant: true,
24827
+ probedPacketCount
24828
+ };
24829
+ }
24830
+ const activeTicks = ticks.subarray(0, n);
24831
+ const underlyingFrameRate = findUnderlyingFrameRate(activeTicks, timeResolution);
24832
+ const unitRate = underlyingFrameRate ?? timeResolution;
24833
+ const ticksPerFrame = underlyingFrameRate !== null ? timeResolution / underlyingFrameRate : null;
24834
+ const histogram = /* @__PURE__ */ new Map();
24835
+ let minDifference = Infinity;
24836
+ let maxDifference = -Infinity;
24837
+ let totalDifference = 0;
24838
+ for (let i = 1; i < n; i++) {
24839
+ const tickDifference = activeTicks[i] - activeTicks[i - 1];
24840
+ const difference = ticksPerFrame !== null ? Math.max(1, Math.round(tickDifference / ticksPerFrame)) : tickDifference;
24841
+ histogram.set(difference, (histogram.get(difference) ?? 0) + 1);
24842
+ minDifference = Math.min(minDifference, difference);
24843
+ maxDifference = Math.max(maxDifference, difference);
24844
+ totalDifference += difference;
24845
+ }
24846
+ const differenceCount = n - 1;
24847
+ const sortedDifferences = [...histogram.keys()].sort((a, b) => a - b);
24848
+ const middleA = differenceCount - 1 >> 1;
24849
+ const middleB = differenceCount >> 1;
24850
+ let medianDifferenceA = 0;
24851
+ let medianDifferenceB = 0;
24852
+ let cumulativeCount = 0;
24853
+ for (const difference of sortedDifferences) {
24854
+ cumulativeCount += histogram.get(difference);
24855
+ if (medianDifferenceA === 0 && cumulativeCount > middleA) {
24856
+ medianDifferenceA = difference;
24857
+ }
24858
+ if (cumulativeCount > middleB) {
24859
+ medianDifferenceB = difference;
24860
+ break;
24861
+ }
24862
+ }
24863
+ const medianFrameRate = (unitRate / medianDifferenceA + unitRate / medianDifferenceB) / 2;
24864
+ return {
24865
+ underlyingFrameRate,
24866
+ bestGuessFrameRate: underlyingFrameRate !== null ? underlyingFrameRate : getBestGuessFrameRate(medianFrameRate),
24867
+ minFrameRate: unitRate / maxDifference,
24868
+ maxFrameRate: unitRate / minDifference,
24869
+ averageFrameRate: unitRate * differenceCount / totalDifference,
24870
+ medianFrameRate,
24871
+ frameRateIsConstant: underlyingFrameRate !== null && minDifference === 1 && maxDifference === 1,
24872
+ probedPacketCount
24873
+ };
24874
+ }
24781
24875
  };
24782
24876
  var InputAudioTrack = class extends InputTrack {
24783
24877
  /** @internal */
@@ -24995,6 +25089,183 @@ var Mediabunny = (() => {
24995
25089
  return 0;
24996
25090
  }).map((x) => x.track);
24997
25091
  };
25092
+ var findUnderlyingFrameRate = (ticks, resolution) => {
25093
+ const MAX_DENOMINATOR = 1e6;
25094
+ const MIN_INLIER_RATIO = 0.98;
25095
+ const DELTA_TOLERANCE = 1 + 1e-9;
25096
+ const MAX_EFFECTIVE_FRAME_SPAN = 1e3;
25097
+ const KNOWN_FRAME_RATES = [
25098
+ 12,
25099
+ 15,
25100
+ 20,
25101
+ 24e3 / 1001,
25102
+ 24,
25103
+ 25,
25104
+ 3e4 / 1001,
25105
+ 30,
25106
+ 48,
25107
+ 50,
25108
+ 6e4 / 1001,
25109
+ 60,
25110
+ 100,
25111
+ 12e4 / 1001,
25112
+ 120,
25113
+ 144,
25114
+ 240
25115
+ ];
25116
+ if (ticks.length < 2) {
25117
+ return null;
25118
+ }
25119
+ const gaps = new Float64Array(ticks.length - 1);
25120
+ for (let i = 1; i < ticks.length; i++) {
25121
+ const gap = ticks[i] - ticks[i - 1];
25122
+ if (!(gap > 0)) {
25123
+ return null;
25124
+ }
25125
+ gaps[i - 1] = gap;
25126
+ }
25127
+ const sortedGaps = gaps.slice();
25128
+ sortedGaps.sort();
25129
+ let period = sortedGaps[Math.floor(sortedGaps.length * 0.05)];
25130
+ for (let iteration = 0; iteration < 6; iteration++) {
25131
+ let totalTicks2 = 0;
25132
+ let totalFrames2 = 0;
25133
+ for (const gap of gaps) {
25134
+ const multiple = Math.max(1, Math.round(gap / period));
25135
+ if (Math.abs(gap - multiple * period) >= DELTA_TOLERANCE) {
25136
+ continue;
25137
+ }
25138
+ totalTicks2 += gap;
25139
+ totalFrames2 += multiple;
25140
+ }
25141
+ if (totalFrames2 === 0) {
25142
+ return null;
25143
+ }
25144
+ const refinedPeriod = totalTicks2 / totalFrames2;
25145
+ if (Math.abs(refinedPeriod - period) <= 1e-12 * Math.max(1, period)) {
25146
+ period = refinedPeriod;
25147
+ break;
25148
+ }
25149
+ period = refinedPeriod;
25150
+ }
25151
+ let inlierCount = 0;
25152
+ let totalTicks = 0;
25153
+ let totalFrames = 0;
25154
+ for (const gap of gaps) {
25155
+ const multiple = Math.max(1, Math.round(gap / period));
25156
+ if (Math.abs(gap - multiple * period) >= DELTA_TOLERANCE) {
25157
+ continue;
25158
+ }
25159
+ inlierCount++;
25160
+ totalTicks += gap;
25161
+ totalFrames += multiple;
25162
+ }
25163
+ if (inlierCount / gaps.length < MIN_INLIER_RATIO) {
25164
+ return null;
25165
+ }
25166
+ period = totalTicks / totalFrames;
25167
+ const uncertainty = 1 / Math.min(
25168
+ totalFrames,
25169
+ MAX_EFFECTIVE_FRAME_SPAN
25170
+ );
25171
+ const periodLo = Math.max(Number.EPSILON, period - uncertainty);
25172
+ const periodHi = period + uncertainty;
25173
+ const fpsLo = resolution / periodHi;
25174
+ const fpsHi = resolution / periodLo;
25175
+ const fittedFps = resolution / period;
25176
+ let fps = null;
25177
+ let bestKnownError = Infinity;
25178
+ for (const candidate of KNOWN_FRAME_RATES) {
25179
+ if (candidate < fpsLo || candidate > fpsHi) {
25180
+ continue;
25181
+ }
25182
+ const error = Math.abs(candidate / fittedFps - 1);
25183
+ if (error < bestKnownError) {
25184
+ fps = candidate;
25185
+ bestKnownError = error;
25186
+ }
25187
+ }
25188
+ if (fps === null) {
25189
+ const periodFraction = simplestFractionBetween(
25190
+ periodLo,
25191
+ periodHi,
25192
+ MAX_DENOMINATOR
25193
+ );
25194
+ const fpsFraction = simplestFractionBetween(
25195
+ fpsLo,
25196
+ fpsHi,
25197
+ MAX_DENOMINATOR
25198
+ );
25199
+ if (fpsFraction && (!periodFraction || fpsFraction.den < periodFraction.den || fpsFraction.den === periodFraction.den && fpsFraction.num <= periodFraction.num)) {
25200
+ fps = fpsFraction.num / fpsFraction.den;
25201
+ } else if (periodFraction) {
25202
+ fps = resolution * periodFraction.den / periodFraction.num;
25203
+ } else {
25204
+ return null;
25205
+ }
25206
+ }
25207
+ const finalPeriod = resolution / fps;
25208
+ let finalInlierCount = 0;
25209
+ for (const gap of gaps) {
25210
+ const multiple = Math.max(1, Math.round(gap / finalPeriod));
25211
+ if (Math.abs(gap - multiple * finalPeriod) < DELTA_TOLERANCE) {
25212
+ finalInlierCount++;
25213
+ }
25214
+ }
25215
+ if (finalInlierCount / gaps.length < MIN_INLIER_RATIO) {
25216
+ return null;
25217
+ }
25218
+ return fps;
25219
+ };
25220
+ var simplestFractionBetween = (lo, hi, maxDenominator) => {
25221
+ for (let den = 1; den <= maxDenominator; den++) {
25222
+ const num = Math.floor(lo * den) + 1;
25223
+ if (num / den < hi) {
25224
+ return simplifyRational({ num, den });
25225
+ }
25226
+ }
25227
+ return null;
25228
+ };
25229
+ var getBestGuessFrameRate = (frameRate) => {
25230
+ const SPECIAL_FRAME_RATES = [
25231
+ 24 / 1.001,
25232
+ 30 / 1.001,
25233
+ 60 / 1.001,
25234
+ 120 / 1.001
25235
+ ];
25236
+ const COMMON_FRAME_RATES = [
25237
+ 12,
25238
+ 15,
25239
+ 20,
25240
+ 24,
25241
+ 25,
25242
+ 30,
25243
+ 48,
25244
+ 50,
25245
+ 60,
25246
+ 100,
25247
+ 120,
25248
+ 144,
25249
+ 240
25250
+ ];
25251
+ const SPECIAL_TOLERANCE = 5e-4;
25252
+ const COMMON_TOLERANCE = 0.025;
25253
+ for (const candidate of SPECIAL_FRAME_RATES) {
25254
+ if (Math.abs(candidate / frameRate - 1) <= SPECIAL_TOLERANCE) {
25255
+ return candidate;
25256
+ }
25257
+ }
25258
+ let best = frameRate;
25259
+ let bestError = Infinity;
25260
+ for (const candidate of COMMON_FRAME_RATES) {
25261
+ const error = Math.abs(candidate / frameRate - 1);
25262
+ if (error <= COMMON_TOLERANCE && error < bestError) {
25263
+ best = candidate;
25264
+ bestError = error;
25265
+ }
25266
+ }
25267
+ return best;
25268
+ };
24998
25269
 
24999
25270
  // src/input.ts
25000
25271
  polyfillSymbolDispose();
@@ -29209,9 +29480,16 @@ var Mediabunny = (() => {
29209
29480
  if (this._monotonicity === true && position !== this._lastFlushEnd) {
29210
29481
  throw new Error("Internal error: Monotonicity violation.");
29211
29482
  }
29483
+ const isPartialView = section.start !== 0 || section.end !== chunk.data.byteLength;
29484
+ let data;
29485
+ if (isPartialView && isWebKit()) {
29486
+ data = chunk.data.slice(section.start, section.end);
29487
+ } else {
29488
+ data = chunk.data.subarray(section.start, section.end);
29489
+ }
29212
29490
  void this._streamWriter.write({
29213
29491
  type: "write",
29214
- data: chunk.data.subarray(section.start, section.end),
29492
+ data,
29215
29493
  position
29216
29494
  }).catch((error) => {
29217
29495
  this._writeError ??= error;