avbridge 2.1.2 → 2.2.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 (67) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +98 -71
  3. package/dist/{avi-GNTV5ZOH.cjs → avi-6SJLWIWW.cjs} +19 -4
  4. package/dist/avi-6SJLWIWW.cjs.map +1 -0
  5. package/dist/{avi-V6HYQVR2.js → avi-GCGM7OJI.js} +18 -3
  6. package/dist/avi-GCGM7OJI.js.map +1 -0
  7. package/dist/{chunk-EJH67FXG.js → chunk-5DMTJVIU.js} +99 -3
  8. package/dist/chunk-5DMTJVIU.js.map +1 -0
  9. package/dist/{chunk-3AUGRKPY.js → chunk-DMWARSEF.js} +160 -27
  10. package/dist/chunk-DMWARSEF.js.map +1 -0
  11. package/dist/{chunk-JQH6D4OE.cjs → chunk-G4APZMCP.cjs} +100 -3
  12. package/dist/chunk-G4APZMCP.cjs.map +1 -0
  13. package/dist/{chunk-Y5FYF5KG.cjs → chunk-HZLQNKFN.cjs} +5 -2
  14. package/dist/chunk-HZLQNKFN.cjs.map +1 -0
  15. package/dist/{chunk-PQTZS7OA.js → chunk-ILKDNBSE.js} +5 -2
  16. package/dist/chunk-ILKDNBSE.js.map +1 -0
  17. package/dist/{chunk-DPVIOYGC.cjs → chunk-UF2N5L63.cjs} +164 -31
  18. package/dist/chunk-UF2N5L63.cjs.map +1 -0
  19. package/dist/element-browser.js +276 -21
  20. package/dist/element-browser.js.map +1 -1
  21. package/dist/element.cjs +4 -4
  22. package/dist/element.d.cts +1 -1
  23. package/dist/element.d.ts +1 -1
  24. package/dist/element.js +3 -3
  25. package/dist/index.cjs +18 -18
  26. package/dist/index.d.cts +2 -2
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +5 -5
  29. package/dist/libav-loader-27RDIN2I.js +3 -0
  30. package/dist/{libav-loader-XKH2TKUW.js.map → libav-loader-27RDIN2I.js.map} +1 -1
  31. package/dist/libav-loader-IV4AJ2HW.cjs +12 -0
  32. package/dist/{libav-loader-6APXVNIV.cjs.map → libav-loader-IV4AJ2HW.cjs.map} +1 -1
  33. package/dist/{player-BdtUG4rh.d.cts → player-U2NPmFvA.d.cts} +4 -3
  34. package/dist/{player-BdtUG4rh.d.ts → player-U2NPmFvA.d.ts} +4 -3
  35. package/dist/source-CN43EI7Z.cjs +28 -0
  36. package/dist/{source-SC6ZEQYR.cjs.map → source-CN43EI7Z.cjs.map} +1 -1
  37. package/dist/source-FFZ7TW2B.js +3 -0
  38. package/dist/{source-ZFS4H7J3.js.map → source-FFZ7TW2B.js.map} +1 -1
  39. package/package.json +1 -1
  40. package/src/classify/rules.ts +9 -2
  41. package/src/player.ts +46 -17
  42. package/src/probe/avi.ts +8 -1
  43. package/src/strategies/fallback/audio-output.ts +25 -3
  44. package/src/strategies/fallback/decoder.ts +96 -8
  45. package/src/strategies/fallback/index.ts +98 -6
  46. package/src/strategies/fallback/libav-loader.ts +12 -0
  47. package/src/strategies/fallback/video-renderer.ts +5 -1
  48. package/src/strategies/hybrid/index.ts +9 -1
  49. package/src/strategies/remux/index.ts +13 -1
  50. package/src/strategies/remux/pipeline.ts +6 -0
  51. package/src/types.ts +10 -1
  52. package/src/util/debug.ts +131 -0
  53. package/src/util/source.ts +4 -0
  54. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  55. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  56. package/dist/avi-GNTV5ZOH.cjs.map +0 -1
  57. package/dist/avi-V6HYQVR2.js.map +0 -1
  58. package/dist/chunk-3AUGRKPY.js.map +0 -1
  59. package/dist/chunk-DPVIOYGC.cjs.map +0 -1
  60. package/dist/chunk-EJH67FXG.js.map +0 -1
  61. package/dist/chunk-JQH6D4OE.cjs.map +0 -1
  62. package/dist/chunk-PQTZS7OA.js.map +0 -1
  63. package/dist/chunk-Y5FYF5KG.cjs.map +0 -1
  64. package/dist/libav-loader-6APXVNIV.cjs +0 -12
  65. package/dist/libav-loader-XKH2TKUW.js +0 -3
  66. package/dist/source-SC6ZEQYR.cjs +0 -28
  67. package/dist/source-ZFS4H7J3.js +0 -3
