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
@@ -29513,7 +29513,8 @@ async function prepareLibavInput(libav, filename, source, transport) {
29513
29513
  if (source.kind === "url") {
29514
29514
  const handle = await attachLibavHttpReader(libav, filename, source.url, {
29515
29515
  requestInit: transport?.requestInit,
29516
- fetchFn: transport?.fetchFn
29516
+ fetchFn: transport?.fetchFn,
29517
+ cacheBytes: transport?.cacheBytes
29517
29518
  });
29518
29519
  return {
29519
29520
  filename,
@@ -29575,7 +29576,9 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29575
29576
  }
29576
29577
  await libav.mkblockreaderdev(filename, size);
29577
29578
  let detached = false;
29578
- let cached = null;
29579
+ const cache2 = /* @__PURE__ */ new Map();
29580
+ let cacheBytes = 0;
29581
+ const cacheBudget = Math.max(0, options.cacheBytes ?? DEFAULT_CACHE_BYTES);
29579
29582
  let inflight = null;
29580
29583
  function clampReadLength(requested) {
29581
29584
  const doubled = requested * 2;
@@ -29583,14 +29586,33 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29583
29586
  if (doubled > MAX_READ) return MAX_READ;
29584
29587
  return doubled;
29585
29588
  }
29586
- function cacheCovers(pos, length) {
29587
- if (!cached) return false;
29588
- return pos >= cached.pos && pos + length <= cached.pos + cached.bytes.byteLength;
29589
+ function cacheLookup(pos, length) {
29590
+ for (const [blockPos, bytes2] of cache2) {
29591
+ if (pos >= blockPos && pos + length <= blockPos + bytes2.byteLength) {
29592
+ cache2.delete(blockPos);
29593
+ cache2.set(blockPos, bytes2);
29594
+ const offset = pos - blockPos;
29595
+ return bytes2.subarray(offset, offset + length);
29596
+ }
29597
+ }
29598
+ return null;
29589
29599
  }
29590
- function sliceFromCache(pos, length) {
29591
- if (!cached) throw new Error("sliceFromCache called with no cache");
29592
- const offset = pos - cached.pos;
29593
- return cached.bytes.subarray(offset, offset + length);
29600
+ function cacheInsert(pos, bytes2) {
29601
+ const existing = cache2.get(pos);
29602
+ if (existing) {
29603
+ cacheBytes -= existing.byteLength;
29604
+ cache2.delete(pos);
29605
+ }
29606
+ cache2.set(pos, bytes2);
29607
+ cacheBytes += bytes2.byteLength;
29608
+ while (cacheBytes > cacheBudget && cache2.size > 0) {
29609
+ const oldestKey = cache2.keys().next().value;
29610
+ if (oldestKey === void 0) break;
29611
+ const oldest = cache2.get(oldestKey);
29612
+ if (!oldest) break;
29613
+ cache2.delete(oldestKey);
29614
+ cacheBytes -= oldest.byteLength;
29615
+ }
29594
29616
  }
29595
29617
  async function fetchRange(pos, length) {
29596
29618
  const end = Math.min(pos + length - 1, size - 1);
@@ -29607,7 +29629,7 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29607
29629
  );
29608
29630
  }
29609
29631
  const buf = new Uint8Array(await res.arrayBuffer());
29610
- cached = { pos, bytes: buf };
29632
+ cacheInsert(pos, buf);
29611
29633
  return buf;
29612
29634
  }
29613
29635
  async function handleRead(name, pos, length) {
@@ -29618,10 +29640,10 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29618
29640
  }
29619
29641
  }
29620
29642
  if (detached) return;
29621
- if (cacheCovers(pos, length)) {
29622
- const data = sliceFromCache(pos, length);
29643
+ const hit = cacheLookup(pos, length);
29644
+ if (hit) {
29623
29645
  try {
29624
- await libav.ff_block_reader_dev_send(name, pos, data);
29646
+ await libav.ff_block_reader_dev_send(name, pos, hit);
29625
29647
  } catch {
29626
29648
  }
29627
29649
  return;
@@ -29674,7 +29696,8 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29674
29696
  } catch {
29675
29697
  }
29676
29698
  }
29677
- cached = null;
29699
+ cache2.clear();
29700
+ cacheBytes = 0;
29678
29701
  try {
29679
29702
  await libav.unlinkreadaheadfile(filename);
29680
29703
  } catch {
@@ -29682,11 +29705,12 @@ async function attachLibavHttpReader(libav, filename, url2, options = {}) {
29682
29705
  }
29683
29706
  };
29684
29707
  }
