avbridge 2.8.3 → 2.9.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 (78) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +74 -1
  3. package/dist/{avi-F6WZJK5T.cjs → avi-2ILLBNPQ.cjs} +8 -2
  4. package/dist/avi-2ILLBNPQ.cjs.map +1 -0
  5. package/dist/{avi-W6L3BTWU.cjs → avi-B5CQYB7L.cjs} +8 -2
  6. package/dist/avi-B5CQYB7L.cjs.map +1 -0
  7. package/dist/{avi-2JPBSHGA.js → avi-JXU4GQL2.js} +8 -2
  8. package/dist/avi-JXU4GQL2.js.map +1 -0
  9. package/dist/{avi-NJXAXUXK.js → avi-RWWPN2PR.js} +8 -2
  10. package/dist/avi-RWWPN2PR.js.map +1 -0
  11. package/dist/{chunk-X2K3GIWE.js → chunk-2NSOOMXW.js} +14 -3
  12. package/dist/chunk-2NSOOMXW.js.map +1 -0
  13. package/dist/{chunk-ZCUXHW55.cjs → chunk-BYGZN4Z5.cjs} +5 -5
  14. package/dist/{chunk-ZCUXHW55.cjs.map → chunk-BYGZN4Z5.cjs.map} +1 -1
  15. package/dist/{chunk-SMH6IOP2.js → chunk-CL6UEUQF.js} +4 -4
  16. package/dist/{chunk-SMH6IOP2.js.map → chunk-CL6UEUQF.js.map} +1 -1
  17. package/dist/{chunk-IUSFLVLJ.cjs → chunk-EY6DZEDT.cjs} +149 -24
  18. package/dist/chunk-EY6DZEDT.cjs.map +1 -0
  19. package/dist/{chunk-SR3MPV4D.js → chunk-GYIJU44C.js} +5 -5
  20. package/dist/{chunk-SR3MPV4D.js.map → chunk-GYIJU44C.js.map} +1 -1
  21. package/dist/{chunk-CPZ7PXAM.cjs → chunk-L7A3ECI2.cjs} +14 -2
  22. package/dist/chunk-L7A3ECI2.cjs.map +1 -0
  23. package/dist/{chunk-Q2VUO52Z.cjs → chunk-OTFS7DC4.cjs} +12 -12
  24. package/dist/{chunk-Q2VUO52Z.cjs.map → chunk-OTFS7DC4.cjs.map} +1 -1
  25. package/dist/{chunk-JSQOBUQB.js → chunk-SN4WZE24.js} +139 -14
  26. package/dist/chunk-SN4WZE24.js.map +1 -0
  27. package/dist/element-browser.js +164 -16
  28. package/dist/element-browser.js.map +1 -1
  29. package/dist/element.cjs +16 -10
  30. package/dist/element.cjs.map +1 -1
  31. package/dist/element.d.cts +11 -6
  32. package/dist/element.d.ts +11 -6
  33. package/dist/element.js +15 -9
  34. package/dist/element.js.map +1 -1
  35. package/dist/index.cjs +20 -20
  36. package/dist/index.d.cts +2 -2
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +8 -8
  39. package/dist/libav-demux-3N5Y3VQA.cjs +31 -0
  40. package/dist/{libav-demux-H2GS46GH.cjs.map → libav-demux-3N5Y3VQA.cjs.map} +1 -1
  41. package/dist/libav-demux-JXD4OTLM.js +6 -0
  42. package/dist/{libav-demux-OWZ4T2YW.js.map → libav-demux-JXD4OTLM.js.map} +1 -1
  43. package/dist/{player-DXEKOky8.d.cts → player-DEcidWk6.d.cts} +8 -1
  44. package/dist/{player-DXEKOky8.d.ts → player-DEcidWk6.d.ts} +8 -1
  45. package/dist/player.cjs +266 -36
  46. package/dist/player.cjs.map +1 -1
  47. package/dist/player.d.cts +37 -11
  48. package/dist/player.d.ts +37 -11
  49. package/dist/player.js +266 -36
  50. package/dist/player.js.map +1 -1
  51. package/dist/{remux-WBYIZBBX.js → remux-56V7LDAD.js} +5 -5
  52. package/dist/{remux-WBYIZBBX.js.map → remux-56V7LDAD.js.map} +1 -1
  53. package/dist/{remux-OBSMIENG.cjs → remux-KUS5GIL6.cjs} +10 -10
  54. package/dist/{remux-OBSMIENG.cjs.map → remux-KUS5GIL6.cjs.map} +1 -1
  55. package/package.json +1 -1
  56. package/src/classify/rules.ts +11 -0
  57. package/src/element/avbridge-player.ts +22 -11
  58. package/src/element/avbridge-video.ts +22 -6
  59. package/src/element/player-styles.ts +68 -3
  60. package/src/player.ts +96 -8
  61. package/src/probe/avi.ts +2 -0
  62. package/src/strategies/fallback/decoder.ts +30 -0
  63. package/src/strategies/fallback/index.ts +30 -0
  64. package/src/strategies/hybrid/decoder.ts +35 -0
  65. package/src/strategies/hybrid/index.ts +17 -0
  66. package/src/strategies/remux/index.ts +8 -0
  67. package/src/types.ts +6 -0
  68. package/src/util/libav-demux.ts +26 -0
  69. package/dist/avi-2JPBSHGA.js.map +0 -1
  70. package/dist/avi-F6WZJK5T.cjs.map +0 -1
  71. package/dist/avi-NJXAXUXK.js.map +0 -1
  72. package/dist/avi-W6L3BTWU.cjs.map +0 -1
  73. package/dist/chunk-CPZ7PXAM.cjs.map +0 -1
  74. package/dist/chunk-IUSFLVLJ.cjs.map +0 -1
  75. package/dist/chunk-JSQOBUQB.js.map +0 -1
  76. package/dist/chunk-X2K3GIWE.js.map +0 -1
  77. package/dist/libav-demux-H2GS46GH.cjs +0 -27
  78. package/dist/libav-demux-OWZ4T2YW.js +0 -6
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
4
- var chunkZCUXHW55_cjs = require('./chunk-ZCUXHW55.cjs');
4
+ var chunkBYGZN4Z5_cjs = require('./chunk-BYGZN4Z5.cjs');
5
5
  var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
