avbridge 2.11.0 → 2.12.1

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 (84) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/{avi-B5CQYB7L.cjs → avi-32UABODO.cjs} +14 -6
  3. package/dist/avi-32UABODO.cjs.map +1 -0
  4. package/dist/{avi-2ILLBNPQ.cjs → avi-5BPR6QUX.cjs} +14 -6
  5. package/dist/avi-5BPR6QUX.cjs.map +1 -0
  6. package/dist/{avi-RWWPN2PR.js → avi-BLIH7KKV.js} +13 -5
  7. package/dist/avi-BLIH7KKV.js.map +1 -0
  8. package/dist/{avi-JXU4GQL2.js → avi-GX2H34IQ.js} +13 -5
  9. package/dist/avi-GX2H34IQ.js.map +1 -0
  10. package/dist/{chunk-DCSOQH2N.js → chunk-3AI5WFFN.js} +40 -16
  11. package/dist/chunk-3AI5WFFN.js.map +1 -0
  12. package/dist/{chunk-2NSOOMXW.js → chunk-3YKWU4FM.js} +3 -3
  13. package/dist/{chunk-2NSOOMXW.js.map → chunk-3YKWU4FM.js.map} +1 -1
  14. package/dist/{chunk-GYIJU44C.js → chunk-5CX7BVVV.js} +5 -5
  15. package/dist/{chunk-GYIJU44C.js.map → chunk-5CX7BVVV.js.map} +1 -1
  16. package/dist/{chunk-CL6UEUQF.js → chunk-B76QWPFM.js} +5 -5
  17. package/dist/{chunk-CL6UEUQF.js.map → chunk-B76QWPFM.js.map} +1 -1
  18. package/dist/{chunk-IHNHHEA2.js → chunk-BN7BRTLY.js} +143 -32
  19. package/dist/chunk-BN7BRTLY.js.map +1 -0
  20. package/dist/{chunk-OTFS7DC4.cjs → chunk-E5MAM2P4.cjs} +14 -14
  21. package/dist/{chunk-OTFS7DC4.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
  22. package/dist/{chunk-L7A3ECI2.cjs → chunk-HZUVMXBN.cjs} +4 -4
  23. package/dist/{chunk-L7A3ECI2.cjs.map → chunk-HZUVMXBN.cjs.map} +1 -1
  24. package/dist/{chunk-37UOSAVI.cjs → chunk-UM6WCSGL.cjs} +157 -46
  25. package/dist/chunk-UM6WCSGL.cjs.map +1 -0
  26. package/dist/{chunk-BYGZN4Z5.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
  27. package/dist/{chunk-BYGZN4Z5.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
  28. package/dist/{chunk-Z33SBWL5.cjs → chunk-YPZFGJV3.cjs} +40 -16
  29. package/dist/chunk-YPZFGJV3.cjs.map +1 -0
  30. package/dist/element-browser.js +186 -43
  31. package/dist/element-browser.js.map +1 -1
  32. package/dist/element.cjs +5 -5
  33. package/dist/element.d.cts +1 -1
  34. package/dist/element.d.ts +1 -1
  35. package/dist/element.js +4 -4
  36. package/dist/index.cjs +21 -21
  37. package/dist/index.d.cts +2 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +9 -9
  40. package/dist/{libav-demux-3N5Y3VQA.cjs → libav-demux-575OYCT2.cjs} +9 -9
  41. package/dist/{libav-demux-3N5Y3VQA.cjs.map → libav-demux-575OYCT2.cjs.map} +1 -1
  42. package/dist/{libav-demux-JXD4OTLM.js → libav-demux-SXZDLC7W.js} +4 -4
  43. package/dist/{libav-demux-JXD4OTLM.js.map → libav-demux-SXZDLC7W.js.map} +1 -1
  44. package/dist/libav-http-reader-2S5HAHW4.js +3 -0
  45. package/dist/{libav-http-reader-WXG3Z7AI.js.map → libav-http-reader-2S5HAHW4.js.map} +1 -1
  46. package/dist/libav-http-reader-Q356EO2K.cjs +16 -0
  47. package/dist/{libav-http-reader-AZLE7YFS.cjs.map → libav-http-reader-Q356EO2K.cjs.map} +1 -1
  48. package/dist/{player-DDdNVFDv.d.cts → player-bQ6n4hVp.d.cts} +15 -0
  49. package/dist/{player-DDdNVFDv.d.ts → player-bQ6n4hVp.d.ts} +15 -0
  50. package/dist/player.cjs +264 -53
  51. package/dist/player.cjs.map +1 -1
  52. package/dist/player.d.cts +22 -0
  53. package/dist/player.d.ts +22 -0
  54. package/dist/player.js +264 -53
  55. package/dist/player.js.map +1 -1
  56. package/dist/remux-NSBJFMLG.cjs +35 -0
  57. package/dist/{remux-KUS5GIL6.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
  58. package/dist/remux-PHUHO3VV.js +10 -0
  59. package/dist/{remux-56V7LDAD.js.map → remux-PHUHO3VV.js.map} +1 -1
  60. package/package.json +1 -1
  61. package/src/element/avbridge-player.ts +123 -23
  62. package/src/element/player-styles.ts +13 -1
  63. package/src/player.ts +3 -3
  64. package/src/probe/avi.ts +34 -2
  65. package/src/strategies/fallback/decoder.ts +148 -19
  66. package/src/strategies/fallback/video-renderer.ts +41 -3
  67. package/src/strategies/hybrid/decoder.ts +34 -9
  68. package/src/types.ts +15 -0
  69. package/src/util/libav-http-reader.ts +58 -19
  70. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  71. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  72. package/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
  73. package/dist/avi-2ILLBNPQ.cjs.map +0 -1
  74. package/dist/avi-B5CQYB7L.cjs.map +0 -1
  75. package/dist/avi-JXU4GQL2.js.map +0 -1
  76. package/dist/avi-RWWPN2PR.js.map +0 -1
  77. package/dist/chunk-37UOSAVI.cjs.map +0 -1
  78. package/dist/chunk-DCSOQH2N.js.map +0 -1
  79. package/dist/chunk-IHNHHEA2.js.map +0 -1
  80. package/dist/chunk-Z33SBWL5.cjs.map +0 -1
  81. package/dist/libav-http-reader-AZLE7YFS.cjs +0 -16
  82. package/dist/libav-http-reader-WXG3Z7AI.js +0 -3
  83. package/dist/remux-56V7LDAD.js +0 -10
  84. package/dist/remux-KUS5GIL6.cjs +0 -35
@@ -1,7 +1,7 @@
1
1
  import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-EDDWAN2L.js';
2
- import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-GYIJU44C.js';
2
+ import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-5CX7BVVV.js';
3
3
  import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-CPJLFFCC.js';
4
- import { packetPtsSec, sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-2NSOOMXW.js';
4
+ import { packetPtsSec, sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-3YKWU4FM.js';
5
5
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
6
6
  import { pickLibavVariant } from './chunk-5YAWWKA3.js';
7
7
 
@@ -1102,10 +1102,20 @@ var VideoRenderer = class {
1102
1102
  /** Resolves once the first decoded frame has been enqueued. */
1103
1103
  firstFrameReady;
1104
1104
  resolveFirstFrame;
1105
- /** True once at least one frame has been enqueued. */
1105
+ /**
1106
+ * True once at least one frame has been enqueued *since the last flush*.
1107
+ * Used by `readyState` — initial cold-start reports HAVE_NOTHING until
1108
+ * any frame has arrived, and after a seek we want the same semantics
1109
+ * (HAVE_NOTHING until post-seek frames arrive), so the cumulative
1110
+ * `framesPainted > 0` that used to live here was wrong: it kept the
1111
+ * state "true forever" after the first frame ever, so post-seek
1112
+ * `waitForBuffer()` would exit immediately with an empty queue and
1113
+ * leave video frozen while audio kept going.
1114
+ */
1106
1115
  hasFrames() {
1107
- return this.queue.length > 0 || this.framesPainted > 0;
1116
+ return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
1108
1117
  }
1118
+ hasEverEnqueuedSinceFlush = false;
1109
1119
  /** Current depth of the frame queue. Used by the decoder for backpressure. */
1110
1120
  queueDepth() {
1111
1121
  return this.queue.length;
@@ -1124,6 +1134,7 @@ var VideoRenderer = class {
1124
1134
  return;
1125
1135
  }
1126
1136
  this.queue.push(frame);
1137
+ this.hasEverEnqueuedSinceFlush = true;
1127
1138
  if (this.queue.length === 1 && this.framesPainted === 0) {
1128
1139
  this.resolveFirstFrame();
1129
1140
  }
@@ -1257,7 +1268,8 @@ var VideoRenderer = class {
1257
1268
  }
1258
1269
  return;
1259
1270
  }
1260
- const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1271
+ const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
1272
+ const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
1261
1273
  let dropped = 0;
1262
1274
  while (bestIdx > 0) {
1263
1275
  const ts = this.queue[0].timestamp ?? 0;
@@ -1318,16 +1330,28 @@ var VideoRenderer = class {
1318
1330
  while (this.queue.length > 0) this.queue.shift()?.close();
1319
1331
  this.prerolled = false;
1320
1332
  this.ptsCalibrated = false;
1333
+ this.hasEverEnqueuedSinceFlush = false;
1321
1334
  if (isDebug() && count > 0) {
1322
1335
  console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1323
1336
  }
1324
1337
  }
1325
1338
  stats() {
1339
+ let queueSpanMs = 0;
1340
+ let queueHeadMs = 0;
1341
+ let queueTailMs = 0;
1342
+ if (this.queue.length > 0) {
1343
+ queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
1344
+ queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
1345
+ queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
1346
+ }
1326
1347
  return {
1327
1348
  framesPainted: this.framesPainted,
1328
1349
  framesDroppedLate: this.framesDroppedLate,
1329
1350
  framesDroppedOverflow: this.framesDroppedOverflow,
1330
- queueDepth: this.queue.length
1351
+ queueDepth: this.queue.length,
1352
+ queueHeadMs,
1353
+ queueTailMs,
1354
+ queueSpanMs
1331
1355
  };
1332
1356
  }
1333
1357
  destroy() {
@@ -1610,7 +1634,7 @@ async function startHybridDecoder(opts) {
1610
1634
  const variant = pickLibavVariant(opts.context);
1611
1635
  const libav = await loadLibav(variant);
1612
1636
  const bridge = await loadBridge();
1613
- const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
1637
+ const { prepareLibavInput } = await import('./libav-http-reader-2S5HAHW4.js');
1614
1638
  const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
1615
1639
  const readPkt = await libav.av_packet_alloc();
1616
1640
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
@@ -1685,6 +1709,7 @@ async function startHybridDecoder(opts) {
1685
1709
  }
1686
1710
  let bsfCtx = null;
1687
1711
  let bsfPkt = null;
1712
+ let bsfRequiredButMissing = false;
1688
1713
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1689
1714
  try {
1690
1715
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -1695,13 +1720,19 @@ async function startHybridDecoder(opts) {
1695
1720
  bsfPkt = await libav.av_packet_alloc();
1696
1721
  dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1697
1722
  } else {
1698
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1723
+ bsfRequiredButMissing = true;
1699
1724
  bsfCtx = null;
1700
1725
  }
1701
1726
  } catch (err) {
1702
- console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1727
+ bsfRequiredButMissing = true;
1703
1728
  bsfCtx = null;
1704
1729
  bsfPkt = null;
1730
+ dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
1731
+ }
1732
+ if (bsfRequiredButMissing) {
1733
+ console.error(
1734
+ "[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering. Rebuild the libav variant with the `avbsf` fragment included."
1735
+ );
1705
1736
  }
1706
1737
  }
1707
1738
  async function applyBSF(packets) {
@@ -1711,7 +1742,6 @@ async function startHybridDecoder(opts) {
1711
1742
  await libav.ff_copyin_packet(bsfPkt, pkt);
1712
1743
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1713
1744
  if (sendErr < 0) {
1714
- out.push(pkt);
1715
1745
  continue;
1716
1746
  }
1717
1747
  while (true) {
@@ -1725,10 +1755,13 @@ async function startHybridDecoder(opts) {
1725
1755
  async function flushBSF() {
1726
1756
  if (!bsfCtx || !bsfPkt) return;
1727
1757
  try {
1728
- await libav.av_bsf_send_packet(bsfCtx, 0);
1729
- while (true) {
1730
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1731
- if (err < 0) break;
1758
+ if (libav.av_bsf_flush) {
1759
+ await libav.av_bsf_flush(bsfCtx);
1760
+ } else {
1761
+ while (true) {
1762
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1763
+ if (err < 0) break;
1764
+ }
1732
1765
  }
1733
1766
  } catch {
1734
1767
  }
@@ -2040,6 +2073,7 @@ async function startHybridDecoder(opts) {
2040
2073
  videoChunksFed,
2041
2074
  audioFramesDecoded,
2042
2075
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2076
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
2043
2077
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2044
2078
  // Confirmed transport info — see fallback decoder for the pattern.
2045
2079
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -2280,7 +2314,7 @@ async function startDecoder(opts) {
2280
2314
  const variant = "avbridge";
2281
2315
  const libav = await loadLibav(variant);
2282
2316
  const bridge = await loadBridge2();
2283
- const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
2317
+ const { prepareLibavInput } = await import('./libav-http-reader-2S5HAHW4.js');
2284
2318
  const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
2285
2319
  const readPkt = await libav.av_packet_alloc();
2286
2320
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
@@ -2339,6 +2373,7 @@ async function startDecoder(opts) {
2339
2373
  }
2340
2374
  let bsfCtx = null;
2341
2375
  let bsfPkt = null;
2376
+ let bsfRequiredButMissing = false;
2342
2377
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2343
2378
  try {
2344
2379
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -2349,13 +2384,19 @@ async function startDecoder(opts) {
2349
2384
  bsfPkt = await libav.av_packet_alloc();
2350
2385
  dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2351
2386
  } else {
2352
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2387
+ bsfRequiredButMissing = true;
2353
2388
  bsfCtx = null;
2354
2389
  }
2355
2390
  } catch (err) {
2356
- console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2391
+ bsfRequiredButMissing = true;
2357
2392
  bsfCtx = null;
2358
2393
  bsfPkt = null;
2394
+ dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
2395
+ }
2396
+ if (bsfRequiredButMissing) {
2397
+ console.error(
2398
+ "[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering (backwards PTS jumps, heavy late-drop stuttering). Rebuild the libav variant with the `avbsf` fragment included. See docs/dev/POSTMORTEMS.md for details."
2399
+ );
2359
2400
  }
2360
2401
  }
2361
2402
  async function applyBSF(packets) {
@@ -2365,7 +2406,6 @@ async function startDecoder(opts) {
2365
2406
  await libav.ff_copyin_packet(bsfPkt, pkt);
2366
2407
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2367
2408
  if (sendErr < 0) {
2368
- out.push(pkt);
2369
2409
  continue;
2370
2410
  }
2371
2411
  while (true) {
@@ -2379,10 +2419,13 @@ async function startDecoder(opts) {
2379
2419
  async function flushBSF() {
2380
2420
  if (!bsfCtx || !bsfPkt) return;
2381
2421
  try {
2382
- await libav.av_bsf_send_packet(bsfCtx, 0);
2383
- while (true) {
2384
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2385
- if (err < 0) break;
2422
+ if (libav.av_bsf_flush) {
2423
+ await libav.av_bsf_flush(bsfCtx);
2424
+ } else {
2425
+ while (true) {
2426
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2427
+ if (err < 0) break;
2428
+ }
2386
2429
  }
2387
2430
  } catch {
2388
2431
  }
@@ -2400,6 +2443,19 @@ async function startDecoder(opts) {
2400
2443
  let watchdogOverflowWarned = false;
2401
2444
  let syntheticVideoUs = 0;
2402
2445
  let syntheticAudioUs = 0;
2446
+ let videoDecodeMsTotal = 0;
2447
+ let audioDecodeMsTotal = 0;
2448
+ let videoDecodeBatches = 0;
2449
+ let audioDecodeBatches = 0;
2450
+ let readMsTotal = 0;
2451
+ let readBatches = 0;
2452
+ let pumpThrottleMsTotal = 0;
2453
+ let pumpThrottleEntries = 0;
2454
+ let slowestVideoBatchMs = 0;
2455
+ let newestVideoPtsUs = 0;
2456
+ let lastEmittedPtsUs = -1;
2457
+ let ptsRegressions = 0;
2458
+ let worstPtsRegressionMs = 0;
2403
2459
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
2404
2460
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
2405
2461
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -2408,9 +2464,12 @@ async function startDecoder(opts) {
2408
2464
  let readErr;
2409
2465
  let packets;
2410
2466
  try {
2467
+ const _readStart = performance.now();
2411
2468
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
2412
2469
  limit: 16 * 1024
2413
2470
  });
2471
+ readMsTotal += performance.now() - _readStart;
2472
+ readBatches++;
2414
2473
  } catch (err) {
2415
2474
  console.error("[avbridge] ff_read_frame_multi failed:", err);
2416
2475
  return;
@@ -2472,8 +2531,17 @@ async function startDecoder(opts) {
2472
2531
  }
2473
2532
  }
2474
2533
  }
2475
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2476
- await new Promise((r) => setTimeout(r, 50));
2534
+ {
2535
+ const _throttleStart = performance.now();
2536
+ let _throttled = false;
2537
+ while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2538
+ _throttled = true;
2539
+ await new Promise((r) => setTimeout(r, 50));
2540
+ }
2541
+ if (_throttled) {
2542
+ pumpThrottleMsTotal += performance.now() - _throttleStart;
2543
+ pumpThrottleEntries++;
2544
+ }
2477
2545
  }
2478
2546
  if (readErr === libav.AVERROR_EOF) {
2479
2547
  if (videoDec) await decodeVideoBatch(
@@ -2499,6 +2567,7 @@ async function startDecoder(opts) {
2499
2567
  async function decodeVideoBatch(pkts, myToken, flush = false) {
2500
2568
  if (!videoDec || destroyed || myToken !== pumpToken) return;
2501
2569
  let frames;
2570
+ const _t0 = performance.now();
2502
2571
  try {
2503
2572
  frames = await libav.ff_decode_multi(
2504
2573
  videoDec.c,
@@ -2511,18 +2580,38 @@ async function startDecoder(opts) {
2511
2580
  console.error("[avbridge] video decode batch failed:", err);
2512
2581
  return;
2513
2582
  }
2583
+ {
2584
+ const _dt = performance.now() - _t0;
2585
+ videoDecodeMsTotal += _dt;
2586
+ videoDecodeBatches++;
2587
+ if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
2588
+ }
2514
2589
  if (myToken !== pumpToken || destroyed) return;
2515
2590
  for (const f of frames) {
2516
2591
  if (myToken !== pumpToken || destroyed) return;
2517
2592
  sanitizeFrameTimestamp(
2518
2593
  f,
2519
2594
  () => {
2520
- const ts = syntheticVideoUs;
2521
- syntheticVideoUs += videoFrameStepUs;
2522
- return ts;
2595
+ const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
2596
+ syntheticVideoUs = base + videoFrameStepUs;
2597
+ return base;
2523
2598
  },
2524
2599
  videoTimeBase
2525
2600
  );
2601
+ const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
2602
+ if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
2603
+ if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
2604
+ ptsRegressions++;
2605
+ const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
2606
+ if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
2607
+ if (ptsRegressions <= 10) {
2608
+ console.warn(
2609
+ `[avbridge:decoder] dropped out-of-order frame #${ptsRegressions}: pts=${(_fPts / 1e3).toFixed(1)}ms < previous=${(lastEmittedPtsUs / 1e3).toFixed(1)}ms (regression=${regressMs.toFixed(1)}ms). Typically a post-seek B-frame reorder tail.`
2610
+ );
2611
+ }
2612
+ continue;
2613
+ }
2614
+ lastEmittedPtsUs = _fPts;
2526
2615
  try {
2527
2616
  const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2528
2617
  opts.renderer.enqueue(vf);
@@ -2537,6 +2626,7 @@ async function startDecoder(opts) {
2537
2626
  async function decodeAudioBatch(pkts, myToken, flush = false) {
2538
2627
  if (!audioDec || destroyed || myToken !== pumpToken) return;
2539
2628
  let frames;
2629
+ const _t0 = performance.now();
2540
2630
  try {
2541
2631
  frames = await libav.ff_decode_multi(
2542
2632
  audioDec.c,
@@ -2549,6 +2639,8 @@ async function startDecoder(opts) {
2549
2639
  console.error("[avbridge] audio decode batch failed:", err);
2550
2640
  return;
2551
2641
  }
2642
+ audioDecodeMsTotal += performance.now() - _t0;
2643
+ audioDecodeBatches++;
2552
2644
  if (myToken !== pumpToken || destroyed) return;
2553
2645
  for (const f of frames) {
2554
2646
  if (myToken !== pumpToken || destroyed) return;
@@ -2670,6 +2762,7 @@ async function startDecoder(opts) {
2670
2762
  await flushBSF();
2671
2763
  syntheticVideoUs = Math.round(timeSec * 1e6);
2672
2764
  syntheticAudioUs = Math.round(timeSec * 1e6);
2765
+ lastEmittedPtsUs = -1;
2673
2766
  pumpRunning = pumpLoop(newToken).catch(
2674
2767
  (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2675
2768
  );
@@ -2707,6 +2800,7 @@ async function startDecoder(opts) {
2707
2800
  await flushBSF();
2708
2801
  syntheticVideoUs = Math.round(timeSec * 1e6);
2709
2802
  syntheticAudioUs = Math.round(timeSec * 1e6);
2803
+ lastEmittedPtsUs = -1;
2710
2804
  pumpRunning = pumpLoop(newToken).catch(
2711
2805
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
2712
2806
  );
@@ -2720,7 +2814,24 @@ async function startDecoder(opts) {
2720
2814
  packetsRead,
2721
2815
  videoFramesDecoded,
2722
2816
  audioFramesDecoded,
2817
+ // Throughput instrumentation — the stats panel turns these into
2818
+ // "decode fps actual / realtime target" and shows slowest batch
2819
+ // + producer throttle share.
2820
+ videoDecodeMsTotal,
2821
+ videoDecodeBatches,
2822
+ audioDecodeMsTotal,
2823
+ audioDecodeBatches,
2824
+ readMsTotal,
2825
+ readBatches,
2826
+ pumpThrottleMsTotal,
2827
+ pumpThrottleEntries,
2828
+ slowestVideoBatchMs,
2829
+ newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
2830
+ ptsRegressions,
2831
+ worstPtsRegressionMs,
2832
+ sourceFps: videoFps,
2723
2833
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2834
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
2724
2835
  // Confirmed transport info: once prepareLibavInput returns
2725
2836
  // successfully, we *know* whether the source is http-range (probe
2726
2837
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -3009,9 +3120,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
3009
3120
  constructor(options, registry) {
3010
3121
  this.options = options;
3011
3122
  this.registry = registry;
3012
- const { requestInit, fetchFn } = options;
3013
- if (requestInit || fetchFn) {
3014
- this.transport = { requestInit, fetchFn };
3123
+ const { requestInit, fetchFn, cacheBytes } = options;
3124
+ if (requestInit || fetchFn || cacheBytes !== void 0) {
3125
+ this.transport = { requestInit, fetchFn, cacheBytes };
3015
3126
  }
3016
3127
  }
3017
3128
  options;
@@ -3543,5 +3654,5 @@ function defaultFallbackChain(strategy) {
3543
3654
  }
3544
3655
 
3545
3656
  export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
3546
- //# sourceMappingURL=chunk-IHNHHEA2.js.map
3547
- //# sourceMappingURL=chunk-IHNHHEA2.js.map
3657
+ //# sourceMappingURL=chunk-BN7BRTLY.js.map
3658
+ //# sourceMappingURL=chunk-BN7BRTLY.js.map