@@ -145,6 +145,9 @@ function sniffContainerFromBytes(head) {
145
145
  }
146
146
  if (head[0] === 48 && head[1] === 38 && head[2] === 178 && head[3] === 117 && head[4] === 142 && head[5] === 102 && head[6] === 207 && head[7] === 17) return "asf";
147
147
  if (head[0] === 70 && head[1] === 76 && head[2] === 86) return "flv";
148
+ if (head[0] === 46 && head[1] === 82 && head[2] === 77 && head[3] === 70) {
149
+ return "rm";
150
+ }
148
151
  if (head[0] === 79 && head[1] === 103 && head[2] === 103 && head[3] === 83) return "ogg";
149
152
  if (head[0] === 102 && head[1] === 76 && head[2] === 97 && head[3] === 67) return "flac";
150
153
  if (head[0] === 73 && head[1] === 68 && head[2] === 51) return "mp3";
@@ -29628,6 +29631,98 @@ var init_libav_http_reader = __esm({
29628
29631
  }
29629
29632
  });
29630
29633
 
29634
+ // src/util/debug.ts
29635
+ function isDebugEnabled() {
29636
+ if (typeof globalThis === "undefined") return false;
29637
+ const g = globalThis;
29638
+ if (g.AVBRIDGE_DEBUG === true) return true;
29639
+ if (typeof location !== "undefined" && typeof URLSearchParams !== "undefined") {
29640
+ try {
29641
+ const p = new URLSearchParams(location.search);
29642
+ if (p.has("avbridge_debug")) {
29643
+ g.AVBRIDGE_DEBUG = true;
29644
+ return true;
29645
+ }
29646
+ } catch {
29647
+ }
29648
+ }
29649
+ return false;
29650
+ }
29651
+ function fmt(tag) {
29652
+ return `[avbridge:${tag}]`;
29653
+ }
29654
+ function hintForTag(tag) {
29655
+ switch (tag) {
29656
+ case "probe":
29657
+ return "slow network (range request), large sniff window, or libav cold-start";
29658
+ case "libav-load":
29659
+ return "large .wasm download, misconfigured AVBRIDGE_LIBAV_BASE, or server-side MIME type";
29660
+ case "bootstrap":
29661
+ return "probe+classify+strategy-init chain; enable AVBRIDGE_DEBUG for a phase breakdown";
29662
+ case "cold-start":
29663
+ return "decoder is producing output slower than realtime \u2014 check framesDecoded in getDiagnostics()";
29664
+ default:
29665
+ return "unknown stage \u2014 enable globalThis.AVBRIDGE_DEBUG for more detail";
29666
+ }
29667
+ }
29668
+ var dbg;
29669
+ var init_debug = __esm({
29670
+ "src/util/debug.ts"() {
29671
+ dbg = {
29672
+ /** Verbose — only when debug is enabled. The hot-path normal case. */
29673
+ info(tag, ...args) {
29674
+ if (isDebugEnabled()) console.info(fmt(tag), ...args);
29675
+ },
29676
+ /** Warning — only when debug is enabled. Non-fatal oddities. */
29677
+ warn(tag, ...args) {
29678
+ if (isDebugEnabled()) console.warn(fmt(tag), ...args);
29679
+ },
29680
+ /**
29681
+ * Self-diagnosis warning. **Always** emits regardless of debug flag.
29682
+ * Use this only for conditions that mean something is actually wrong
29683
+ * or degraded — not for routine chatter.
29684
+ */
29685
+ diag(tag, ...args) {
29686
+ console.warn(fmt(tag), ...args);
29687
+ },
29688
+ /**
29689
+ * Timing helper: wraps an async call and logs its elapsed time when
29690
+ * debug is on. The callback runs whether debug is on or off — this is
29691
+ * just for the `dbg.info` at the end.
29692
+ *
29693
+ * Also unconditionally fires `dbg.diag` if the elapsed time exceeds
29694
+ * `slowMs`, so "the bootstrap took 8 seconds" shows up even without
29695
+ * debug mode enabled.
29696
+ */
29697
+ async timed(tag, label, slowMs, fn) {
29698
+ const start = performance.now();
29699
+ try {
29700
+ const result = await fn();
29701
+ const elapsed = performance.now() - start;
29702
+ if (isDebugEnabled()) {
29703
+ console.info(fmt(tag), `${label} ${elapsed.toFixed(0)}ms`);
29704
+ }
29705
+ if (elapsed > slowMs) {
29706
+ console.warn(
29707
+ fmt(tag),
29708
+ `${label} took ${elapsed.toFixed(0)}ms (>${slowMs}ms expected) \u2014 this is unusually slow; possible causes: ${hintForTag(tag)}`
29709
+ );
29710
+ }
29711
+ return result;
29712
+ } catch (err) {
29713
+ const elapsed = performance.now() - start;
29714
+ console.warn(
29715
+ fmt(tag),
29716
+ `${label} FAILED after ${elapsed.toFixed(0)}ms:`,
29717
+ err
29718
+ );
29719
+ throw err;
29720
+ }
29721
+ }
29722
+ };
29723
+ }
29724
+ });
29725
+
29631
29726
  // src/strategies/fallback/libav-loader.ts