29685
- var MIN_READ, MAX_READ;
29708
+ var MIN_READ, MAX_READ, DEFAULT_CACHE_BYTES;
29686
29709
  var init_libav_http_reader = __esm({
29687
29710
  "src/util/libav-http-reader.ts"() {
29688
29711
  MIN_READ = 256 * 1024;
29689
29712
  MAX_READ = 1 * 1024 * 1024;
29713
+ DEFAULT_CACHE_BYTES = 8 * 1024 * 1024;
29690
29714
  }
29691
29715
  });
29692
29716
 
@@ -29933,7 +29957,7 @@ async function probeWithLibav(source, sniffed) {
29933
29957
  codec: ffmpegToAvbridgeVideo(codecName),
29934
29958
  width: codecpar?.width ?? 0,
29935
29959
  height: codecpar?.height ?? 0,
29936
- fps: framerate(stream)
29960
+ fps: await framerate(libav, stream)
29937
29961
  });
29938
29962
  } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
29939
29963
  audioTracks.push({
@@ -29961,7 +29985,7 @@ async function probeWithLibav(source, sniffed) {
29961
29985
  duration
29962
29986
  };
29963
29987
  }
29964
- function framerate(stream) {
29988
+ async function framerate(libav, stream) {
29965
29989
  if (typeof stream.avg_frame_rate_num === "number" && stream.avg_frame_rate_den) {
29966
29990
  return stream.avg_frame_rate_num / stream.avg_frame_rate_den;
29967
29991
  }
@@ -29969,6 +29993,14 @@ function framerate(stream) {
29969
29993
  if (stream.avg_frame_rate.den === 0) return void 0;
29970
29994
  return stream.avg_frame_rate.num / stream.avg_frame_rate.den;
29971
29995
  }
29996
+ try {
29997
+ const num = await libav.AVCodecParameters_framerate_num?.(stream.codecpar);
29998
+ const den = await libav.AVCodecParameters_framerate_den?.(stream.codecpar);
29999
+ if (typeof num === "number" && typeof den === "number" && den > 0 && num > 0) {
30000
+ return num / den;
30001
+ }
30002
+ } catch {
30003
+ }
29972
30004
  return void 0;
29973
30005
  }
29974
30006
  async function safeDuration(libav, fmt_ctx) {
@@ -32537,10 +32569,20 @@ var VideoRenderer = class {
32537
32569
  /** Resolves once the first decoded frame has been enqueued. */
32538
32570
  firstFrameReady;
32539
32571
  resolveFirstFrame;
32540
- /** True once at least one frame has been enqueued. */
32572
+ /**
32573
+ * True once at least one frame has been enqueued *since the last flush*.
32574
+ * Used by `readyState` — initial cold-start reports HAVE_NOTHING until
32575
+ * any frame has arrived, and after a seek we want the same semantics
32576
+ * (HAVE_NOTHING until post-seek frames arrive), so the cumulative
32577
+ * `framesPainted > 0` that used to live here was wrong: it kept the
32578
+ * state "true forever" after the first frame ever, so post-seek
32579
+ * `waitForBuffer()` would exit immediately with an empty queue and
32580
+ * leave video frozen while audio kept going.
32581
+ */
32541
32582
  hasFrames() {
32542
- return this.queue.length > 0 || this.framesPainted > 0;
32583
+ return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
32543
32584
  }
32585
+ hasEverEnqueuedSinceFlush = false;
32544
32586
  /** Current depth of the frame queue. Used by the decoder for backpressure. */
32545
32587
  queueDepth() {
32546
32588
  return this.queue.length;
@@ -32559,6 +32601,7 @@ var VideoRenderer = class {
32559
32601
  return;
32560
32602
  }
32561
32603
  this.queue.push(frame);
32604
+ this.hasEverEnqueuedSinceFlush = true;
32562
32605
  if (this.queue.length === 1 && this.framesPainted === 0) {
32563
32606
  this.resolveFirstFrame();
32564
32607
  }
@@ -32692,7 +32735,8 @@ var VideoRenderer = class {
32692
32735
  }
32693
32736
  return;
32694
32737
  }
32695
- const dropThresholdUs = audioNowUs - frameDurationUs * 2;
32738
+ const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
32739
+ const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
32696
32740
  let dropped = 0;
32697
32741
  while (bestIdx > 0) {
32698
32742
  const ts = this.queue[0].timestamp ?? 0;
@@ -32753,16 +32797,28 @@ var VideoRenderer = class {
32753
32797
  while (this.queue.length > 0) this.queue.shift()?.close();
32754
32798
  this.prerolled = false;
32755
32799
  this.ptsCalibrated = false;
32800
+ this.hasEverEnqueuedSinceFlush = false;
32756
32801
  if (isDebug() && count > 0) {
32757
32802
  console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
32758
32803
  }
32759
32804
  }
32760
32805
  stats() {
32806
+ let queueSpanMs = 0;
32807
+ let queueHeadMs = 0;
32808
+ let queueTailMs = 0;
32809
+ if (this.queue.length > 0) {
32810
+ queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
32811
+ queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
32812
+ queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
32813
+ }
32761
32814
  return {
32762
32815
  framesPainted: this.framesPainted,
32763
32816
  framesDroppedLate: this.framesDroppedLate,
32764
32817
  framesDroppedOverflow: this.framesDroppedOverflow,
32765
- queueDepth: this.queue.length
32818
+ queueDepth: this.queue.length,
32819
+ queueHeadMs,
32820
+ queueTailMs,
32821
+ queueSpanMs
32766
32822
  };
32767
32823
  }
32768
32824
  destroy() {
@@ -33304,6 +33360,7 @@ async function startHybridDecoder(opts) {
33304
33360
  }
33305
33361
  let bsfCtx = null;
33306
33362
  let bsfPkt = null;
33363
+ let bsfRequiredButMissing = false;
33307
33364
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
33308
33365
  try {
33309
33366
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -33314,13 +33371,19 @@ async function startHybridDecoder(opts) {
33314
33371
  bsfPkt = await libav.av_packet_alloc();
33315
33372
  dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
33316
33373
  } else {
33317
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
33374
+ bsfRequiredButMissing = true;
33318
33375
  bsfCtx = null;
33319
33376
  }
33320
33377
  } catch (err) {
33321
- console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
33378
+ bsfRequiredButMissing = true;
33322
33379
  bsfCtx = null;
33323
33380
  bsfPkt = null;
33381
+ dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
33382
+ }
33383
+ if (bsfRequiredButMissing) {
33384
+ console.error(
33385
+ "[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."
33386
+ );
33324
33387
  }
33325
33388
  }
33326
33389
  async function applyBSF(packets) {
@@ -33330,7 +33393,6 @@ async function startHybridDecoder(opts) {
33330
33393
  await libav.ff_copyin_packet(bsfPkt, pkt);
33331
33394
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
33332
33395
  if (sendErr < 0) {
33333
- out.push(pkt);
33334
33396
  continue;
33335
33397
  }
33336
33398
  while (true) {
@@ -33344,10 +33406,13 @@ async function startHybridDecoder(opts) {
33344
33406
  async function flushBSF() {
33345
33407
  if (!bsfCtx || !bsfPkt) return;
33346
33408
  try {
33347
- await libav.av_bsf_send_packet(bsfCtx, 0);
33348
- while (true) {
33349
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
33350
- if (err < 0) break;
33409
+ if (libav.av_bsf_flush) {
33410
+ await libav.av_bsf_flush(bsfCtx);
33411
+ } else {
33412
+ while (true) {
33413
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
33414
+ if (err < 0) break;
33415
+ }
33351
33416
  }
33352
33417
  } catch {
33353
33418
  }
@@ -33659,6 +33724,7 @@ async function startHybridDecoder(opts) {
33659
33724
  videoChunksFed,
33660
33725
  audioFramesDecoded,
33661
33726
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
33727
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
33662
33728
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
33663
33729
  // Confirmed transport info — see fallback decoder for the pattern.
33664
33730
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -33960,6 +34026,7 @@ async function startDecoder(opts) {
33960
34026
  }
33961
34027
  let bsfCtx = null;
33962
34028
  let bsfPkt = null;
34029
+ let bsfRequiredButMissing = false;
33963
34030
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
33964
34031
  try {
33965
34032
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -33970,13 +34037,19 @@ async function startDecoder(opts) {
33970
34037
  bsfPkt = await libav.av_packet_alloc();
33971
34038
  dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
33972
34039
  } else {
33973
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
34040
+ bsfRequiredButMissing = true;
33974
34041
  bsfCtx = null;
33975
34042
  }
33976
34043
  } catch (err) {
33977
- console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
34044
+ bsfRequiredButMissing = true;
33978
34045
  bsfCtx = null;
33979
34046
  bsfPkt = null;
34047
+ dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
34048
+ }
34049
+ if (bsfRequiredButMissing) {
34050
+ console.error(
34051
+ "[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."
34052
+ );
33980
34053
  }
33981
34054
  }
33982
34055
  async function applyBSF(packets) {
@@ -33986,7 +34059,6 @@ async function startDecoder(opts) {
33986
34059
  await libav.ff_copyin_packet(bsfPkt, pkt);
33987
34060
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
33988
34061
  if (sendErr < 0) {
33989
- out.push(pkt);
33990
34062
  continue;
33991
34063
  }
33992
34064
  while (true) {
@@ -34000,10 +34072,13 @@ async function startDecoder(opts) {
34000
34072
  async function flushBSF() {
34001
34073
  if (!bsfCtx || !bsfPkt) return;
34002
34074
  try {
34003
- await libav.av_bsf_send_packet(bsfCtx, 0);
34004
- while (true) {
34005
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
34006
- if (err < 0) break;
34075
+ if (libav.av_bsf_flush) {
34076
+ await libav.av_bsf_flush(bsfCtx);
34077
+ } else {
34078
+ while (true) {
34079
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
34080
+ if (err < 0) break;
34081
+ }
34007
34082
  }
34008
34083
  } catch {
34009
34084
  }
@@ -34021,6 +34096,19 @@ async function startDecoder(opts) {
34021
34096
  let watchdogOverflowWarned = false;
34022
34097
  let syntheticVideoUs = 0;
34023
34098
  let syntheticAudioUs = 0;
34099
+ let videoDecodeMsTotal = 0;
34100
+ let audioDecodeMsTotal = 0;
34101
+ let videoDecodeBatches = 0;
34102
+ let audioDecodeBatches = 0;
34103
+ let readMsTotal = 0;
34104
+ let readBatches = 0;
34105
+ let pumpThrottleMsTotal = 0;
34106
+ let pumpThrottleEntries = 0;
34107
+ let slowestVideoBatchMs = 0;
34108
+ let newestVideoPtsUs = 0;
34109
+ let lastEmittedPtsUs = -1;
34110
+ let ptsRegressions = 0;
34111
+ let worstPtsRegressionMs = 0;
34024
34112
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
34025
34113
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
34026
34114
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -34029,9 +34117,12 @@ async function startDecoder(opts) {
34029
34117
  let readErr;
34030
34118
  let packets;
34031
34119
  try {
34120
+ const _readStart = performance.now();
34032
34121
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
34033
34122
  limit: 16 * 1024
34034
34123
  });
34124
+ readMsTotal += performance.now() - _readStart;
34125
+ readBatches++;
34035
34126
  } catch (err) {
34036
34127
  console.error("[avbridge] ff_read_frame_multi failed:", err);
34037
34128
  return;
@@ -34093,8 +34184,17 @@ async function startDecoder(opts) {
34093
34184
  }
34094
34185
  }
34095
34186
  }
34096
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
34097
- await new Promise((r) => setTimeout(r, 50));
34187
+ {
34188
+ const _throttleStart = performance.now();
34189
+ let _throttled = false;
34190
+ while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
34191
+ _throttled = true;
34192
+ await new Promise((r) => setTimeout(r, 50));
34193
+ }
34194
+ if (_throttled) {
34195
+ pumpThrottleMsTotal += performance.now() - _throttleStart;
34196
+ pumpThrottleEntries++;
34197
+ }
34098
34198
  }
34099
34199
  if (readErr === libav.AVERROR_EOF) {
34100
34200
  if (videoDec) await decodeVideoBatch(
@@ -34120,6 +34220,7 @@ async function startDecoder(opts) {
34120
34220
  async function decodeVideoBatch(pkts, myToken, flush = false) {
34121
34221
  if (!videoDec || destroyed || myToken !== pumpToken) return;
34122
34222
  let frames;
34223
+ const _t0 = performance.now();
34123
34224
  try {
34124
34225
  frames = await libav.ff_decode_multi(
34125
34226
  videoDec.c,
@@ -34132,18 +34233,38 @@ async function startDecoder(opts) {
34132
34233
  console.error("[avbridge] video decode batch failed:", err);
34133
34234
  return;
34134
34235
  }
34236
+ {
34237
+ const _dt = performance.now() - _t0;
34238
+ videoDecodeMsTotal += _dt;
34239
+ videoDecodeBatches++;
34240
+ if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
34241
+ }
34135
34242
  if (myToken !== pumpToken || destroyed) return;
34136
34243
  for (const f of frames) {
34137
34244
  if (myToken !== pumpToken || destroyed) return;
34138
34245
  sanitizeFrameTimestamp(
34139
34246
  f,
34140
34247
  () => {
34141
- const ts = syntheticVideoUs;
34142
- syntheticVideoUs += videoFrameStepUs;
34143
- return ts;
34248
+ const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
34249
+ syntheticVideoUs = base + videoFrameStepUs;
34250
+ return base;
34144
34251
  },
34145
34252
  videoTimeBase
34146
34253
  );
34254
+ const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
34255
+ if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
34256
+ if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
34257
+ ptsRegressions++;
34258
+ const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
34259
+ if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
34260
+ if (ptsRegressions <= 10) {
34261
+ console.warn(
34262
+ `[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.`
34263
+ );
34264
+ }
34265
+ continue;
34266
+ }
34267
+ lastEmittedPtsUs = _fPts;
34147
34268
  try {
34148
34269
  const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
34149
34270
  opts.renderer.enqueue(vf);
@@ -34158,6 +34279,7 @@ async function startDecoder(opts) {
34158
34279
  async function decodeAudioBatch(pkts, myToken, flush = false) {
34159
34280
  if (!audioDec || destroyed || myToken !== pumpToken) return;
34160
34281
  let frames;
34282
+ const _t0 = performance.now();
34161
34283
  try {
34162
34284
  frames = await libav.ff_decode_multi(
34163
34285
  audioDec.c,
@@ -34170,6 +34292,8 @@ async function startDecoder(opts) {
34170
34292
  console.error("[avbridge] audio decode batch failed:", err);
34171
34293
  return;
34172
34294
  }
34295
+ audioDecodeMsTotal += performance.now() - _t0;
34296
+ audioDecodeBatches++;
34173
34297
  if (myToken !== pumpToken || destroyed) return;
34174
34298
  for (const f of frames) {
34175
34299
  if (myToken !== pumpToken || destroyed) return;
@@ -34291,6 +34415,7 @@ async function startDecoder(opts) {
34291
34415
  await flushBSF();
34292
34416
  syntheticVideoUs = Math.round(timeSec * 1e6);
34293
34417
  syntheticAudioUs = Math.round(timeSec * 1e6);
34418
+ lastEmittedPtsUs = -1;
34294
34419
  pumpRunning = pumpLoop(newToken).catch(
34295
34420
  (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
34296
34421
  );
@@ -34328,6 +34453,7 @@ async function startDecoder(opts) {
34328
34453
  await flushBSF();
34329
34454
  syntheticVideoUs = Math.round(timeSec * 1e6);
34330
34455
  syntheticAudioUs = Math.round(timeSec * 1e6);
34456
+ lastEmittedPtsUs = -1;
34331
34457
  pumpRunning = pumpLoop(newToken).catch(
34332
34458
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
34333
34459
  );
@@ -34341,7 +34467,24 @@ async function startDecoder(opts) {
34341
34467
  packetsRead,
34342
34468
  videoFramesDecoded,
34343
34469
  audioFramesDecoded,
34470
+ // Throughput instrumentation — the stats panel turns these into
34471
+ // "decode fps actual / realtime target" and shows slowest batch
34472
+ // + producer throttle share.
34473
+ videoDecodeMsTotal,
34474
+ videoDecodeBatches,
34475
+ audioDecodeMsTotal,
34476
+ audioDecodeBatches,
34477
+ readMsTotal,
34478
+ readBatches,
34479
+ pumpThrottleMsTotal,
34480
+ pumpThrottleEntries,
34481
+ slowestVideoBatchMs,
34482
+ newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
34483
+ ptsRegressions,
34484
+ worstPtsRegressionMs,
34485
+ sourceFps: videoFps,
34344
34486
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
34487
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
34345
34488
  // Confirmed transport info: once prepareLibavInput returns
34346
34489
  // successfully, we *know* whether the source is http-range (probe
34347
34490
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -34634,9 +34777,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
34634
34777
  constructor(options, registry) {
34635
34778
  this.options = options;
34636
34779
  this.registry = registry;
34637
- const { requestInit, fetchFn } = options;
34638
- if (requestInit || fetchFn) {
34639
- this.transport = { requestInit, fetchFn };
34780
+ const { requestInit, fetchFn, cacheBytes } = options;
34781
+ if (requestInit || fetchFn || cacheBytes !== void 0) {
34782
+ this.transport = { requestInit, fetchFn, cacheBytes };
34640
34783
  }
34641
34784
  }
34642
34785
  options;