avbridge 2.8.3 → 2.8.4

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.
@@ -233,10 +233,12 @@ function classifyContext(ctx) {
233
233
  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
234
  };
235
235
  }
236
+ const fallbackChain = webCodecsAvailable() ? ["hybrid", "fallback"] : ["fallback"];
236
237
  return {
237
238
  class: "REMUX_CANDIDATE",
238
239
  strategy: "remux",
239
- reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`
240
+ reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`,
241
+ fallbackChain
240
242
  };
241
243
  }
242
244
  if (webCodecsAvailable()) {
@@ -2860,6 +2862,29 @@ function registerBuiltins(registry) {
2860
2862
  }
2861
2863
 
2862
2864
  // src/player.ts
2865
+ function readDecodedFrameCount(target) {
2866
+ if (typeof HTMLVideoElement === "undefined" || !(target instanceof HTMLVideoElement)) return 0;
2867
+ const vq = target.getVideoPlaybackQuality;
2868
+ if (typeof vq === "function") {
2869
+ try {
2870
+ return vq.call(target).totalVideoFrames;
2871
+ } catch {
2872
+ }
2873
+ }
2874
+ const legacy = target.webkitDecodedFrameCount;
2875
+ return typeof legacy === "number" ? legacy : 0;
2876
+ }
2877
+ function evaluateDecodeHealth(input) {
2878
+ const timeThreshold = input.timeStallThresholdMs ?? 5e3;
2879
+ const frameThreshold = input.frameStallThresholdMs ?? 3e3;
2880
+ if (!input.timeAdvanced && input.now - input.lastProgressTime > timeThreshold) {
2881
+ return { escalate: true, kind: "time-stall" };
2882
+ }
2883
+ if (input.hasVideoTrack && input.timeAdvanced && !input.framesAdvanced && input.now - input.lastFrameProgressTime > frameThreshold) {
2884
+ return { escalate: true, kind: "silent-video" };
2885
+ }
2886
+ return { escalate: false };
2887
+ }
2863
2888
  var UnifiedPlayer = class _UnifiedPlayer {
2864
2889
  /**
2865
2890
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -2885,6 +2910,13 @@ var UnifiedPlayer = class _UnifiedPlayer {
2885
2910
  stallTimer = null;
2886
2911
  lastProgressTime = 0;
2887
2912
  lastProgressPosition = -1;
2913
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
2914
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
2915
+ * watchdog — catches cases where `currentTime` advances (audio plays)
2916
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
2917
+ * via MSE when the decoder actually can't decode HEVC. */
2918
+ lastVideoFrameCount = 0;
2919
+ lastVideoFrameProgressTime = 0;
2888
2920
  errorListener = null;
2889
2921
  // Bound so we can removeEventListener in destroy(); without this the
2890
2922
  // listener outlives the player and accumulates on elements that swap
@@ -3129,22 +3161,41 @@ var UnifiedPlayer = class _UnifiedPlayer {
3129
3161
  if (strategy === "native" || strategy === "remux") {
3130
3162
  this.lastProgressPosition = this.options.target.currentTime;
3131
3163
  this.lastProgressTime = performance.now();
3164
+ this.lastVideoFrameCount = readDecodedFrameCount(this.options.target);
3165
+ this.lastVideoFrameProgressTime = performance.now();
3166
+ const hasVideoTrack = (this.mediaContext?.videoTracks.length ?? 0) > 0;
3132
3167
  this.stallTimer = setInterval(() => {
3133
3168
  const t = this.options.target;
3169
+ const now = performance.now();
3134
3170
  if (t.paused || t.ended || t.readyState < 2) {
3135
3171
  this.lastProgressPosition = t.currentTime;
3136
- this.lastProgressTime = performance.now();
3172
+ this.lastProgressTime = now;
3173
+ this.lastVideoFrameCount = readDecodedFrameCount(t);
3174
+ this.lastVideoFrameProgressTime = now;
3137
3175
  return;
3138
3176
  }
3139
- if (t.currentTime !== this.lastProgressPosition) {
3177
+ const timeAdvanced = t.currentTime !== this.lastProgressPosition;
3178
+ const frames = readDecodedFrameCount(t);
3179
+ const framesAdvanced = frames > this.lastVideoFrameCount;
3180
+ const health = evaluateDecodeHealth({
3181
+ hasVideoTrack,
3182
+ timeAdvanced,
3183
+ framesAdvanced,
3184
+ now,
3185
+ lastProgressTime: this.lastProgressTime,
3186
+ lastFrameProgressTime: this.lastVideoFrameProgressTime
3187
+ });
3188
+ if (timeAdvanced) {
3140
3189
  this.lastProgressPosition = t.currentTime;
3141
- this.lastProgressTime = performance.now();
3142
- return;
3190
+ this.lastProgressTime = now;
3143
3191
  }
3144
- if (performance.now() - this.lastProgressTime > 5e3) {
3145
- void this.escalate(
3146
- `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s`
3147
- );
3192
+ if (framesAdvanced) {
3193
+ this.lastVideoFrameCount = frames;
3194
+ this.lastVideoFrameProgressTime = now;
3195
+ }
3196
+ if (health.escalate) {
3197
+ 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`;
3198
+ void this.escalate(reason);
3148
3199
  }
3149
3200
  }, 1e3);
3150
3201
  const onError = () => {
@@ -3381,5 +3432,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3381
3432
  exports.UnifiedPlayer = UnifiedPlayer;
3382
3433
  exports.classifyContext = classifyContext;
3383
3434
  exports.createPlayer = createPlayer;
3384
- //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map
3385
- //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map
3435
+ //# sourceMappingURL=chunk-YX4AGLNF.cjs.map
3436
+ //# sourceMappingURL=chunk-YX4AGLNF.cjs.map