6
- var chunkCPZ7PXAM_cjs = require('./chunk-CPZ7PXAM.cjs');
6
+ var chunkL7A3ECI2_cjs = require('./chunk-L7A3ECI2.cjs');
7
7
  var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
8
8
  var chunkF3LQJKXK_cjs = require('./chunk-F3LQJKXK.cjs');
9
9
 
@@ -92,7 +92,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
92
92
  "rv40",
93
93
  "mpeg2",
94
94
  "mpeg1",
95
- "theora"
95
+ "theora",
96
+ "dv",
97
+ "hq_hqa",
98
+ "rawvideo",
99
+ "qtrle",
100
+ "png",
101
+ "vp6f"
96
102
  ]);
97
103
  var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
98
104
  "wmav2",
@@ -233,10 +239,12 @@ function classifyContext(ctx) {
233
239
  reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
234
240
  };
235
241
  }
242
+ const fallbackChain = webCodecsAvailable() ? ["hybrid", "fallback"] : ["fallback"];
236
243
  return {
237
244
  class: "REMUX_CANDIDATE",
238
245
  strategy: "remux",
239
- reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`
246
+ reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`,
247
+ fallbackChain
240
248
  };
241
249
  }
242
250
  if (webCodecsAvailable()) {
@@ -726,12 +734,12 @@ async function createRemuxPipeline(ctx, video) {
726
734
  const mb = await import('mediabunny');
727
735
  const videoTrackInfo = ctx.videoTracks[0];
728
736
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
729
- const mbVideoCodec = chunkZCUXHW55_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
737
+ const mbVideoCodec = chunkBYGZN4Z5_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
730
738
  if (!mbVideoCodec) {
731
739
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
732
740
  }
733
741
  const input = new mb.Input({
734
- source: await chunkZCUXHW55_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
742
+ source: await chunkBYGZN4Z5_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
735
743
  formats: mb.ALL_FORMATS
736
744
  });
737
745
  const allTracks = await input.getTracks();
@@ -763,7 +771,7 @@ async function createRemuxPipeline(ctx, video) {
763
771
  throw new Error("remux: audio track not found in input");
764
772
  }
765
773
  inputAudio = newInput;
766
- mbAudioCodec = chunkZCUXHW55_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
774
+ mbAudioCodec = chunkBYGZN4Z5_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
767
775
  audioSink = new mb.EncodedPacketSink(newInput);
768
776
  audioConfig = await newInput.getDecoderConfig();
769
777
  }
@@ -974,6 +982,12 @@ async function createRemuxSession(context, video) {
974
982
  }
975
983
  const wasPlaying = !video.paused;
976
984
  await pipeline.seek(time, wasPlaying || wantPlay);
985
+ queueMicrotask(() => {
986
+ try {
987
+ video.dispatchEvent(new Event("seeked"));
988
+ } catch {
989
+ }
990
+ });
977
991
  },