29632
29727
  function cacheKey(variant, threads) {
29633
29728
  return `${variant}:${threads ? "thr" : "wasm"}`;
@@ -29644,9 +29739,18 @@ function loadLibav(variant = "webcodecs", opts = {}) {
29644
29739
  return entry;
29645
29740
  }
29646
29741
  async function loadVariant(variant, wantThreads) {
29742
+ return dbg.timed(
29743
+ "libav-load",
29744
+ `load "${variant}" (threads=${wantThreads})`,
29745
+ 5e3,
29746
+ () => loadVariantInner(variant, wantThreads)
29747
+ );
29748
+ }
29749
+ async function loadVariantInner(variant, wantThreads) {
29647
29750
  const key = cacheKey(variant, wantThreads);
29648
29751
  const base = `${libavBaseUrl()}/${variant}`;
29649
29752
  const variantUrl = `${base}/libav-${variant}.mjs`;
29753
+ dbg.info("libav-load", `fetching ${variantUrl}`);
29650
29754
  if (typeof fetch === "function") {
29651
29755
  try {
29652
29756
  const head = await fetch(variantUrl, { method: "GET", headers: { Range: "bytes=0-0" } });
@@ -29730,6 +29834,7 @@ function chain(message, err) {
29730
29834
  var cache;
29731
29835
  var init_libav_loader = __esm({
29732
29836
  "src/strategies/fallback/libav-loader.ts"() {
29837
+ init_debug();
29733
29838
  cache = /* @__PURE__ */ new Map();
29734
29839
  }
29735
29840
  });
@@ -29858,7 +29963,12 @@ function ffmpegToAvbridgeVideo(name) {
29858
29963
  return "mpeg1";
29859
29964
  case "theora":
29860
29965
  return "theora";
29966
+ case "rv10":
29967
+ return "rv10";
29968
+ case "rv20":
29969
+ return "rv20";
29861
29970
  case "rv30":
29971
+ return "rv30";
29862
29972
  case "rv40":
29863
29973
  return "rv40";
29864
29974
  default:
@@ -29889,6 +29999,16 @@ function ffmpegToAvbridgeAudio(name) {
29889
29999
  return "wmapro";
29890
30000
  case "alac":
29891
30001
  return "alac";
30002
+ case "cook":
30003
+ return "cook";
30004
+ case "ra_144":
30005
+ return "ra_144";
30006
+ case "ra_288":
30007
+ return "ra_288";
30008
+ case "sipr":
30009
+ return "sipr";
30010
+ case "atrac3":
30011
+ return "atrac3";
29892
30012
  default:
29893
30013
  return name;
29894
30014
  }
@@ -31154,8 +31274,29 @@ var NATIVE_AUDIO_CODECS = /* @__PURE__ */ new Set([
31154
31274
  "vorbis",
31155
31275
  "flac"
31156
31276
  ]);
31157
- var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set(["wmv3", "vc1", "mpeg4", "rv40", "mpeg2", "mpeg1", "theora"]);
31158
- var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set(["wmav2", "wmapro", "ac3", "eac3"]);
31277
+ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
31278
+ "wmv3",
31279
+ "vc1",
31280
+ "mpeg4",
31281
+ "rv10",
31282
+ "rv20",
31283
+ "rv30",
31284
+ "rv40",
31285
+ "mpeg2",
31286
+ "mpeg1",
31287
+ "theora"
31288
+ ]);
31289
+ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
31290
+ "wmav2",
31291
+ "wmapro",
31292
+ "ac3",
31293
+ "eac3",
31294
+ "cook",
31295
+ "ra_144",
31296
+ "ra_288",
31297
+ "sipr",
31298
+ "atrac3"
31299
+ ]);
31159
31300
  var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
31160
31301
  "mp4",
31161
31302
  "mov",
@@ -31837,6 +31978,10 @@ async function createRemuxPipeline(ctx, video) {
31837
31978
  console.error("[avbridge] remux pipeline reseek failed:", err);
31838
31979
  });
31839
31980
  },
