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.
package/dist/player.d.cts CHANGED
@@ -358,6 +358,19 @@ declare class AvbridgePlayerElement extends HTMLElement {
358
358
  private _updateStats;
359
359
  private _toggleFullscreen;
360
360
  private _updateFullscreenIcon;
361
+ /**
362
+ * Reveal the auto-hiding chrome (top toolbar + bottom controls) and
363
+ * re-start the auto-hide timer. Call this from app-level code to
364
+ * briefly surface the player UI — e.g. to confirm "you just swiped to
365
+ * this video" in a carousel, or to flash the title on focus change.
366
+ *
367
+ * @param durationMs How long the chrome stays visible before fading.
368
+ * Defaults to the player's normal 3 s auto-hide.
369
+ * Pointer movement or any other interaction resets
370
+ * the timer, so a user hovering during the flash
371
+ * sees no flicker.
372
+ */
373
+ showControls(durationMs?: number): void;
361
374
  private _showControls;
362
375
  private _scheduleHide;
363
376
  /** Track whether the last interaction was touch so click handler can skip. */
@@ -446,6 +459,13 @@ declare class UnifiedPlayer {
446
459
  private stallTimer;
447
460
  private lastProgressTime;
448
461
  private lastProgressPosition;
462
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
463
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
464
+ * watchdog — catches cases where `currentTime` advances (audio plays)
465
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
466
+ * via MSE when the decoder actually can't decode HEVC. */
467
+ private lastVideoFrameCount;
468
+ private lastVideoFrameProgressTime;
449
469
  private errorListener;
450
470
  private endedListener;
451
471
  private userIntent;
package/dist/player.d.ts CHANGED
@@ -358,6 +358,19 @@ declare class AvbridgePlayerElement extends HTMLElement {
358
358
  private _updateStats;
359
359
  private _toggleFullscreen;
360
360
  private _updateFullscreenIcon;
361
+ /**
362
+ * Reveal the auto-hiding chrome (top toolbar + bottom controls) and
363
+ * re-start the auto-hide timer. Call this from app-level code to
364
+ * briefly surface the player UI — e.g. to confirm "you just swiped to
365
+ * this video" in a carousel, or to flash the title on focus change.
366
+ *
367
+ * @param durationMs How long the chrome stays visible before fading.
368
+ * Defaults to the player's normal 3 s auto-hide.
369
+ * Pointer movement or any other interaction resets
370
+ * the timer, so a user hovering during the flash
371
+ * sees no flicker.
372
+ */
373
+ showControls(durationMs?: number): void;
361
374
  private _showControls;
362
375
  private _scheduleHide;
363
376
  /** Track whether the last interaction was touch so click handler can skip. */
@@ -446,6 +459,13 @@ declare class UnifiedPlayer {
446
459
  private stallTimer;
447
460
  private lastProgressTime;
448
461
  private lastProgressPosition;
462
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
463
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
464
+ * watchdog — catches cases where `currentTime` advances (audio plays)
465
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
466
+ * via MSE when the decoder actually can't decode HEVC. */
467
+ private lastVideoFrameCount;
468
+ private lastVideoFrameProgressTime;
449
469
  private errorListener;
450
470
  private endedListener;
451
471
  private userIntent;
package/dist/player.js CHANGED
@@ -510,10 +510,12 @@ function classifyContext(ctx) {
510
510
  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`
511
511
  };
512
512
  }
513
+ const fallbackChain = webCodecsAvailable() ? ["hybrid", "fallback"] : ["fallback"];
513
514
  return {
514
515
  class: "REMUX_CANDIDATE",
515
516
  strategy: "remux",
516
- reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`
517
+ reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`,
518
+ fallbackChain
517
519
  };
518
520
  }
519
521
  if (webCodecsAvailable()) {
@@ -3258,6 +3260,29 @@ function registerBuiltins(registry) {
3258
3260
  }
3259
3261
 
3260
3262
  // src/player.ts
3263
+ function readDecodedFrameCount(target) {
3264
+ if (typeof HTMLVideoElement === "undefined" || !(target instanceof HTMLVideoElement)) return 0;
3265
+ const vq = target.getVideoPlaybackQuality;
3266
+ if (typeof vq === "function") {
3267
+ try {
3268
+ return vq.call(target).totalVideoFrames;
3269
+ } catch {
3270
+ }
3271
+ }
3272
+ const legacy = target.webkitDecodedFrameCount;
3273
+ return typeof legacy === "number" ? legacy : 0;
3274
+ }
3275
+ function evaluateDecodeHealth(input) {
3276
+ const timeThreshold = input.timeStallThresholdMs ?? 5e3;
3277
+ const frameThreshold = input.frameStallThresholdMs ?? 3e3;
3278
+ if (!input.timeAdvanced && input.now - input.lastProgressTime > timeThreshold) {
3279
+ return { escalate: true, kind: "time-stall" };
3280
+ }
3281
+ if (input.hasVideoTrack && input.timeAdvanced && !input.framesAdvanced && input.now - input.lastFrameProgressTime > frameThreshold) {
3282
+ return { escalate: true, kind: "silent-video" };
3283
+ }
3284
+ return { escalate: false };
3285
+ }
3261
3286
  var UnifiedPlayer = class _UnifiedPlayer {
3262
3287
  /**
3263
3288
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -3283,6 +3308,13 @@ var UnifiedPlayer = class _UnifiedPlayer {
3283
3308
  stallTimer = null;
3284
3309
  lastProgressTime = 0;
3285
3310
  lastProgressPosition = -1;
3311
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
3312
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
3313
+ * watchdog — catches cases where `currentTime` advances (audio plays)
3314
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
3315
+ * via MSE when the decoder actually can't decode HEVC. */
3316
+ lastVideoFrameCount = 0;
3317
+ lastVideoFrameProgressTime = 0;
3286
3318
  errorListener = null;
3287
3319
  // Bound so we can removeEventListener in destroy(); without this the
3288
3320
  // listener outlives the player and accumulates on elements that swap
@@ -3527,22 +3559,41 @@ var UnifiedPlayer = class _UnifiedPlayer {
3527
3559
  if (strategy === "native" || strategy === "remux") {
3528
3560
  this.lastProgressPosition = this.options.target.currentTime;
3529
3561
  this.lastProgressTime = performance.now();
3562
+ this.lastVideoFrameCount = readDecodedFrameCount(this.options.target);
3563
+ this.lastVideoFrameProgressTime = performance.now();
3564
+ const hasVideoTrack = (this.mediaContext?.videoTracks.length ?? 0) > 0;
3530
3565
  this.stallTimer = setInterval(() => {
3531
3566
  const t = this.options.target;
3567
+ const now = performance.now();
3532
3568
  if (t.paused || t.ended || t.readyState < 2) {
3533
3569
  this.lastProgressPosition = t.currentTime;
3534
- this.lastProgressTime = performance.now();
3570
+ this.lastProgressTime = now;
3571
+ this.lastVideoFrameCount = readDecodedFrameCount(t);
3572
+ this.lastVideoFrameProgressTime = now;
3535
3573
  return;
3536
3574
  }
3537
- if (t.currentTime !== this.lastProgressPosition) {
3575
+ const timeAdvanced = t.currentTime !== this.lastProgressPosition;
3576
+ const frames = readDecodedFrameCount(t);
3577
+ const framesAdvanced = frames > this.lastVideoFrameCount;
3578
+ const health = evaluateDecodeHealth({
3579
+ hasVideoTrack,
3580
+ timeAdvanced,
3581
+ framesAdvanced,
3582
+ now,
3583
+ lastProgressTime: this.lastProgressTime,
3584
+ lastFrameProgressTime: this.lastVideoFrameProgressTime
3585
+ });
3586
+ if (timeAdvanced) {
3538
3587
  this.lastProgressPosition = t.currentTime;
3539
- this.lastProgressTime = performance.now();
3540
- return;
3588
+ this.lastProgressTime = now;
3541
3589
  }
3542
- if (performance.now() - this.lastProgressTime > 5e3) {
3543
- void this.escalate(
3544
- `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s`
3545
- );
3590
+ if (framesAdvanced) {
3591
+ this.lastVideoFrameCount = frames;
3592
+ this.lastVideoFrameProgressTime = now;
3593
+ }
3594
+ if (health.escalate) {
3595
+ 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`;
3596
+ void this.escalate(reason);
3546
3597
  }
3547
3598
  }, 1e3);
3548
3599
  const onError = () => {
@@ -5646,12 +5697,27 @@ var AvbridgePlayerElement = class extends HTMLElement {
5646
5697
  this._fullscreenBtn.innerHTML = fs ? ICON_FULLSCREEN_EXIT : ICON_FULLSCREEN;
5647
5698
  }
5648
5699
  // ── Controls: auto-hide ────────────────────────────────────────────────
5649
- _showControls() {
5700
+ /**
5701
+ * Reveal the auto-hiding chrome (top toolbar + bottom controls) and
5702
+ * re-start the auto-hide timer. Call this from app-level code to
5703
+ * briefly surface the player UI — e.g. to confirm "you just swiped to
5704
+ * this video" in a carousel, or to flash the title on focus change.
5705
+ *
5706
+ * @param durationMs How long the chrome stays visible before fading.
5707
+ * Defaults to the player's normal 3 s auto-hide.
5708
+ * Pointer movement or any other interaction resets
5709
+ * the timer, so a user hovering during the flash
5710
+ * sees no flicker.
5711
+ */
5712
+ showControls(durationMs) {
5650
5713
  this.removeAttribute("data-controls-hidden");
5651
5714
  this._toolbarTop.setAttribute("data-visible", "true");
5652
- this._scheduleHide();
5715
+ this._scheduleHide(durationMs);
5716
+ }
5717
+ _showControls() {
5718
+ this.showControls();
5653
5719
  }
5654
- _scheduleHide() {
5720
+ _scheduleHide(durationMs = CONTROLS_HIDE_MS) {
5655
5721
  if (this._controlsTimer) clearTimeout(this._controlsTimer);
5656
5722
  if (this._state !== "playing" && this._state !== "buffering") return;
5657
5723
  if (this._settingsOpen) return;
@@ -5660,7 +5726,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5660
5726
  this.setAttribute("data-controls-hidden", "");
5661
5727
  this._toolbarTop.setAttribute("data-visible", "false");
5662
5728
  }
5663
- }, CONTROLS_HIDE_MS);
5729
+ }, durationMs);
5664
5730
  }
5665
5731
  // Strategy is visible in Stats for Nerds, no badge in controls bar.
5666
5732
  // ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────