978
992
  async setAudioTrack(id) {
979
993
  if (!context.audioTracks.some((t) => t.id === id)) {
@@ -1701,6 +1715,7 @@ async function startHybridDecoder(opts) {
1701
1715
  let videoFramesDecoded = 0;
1702
1716
  let audioFramesDecoded = 0;
1703
1717
  let videoChunksFed = 0;
1718
+ let bufferedUntilSec = 0;
1704
1719
  let syntheticVideoUs = 0;
1705
1720
  let syntheticAudioUs = 0;
1706
1721
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -1721,6 +1736,18 @@ async function startHybridDecoder(opts) {
1721
1736
  if (myToken !== pumpToken || destroyed) return;
1722
1737
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1723
1738
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1739
+ if (videoPackets && videoTimeBase) {
1740
+ for (const pkt of videoPackets) {
1741
+ const sec = chunkL7A3ECI2_cjs.packetPtsSec(pkt, videoTimeBase);
1742
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
1743
+ }
1744
+ }
1745
+ if (audioPackets && audioTimeBase) {
1746
+ for (const pkt of audioPackets) {
1747
+ const sec = chunkL7A3ECI2_cjs.packetPtsSec(pkt, audioTimeBase);
1748
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
1749
+ }
1750
+ }
1724
1751
  if (audioDec && audioPackets && audioPackets.length > 0) {
1725
1752
  await decodeAudioBatch(audioPackets, myToken);
1726
1753
  }
@@ -1731,7 +1758,7 @@ async function startHybridDecoder(opts) {
1731
1758
  const processed = await applyBSF(videoPackets);
1732
1759
  for (const pkt of processed) {
1733
1760
  if (myToken !== pumpToken || destroyed) return;
1734
- chunkCPZ7PXAM_cjs.sanitizePacketTimestamp(pkt, () => {
1761
+ chunkL7A3ECI2_cjs.sanitizePacketTimestamp(pkt, () => {
1735
1762
  const ts = syntheticVideoUs;
1736
1763
  syntheticVideoUs += videoFrameStepUs;
1737
1764
  return ts;
@@ -1810,7 +1837,7 @@ async function startHybridDecoder(opts) {
1810
1837
  const frames = allFrames;
1811
1838
  for (const f of frames) {
1812
1839
  if (myToken !== pumpToken || destroyed) return;
1813
- chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
1840
+ chunkL7A3ECI2_cjs.sanitizeFrameTimestamp(
1814
1841
  f,
1815
1842
  () => {
1816
1843
  const ts = syntheticAudioUs;
@@ -1821,7 +1848,7 @@ async function startHybridDecoder(opts) {
1821
1848
  },
1822
1849
  audioTimeBase
1823
1850
  );
1824
- const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
1851
+ const samples = chunkL7A3ECI2_cjs.libavFrameToInterleavedFloat32(f);
1825
1852
  if (samples) {
1826
1853
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
1827
1854
  audioFramesDecoded++;
@@ -1977,6 +2004,9 @@ async function startHybridDecoder(opts) {
1977
2004
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
1978
2005
  );
1979
2006
  },
2007
+ bufferedUntilSec() {
2008
+ return bufferedUntilSec;
2009
+ },
1980
2010
  stats() {
1981
2011
  return {
1982
2012
  decoderType: "webcodecs-hybrid",
@@ -2104,6 +2134,13 @@ async function createHybridSession(ctx, target, transport) {
2104
2134
  configurable: true,
2105
2135
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2106
2136
  });
2137
+ Object.defineProperty(target, "buffered", {
2138
+ configurable: true,
2139
+ get: () => {
2140
+ const end = handles.bufferedUntilSec();
2141
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
2142
+ }
2143
+ });
2107
2144
  async function waitForBuffer() {
2108
2145
  const start = performance.now();
2109
2146
  while (true) {
@@ -2117,6 +2154,7 @@ async function createHybridSession(ctx, target, transport) {
2117
2154
  }
2118
2155
  async function doSeek(timeSec) {
2119
2156
  const wasPlaying = audio.isPlaying();
2157
+ target.dispatchEvent(new Event("seeking"));
2120
2158
  await audio.pause().catch(() => {
2121
2159
  });
2122
2160
  await handles.seek(timeSec).catch(
@@ -2128,7 +2166,14 @@ async function createHybridSession(ctx, target, transport) {
2128
2166
  await waitForBuffer();
2129
2167
  await audio.start();
2130
2168
  }
2169
+ target.dispatchEvent(new Event("seeked"));
2131
2170
  }
2171
+ queueMicrotask(() => {
2172
+ try {
2173
+ target.dispatchEvent(new Event("loadedmetadata"));
2174
+ } catch {
2175
+ }
2176
+ });
2132
2177
  let fatalErrorHandler = null;
2133
2178
  handles.onFatalError((reason) => fatalErrorHandler?.(reason));
2134
2179
  return {
@@ -2313,6 +2358,7 @@ async function startDecoder(opts) {
2313
2358
  let pumpRunning = null;
2314
2359
  let packetsRead = 0;
2315
2360
  let videoFramesDecoded = 0;
2361
+ let bufferedUntilSec = 0;
2316
2362
  let audioFramesDecoded = 0;
2317
2363
  let watchdogFirstFrameMs = 0;
2318
2364
  let watchdogSlowSinceMs = 0;
@@ -2338,6 +2384,18 @@ async function startDecoder(opts) {
2338
2384
  if (myToken !== pumpToken || destroyed) return;
2339
2385
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2340
2386
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2387
+ if (videoPackets && videoTimeBase) {
2388
+ for (const pkt of videoPackets) {
2389
+ const sec = chunkL7A3ECI2_cjs.packetPtsSec(pkt, videoTimeBase);
2390
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2391
+ }
2392
+ }
2393
+ if (audioPackets && audioTimeBase) {
2394
+ for (const pkt of audioPackets) {
2395
+ const sec = chunkL7A3ECI2_cjs.packetPtsSec(pkt, audioTimeBase);
2396
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2397
+ }
2398
+ }
2341
2399
  if (audioDec && audioPackets && audioPackets.length > 0) {
2342
2400
  await decodeAudioBatch(audioPackets, myToken);
2343
2401
  }
@@ -2422,7 +2480,7 @@ async function startDecoder(opts) {
2422
2480
  if (myToken !== pumpToken || destroyed) return;
2423
2481
  for (const f of frames) {
2424
2482
  if (myToken !== pumpToken || destroyed) return;
2425
- chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
2483
+ chunkL7A3ECI2_cjs.sanitizeFrameTimestamp(
2426
2484
  f,
2427
2485
  () => {
2428
2486
  const ts = syntheticVideoUs;
@@ -2460,7 +2518,7 @@ async function startDecoder(opts) {
2460
2518
  if (myToken !== pumpToken || destroyed) return;
2461
2519
  for (const f of frames) {
2462
2520
  if (myToken !== pumpToken || destroyed) return;
2463
- chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
2521
+ chunkL7A3ECI2_cjs.sanitizeFrameTimestamp(
2464
2522
  f,
2465
2523
  () => {
2466
2524
  const ts = syntheticAudioUs;
@@ -2471,7 +2529,7 @@ async function startDecoder(opts) {
2471
2529
  },
2472
2530
  audioTimeBase
2473
2531
  );
2474
- const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
2532
+ const samples = chunkL7A3ECI2_cjs.libavFrameToInterleavedFloat32(f);
2475
2533
  if (samples) {
2476
2534
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2477
2535
  audioFramesDecoded++;
@@ -2619,6 +2677,9 @@ async function startDecoder(opts) {
2619
2677
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
2620
2678
  );
2621
2679
  },
2680
+ bufferedUntilSec() {
2681
+ return bufferedUntilSec;
2682
+ },
2622
2683
  stats() {
2623
2684
  return {
2624
2685
  decoderType: "libav-wasm",
@@ -2718,6 +2779,13 @@ async function createFallbackSession(ctx, target, transport) {
2718
2779
  configurable: true,
2719
2780
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2720
2781
  });
2782
+ Object.defineProperty(target, "buffered", {
2783
+ configurable: true,
2784
+ get: () => {
2785
+ const end = handles.bufferedUntilSec();
2786
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
2787
+ }
2788
+ });
2721
2789
  async function waitForBuffer() {
2722
2790
  const start = performance.now();
2723
2791
  let firstFrameAtMs = 0;
@@ -2757,6 +2825,7 @@ async function createFallbackSession(ctx, target, transport) {
2757
2825
  }
2758
2826
  async function doSeek(timeSec) {
2759
2827
  const wasPlaying = audio.isPlaying();
2828
+ target.dispatchEvent(new Event("seeking"));
2760
2829
  await audio.pause().catch(() => {
2761
2830
  });
2762
2831
  await handles.seek(timeSec).catch(
@@ -2768,7 +2837,14 @@ async function createFallbackSession(ctx, target, transport) {
2768
2837
  await waitForBuffer();
2769
2838
  await audio.start();
2770
2839
  }
2840
+ target.dispatchEvent(new Event("seeked"));
2771
2841
  }
2842
+ queueMicrotask(() => {
2843
+ try {
2844
+ target.dispatchEvent(new Event("loadedmetadata"));
2845
+ } catch {
2846
+ }
2847
+ });
2772
2848
  return {
2773
2849
  strategy: "fallback",
2774
2850
  async play() {
@@ -2860,6 +2936,29 @@ function registerBuiltins(registry) {
2860
2936
  }
2861
2937
 
2862
2938
  // src/player.ts
2939
+ function readDecodedFrameCount(target) {
2940
+ if (typeof HTMLVideoElement === "undefined" || !(target instanceof HTMLVideoElement)) return 0;
2941
+ const vq = target.getVideoPlaybackQuality;
2942
+ if (typeof vq === "function") {
2943
+ try {
2944
+ return vq.call(target).totalVideoFrames;
2945
+ } catch {
2946
+ }
2947
+ }
2948
+ const legacy = target.webkitDecodedFrameCount;
2949
+ return typeof legacy === "number" ? legacy : 0;
2950
+ }
2951
+ function evaluateDecodeHealth(input) {
2952
+ const timeThreshold = input.timeStallThresholdMs ?? 5e3;
2953
+ const frameThreshold = input.frameStallThresholdMs ?? 3e3;
2954
+ if (!input.timeAdvanced && input.now - input.lastProgressTime > timeThreshold) {
2955
+ return { escalate: true, kind: "time-stall" };
2956
+ }
2957
+ if (input.hasVideoTrack && input.timeAdvanced && !input.framesAdvanced && input.now - input.lastFrameProgressTime > frameThreshold) {
2958
+ return { escalate: true, kind: "silent-video" };
2959
+ }
2960
+ return { escalate: false };
2961
+ }
2863
2962
  var UnifiedPlayer = class _UnifiedPlayer {
2864
2963
  /**
2865
2964
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -2885,6 +2984,13 @@ var UnifiedPlayer = class _UnifiedPlayer {
2885
2984
  stallTimer = null;
2886
2985
  lastProgressTime = 0;
2887
2986
  lastProgressPosition = -1;
2987
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
2988
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
2989
+ * watchdog — catches cases where `currentTime` advances (audio plays)
2990
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
2991
+ * via MSE when the decoder actually can't decode HEVC. */
2992
+ lastVideoFrameCount = 0;
2993
+ lastVideoFrameProgressTime = 0;
2888
2994
  errorListener = null;
2889
2995
  // Bound so we can removeEventListener in destroy(); without this the
2890
2996
  // listener outlives the player and accumulates on elements that swap
@@ -2930,7 +3036,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2930
3036
  const bootstrapStart = performance.now();
2931
3037
  try {
2932
3038
  chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
2933
- const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkZCUXHW55_cjs.probe(this.options.source, this.transport));
3039
+ const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkBYGZN4Z5_cjs.probe(this.options.source, this.transport));
2934
3040
  chunkG4APZMCP_cjs.dbg.info(
2935
3041
  "probe",
2936
3042
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -3129,22 +3235,41 @@ var UnifiedPlayer = class _UnifiedPlayer {
3129
3235
  if (strategy === "native" || strategy === "remux") {
3130
3236
  this.lastProgressPosition = this.options.target.currentTime;
3131
3237
  this.lastProgressTime = performance.now();
3238
+ this.lastVideoFrameCount = readDecodedFrameCount(this.options.target);
3239
+ this.lastVideoFrameProgressTime = performance.now();
3240
+ const hasVideoTrack = (this.mediaContext?.videoTracks.length ?? 0) > 0;
3132
3241
  this.stallTimer = setInterval(() => {
3133
3242
  const t = this.options.target;
3243
+ const now = performance.now();
3134
3244
  if (t.paused || t.ended || t.readyState < 2) {
3135
3245
  this.lastProgressPosition = t.currentTime;
3136
- this.lastProgressTime = performance.now();
3246
+ this.lastProgressTime = now;
3247
+ this.lastVideoFrameCount = readDecodedFrameCount(t);
3248
+ this.lastVideoFrameProgressTime = now;
3137
3249
  return;
3138
3250
  }
3139
- if (t.currentTime !== this.lastProgressPosition) {
3251
+ const timeAdvanced = t.currentTime !== this.lastProgressPosition;
3252
+ const frames = readDecodedFrameCount(t);
3253
+ const framesAdvanced = frames > this.lastVideoFrameCount;
3254
+ const health = evaluateDecodeHealth({
3255
+ hasVideoTrack,
3256
+ timeAdvanced,
3257
+ framesAdvanced,
3258
+ now,
3259
+ lastProgressTime: this.lastProgressTime,
3260
+ lastFrameProgressTime: this.lastVideoFrameProgressTime
3261
+ });
3262
+ if (timeAdvanced) {
3140
3263
  this.lastProgressPosition = t.currentTime;
3141
- this.lastProgressTime = performance.now();
3142
- return;
3264
+ this.lastProgressTime = now;
3143
3265
  }
3144
- if (performance.now() - this.lastProgressTime > 5e3) {
3145
- void this.escalate(
3146
- `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s`
3147
- );
3266
+ if (framesAdvanced) {
3267
+ this.lastVideoFrameCount = frames;
3268
+ this.lastVideoFrameProgressTime = now;
3269
+ }
3270
+ if (health.escalate) {
3271
+ const reason = health.kind === "time-stall" ? `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s` : `${strategy} strategy: audio is advancing but the video decoder has produced no new frames for 3s \u2014 likely a silent codec failure`;
3272
+ void this.escalate(reason);
3148
3273
  }
3149
3274
  }, 1e3);
3150
3275
  const onError = () => {
@@ -3381,5 +3506,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3381
3506
  exports.UnifiedPlayer = UnifiedPlayer;
3382
3507
  exports.classifyContext = classifyContext;
3383
3508
  exports.createPlayer = createPlayer;
3384
- //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map
3385
- //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map
3509
+ //# sourceMappingURL=chunk-EY6DZEDT.cjs.map
3510
+ //# sourceMappingURL=chunk-EY6DZEDT.cjs.map