31981
+ setAutoPlay(autoPlay) {
31982
+ pendingAutoPlay = autoPlay;
31983
+ if (sink) sink.setPlayOnSeek(autoPlay);
31984
+ },
31840
31985
  async destroy() {
31841
31986
  destroyed = true;
31842
31987
  pumpToken++;
@@ -31877,7 +32022,11 @@ async function createRemuxSession(context, video) {
31877
32022
  await pipeline.start(video.currentTime || 0, true);
31878
32023
  return;
31879
32024
  }
31880
- await video.play();
32025
+ pipeline.setAutoPlay(true);
32026
+ try {
32027
+ await video.play();
32028
+ } catch {
32029
+ }
31881
32030
  },
31882
32031
  pause() {
31883
32032
  wantPlay = false;
@@ -31925,7 +32074,7 @@ var VideoRenderer = class {
31925
32074
  this.resolveFirstFrame = resolve;
31926
32075
  });
31927
32076
  this.canvas = document.createElement("canvas");
31928
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
32077
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
31929
32078
  const parent = target.parentElement ?? target.parentNode;
31930
32079
  if (parent && parent instanceof HTMLElement) {
31931
32080
  if (getComputedStyle(parent).position === "static") {
@@ -32167,9 +32316,13 @@ var AudioOutput = class {
32167
32316
  const node3 = this.ctx.createBufferSource();
32168
32317
  node3.buffer = buffer;
32169
32318
  node3.connect(this.gain);
32170
- const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
32171
- const safeStart = Math.max(ctxStart, this.ctx.currentTime);
32172
- node3.start(safeStart);
32319
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
32320
+ if (ctxStart < this.ctx.currentTime) {
32321
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
32322
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
32323
+ ctxStart = this.ctx.currentTime;
32324
+ }
32325
+ node3.start(ctxStart);
32173
32326
  this.mediaTimeOfNext += frameCount / sampleRate;
32174
32327
  this.framesScheduled++;
32175
32328
  }
@@ -32764,6 +32917,10 @@ async function createHybridSession(ctx, target) {
32764
32917
  void doSeek(v);
32765
32918
  }
32766
32919
  });
32920
+ Object.defineProperty(target, "paused", {
32921
+ configurable: true,
32922
+ get: () => !audio.isPlaying()
32923
+ });
32767
32924
  if (ctx.duration && Number.isFinite(ctx.duration)) {
32768
32925
  Object.defineProperty(target, "duration", {
32769
32926
  configurable: true,
@@ -32828,6 +32985,7 @@ async function createHybridSession(ctx, target) {
32828
32985
  try {
32829
32986
  delete target.currentTime;
32830
32987
  delete target.duration;
32988
+ delete target.paused;
32831
32989
  } catch {
32832
32990
  }
32833
32991
  },
@@ -32906,6 +33064,10 @@ async function startDecoder(opts) {
32906
33064
  let packetsRead = 0;
32907
33065
  let videoFramesDecoded = 0;
32908
33066
  let audioFramesDecoded = 0;
33067
+ let watchdogFirstFrameMs = 0;
33068
+ let watchdogSlowSinceMs = 0;
33069
+ let watchdogSlowWarned = false;
33070
+ let watchdogOverflowWarned = false;
32909
33071
  let syntheticVideoUs = 0;
32910
33072
  let syntheticAudioUs = 0;
32911
33073
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -32926,14 +33088,47 @@ async function startDecoder(opts) {
32926
33088
  if (myToken !== pumpToken || destroyed) return;
32927
33089
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
32928
33090
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
32929
- if (videoDec && videoPackets && videoPackets.length > 0) {
32930
- await decodeVideoBatch(videoPackets, myToken);
32931
- }
32932
- if (myToken !== pumpToken || destroyed) return;
32933
33091
  if (audioDec && audioPackets && audioPackets.length > 0) {
32934
33092
  await decodeAudioBatch(audioPackets, myToken);
32935
33093
  }
33094
+ if (myToken !== pumpToken || destroyed) return;
33095
+ if (videoDec && videoPackets && videoPackets.length > 0) {
33096
+ await decodeVideoBatch(videoPackets, myToken);
33097
+ }
32936
33098
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
33099
+ if (videoFramesDecoded > 0) {
33100
+ if (watchdogFirstFrameMs === 0) {
33101
+ watchdogFirstFrameMs = performance.now();
33102
+ }
33103
+ const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
33104
+ if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
33105
+ const expectedFrames = elapsedSinceFirst * videoFps;
33106
+ const ratio = videoFramesDecoded / expectedFrames;
33107
+ if (ratio < 0.6) {
33108
+ if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
33109
+ if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
33110
+ watchdogSlowWarned = true;
33111
+ console.warn(
33112
+ "[avbridge:decode-rate]",
33113
+ `decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
33114
+ );
33115
+ }
33116
+ } else {
33117
+ watchdogSlowSinceMs = 0;
33118
+ }
33119
+ }
33120
+ if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
33121
+ const rendererStats = opts.renderer.stats();
33122
+ const overflow = rendererStats.framesDroppedOverflow ?? 0;
33123
+ if (overflow / videoFramesDecoded > 0.1) {
33124
+ watchdogOverflowWarned = true;
33125
+ console.warn(
33126
+ "[avbridge:overflow-drop]",
33127
+ `renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
33128
+ );
33129
+ }
33130
+ }
33131
+ }
32937
33132
  while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
32938
33133
  await new Promise((r) => setTimeout(r, 50));
32939
33134
  }
@@ -33260,8 +33455,9 @@ async function loadBridge2() {
33260
33455
  }
33261
33456
 
33262
33457
  // src/strategies/fallback/index.ts
33263
- var READY_AUDIO_BUFFER_SECONDS2 = 0.3;
33264
- var READY_TIMEOUT_SECONDS2 = 10;
33458
+ init_debug();
33459
+ var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
33460
+ var READY_TIMEOUT_SECONDS2 = 3;
33265
33461
  async function createFallbackSession(ctx, target) {
33266
33462
  const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
33267
33463
  const source = await normalizeSource2(ctx.source);
@@ -33289,6 +33485,10 @@ async function createFallbackSession(ctx, target) {
33289
33485
  void doSeek(v);
33290
33486
  }
33291
33487
  });
33488
+ Object.defineProperty(target, "paused", {
33489
+ configurable: true,
33490
+ get: () => !audio.isPlaying()
33491
+ });
33292
33492
  if (ctx.duration && Number.isFinite(ctx.duration)) {
33293
33493
  Object.defineProperty(target, "duration", {
33294
33494
  configurable: true,
@@ -33297,12 +33497,36 @@ async function createFallbackSession(ctx, target) {
33297
33497
  }
33298
33498
  async function waitForBuffer() {
33299
33499
  const start = performance.now();
33500
+ let firstFrameAtMs = 0;
33501
+ dbg.info(
33502
+ "cold-start",
33503
+ `gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
33504
+ );
33300
33505
  while (true) {
33301
- const audioReady = audio.isNoAudio() || audio.bufferAhead() >= READY_AUDIO_BUFFER_SECONDS2;
33302
- if (audioReady && renderer.hasFrames()) {
33506
+ const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
33507
+ const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
33508
+ const hasFrames = renderer.hasFrames();
33509
+ const nowMs = performance.now();
33510
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
33511
+ if (audioReady && hasFrames) {
33512
+ dbg.info(
33513
+ "cold-start",
33514
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
33515
+ );
33303
33516
  return;
33304
33517
  }
33305
- if ((performance.now() - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
33518
+ if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
33519
+ dbg.info(
33520
+ "cold-start",
33521
+ `gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
33522
+ );
33523
+ return;
33524
+ }
33525
+ if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
33526
+ dbg.diag(
33527
+ "cold-start",
33528
+ `gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
33529
+ );
33306
33530
  return;
33307
33531
  }
33308
33532
  await new Promise((r) => setTimeout(r, 50));
@@ -33350,6 +33574,7 @@ async function createFallbackSession(ctx, target) {
33350
33574
  try {
33351
33575
  delete target.currentTime;
33352
33576
  delete target.duration;
33577
+ delete target.paused;
33353
33578
  } catch {
33354
33579
  }
33355
33580
  },
@@ -33413,8 +33638,8 @@ function convertTiming(line) {
33413
33638
  line.trim()
33414
33639
  );
33415
33640
  if (!m) return null;
33416
- const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
33417
- return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
33641
+ const fmt2 = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
33642
+ return `${fmt2(m[1], m[2], m[3], m[4])} --> ${fmt2(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
33418
33643
  }
33419
33644
 
33420
33645
  // src/subtitles/vtt.ts
@@ -33500,6 +33725,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
33500
33725
  }
33501
33726
 
33502
33727
  // src/player.ts
33728
+ init_debug();
33503
33729
  var UnifiedPlayer = class _UnifiedPlayer {
33504
33730
  /**
33505
33731
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -33522,6 +33748,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
33522
33748
  lastProgressTime = 0;
33523
33749
  lastProgressPosition = -1;
33524
33750
  errorListener = null;
33751
+ // Bound so we can removeEventListener in destroy(); without this the
33752
+ // listener outlives the player and accumulates on elements that swap
33753
+ // source (e.g. <avbridge-video>).
33754
+ endedListener = null;
33525
33755
  // Serializes escalation / setStrategy calls
33526
33756
  switchingPromise = Promise.resolve();
33527
33757
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
@@ -33547,8 +33777,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
33547
33777
  return player;
33548
33778
  }
33549
33779
  async bootstrap() {
33780
+ const bootstrapStart = performance.now();
33550
33781
  try {
33551
- const ctx = await probe(this.options.source);
33782
+ dbg.info("bootstrap", "start");
33783
+ const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
33784
+ dbg.info(
33785
+ "probe",
33786
+ `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
33787
+ );
33552
33788
  this.diag.recordProbe(ctx);
33553
33789
  this.mediaContext = ctx;
33554
33790
  if (this.options.subtitles) {
@@ -33574,6 +33810,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
33574
33810
  }
33575
33811
  }
33576
33812
  const decision = this.options.initialStrategy ? buildInitialDecision(this.options.initialStrategy, ctx) : classifyContext(ctx);
33813
+ dbg.info(
33814
+ "classify",
33815
+ `strategy=${decision.strategy} class=${decision.class} reason="${decision.reason}"` + (decision.fallbackChain ? ` fallback=${decision.fallbackChain.join("\u2192")}` : "")
33816
+ );
33577
33817
  this.classification = decision;
33578
33818
  this.diag.recordClassification(decision);
33579
33819
  this.emitter.emitSticky("strategy", {
@@ -33597,8 +33837,17 @@ var UnifiedPlayer = class _UnifiedPlayer {
33597
33837
  subtitle: ctx.subtitleTracks
33598
33838
  });
33599
33839
  this.startTimeupdateLoop();
33600
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", void 0));
33840
+ this.endedListener = () => this.emitter.emit("ended", void 0);
33841
+ this.options.target.addEventListener("ended", this.endedListener);
33601
33842
  this.emitter.emitSticky("ready", void 0);
33843
+ const bootstrapElapsed = performance.now() - bootstrapStart;
33844
+ dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
33845
+ if (bootstrapElapsed > 5e3) {
33846
+ console.warn(
33847
+ "[avbridge:bootstrap]",
33848
+ `total bootstrap time ${bootstrapElapsed.toFixed(0)}ms \u2014 unusually slow. Enable globalThis.AVBRIDGE_DEBUG for a per-phase breakdown.`
33849
+ );
33850
+ }
33602
33851
  } catch (err) {
33603
33852
  const e = err instanceof Error ? err : new Error(String(err));
33604
33853
  this.diag.recordError(e);
@@ -33875,6 +34124,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
33875
34124
  this.timeupdateInterval = null;
33876
34125
  }
33877
34126
  this.clearSupervisor();
34127
+ if (this.endedListener) {
34128
+ this.options.target.removeEventListener("ended", this.endedListener);
34129
+ this.endedListener = null;
34130
+ }
33878
34131
  if (this.session) {
33879
34132
  await this.session.destroy();
33880
34133
  this.session = null;
@@ -33889,11 +34142,13 @@ async function createPlayer(options) {
33889
34142
  function buildInitialDecision(initial, ctx) {
33890
34143
  const natural = classifyContext(ctx);
33891
34144
  const cls = strategyToClass(initial, natural);
34145
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
34146
+ const fallbackChain = inherited.filter((s) => s !== initial);
33892
34147
  return {
33893
34148
  class: cls,
33894
34149
  strategy: initial,
33895
34150
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
33896
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
34151
+ fallbackChain
33897
34152
  };
33898
34153
  }
33899
34154
  function strategyToClass(strategy, natural) {