fix(player): clamp scrubber progress when postMessage frame exceeds duration (#700)

The postMessage state path set `_currentTime` without clamping, while the
direct timeline path already used `Math.min(currentTime, _duration)`. A
final-frame state message with a frame count slightly past the end would
set `_currentTime > _duration`, causing the progress bar (position:
absolute, no overflow guard) to bleed out of the scrubber track and
visually cover the volume button, and the time display to show values
like "0:05 / 0:04".

- Clamp `_currentTime` in `_onMessage` to match the direct timeline path
- Clamp defensively in `updateTime` so the display layer never overflows
- Add `overflow: hidden` + `min-width: 0` to `.hfp-scrubber` as a CSS
  safety net; remove now-redundant `border-radius` from `.hfp-progress`
  (parent `overflow: hidden` handles clipping to the rounded shape)
- Apply the same `overflow: hidden` fix to `.hfp-volume-slider` for
  consistency; remove redundant `border-radius` from `.hfp-volume-fill`
- Add regression test covering the postMessage over-duration case

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
terencecho
2026-05-09 22:23:19 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 811f309ea5
commit dd375e2784
4 changed files with 34 additions and 5 deletions
+4 -2
View File
@@ -326,9 +326,11 @@ export function createControls(
return {
updateTime(current: number, duration: number) {
const pct = duration > 0 ? (current / duration) * 100 : 0;
// Defensive: source should already clamp, but guard here so the UI never overflows.
const clampedCurrent = duration > 0 ? Math.min(current, duration) : current;
const pct = duration > 0 ? (clampedCurrent / duration) * 100 : 0;
progress.style.width = `${pct}%`;
time.textContent = `${formatTime(current)} / ${formatTime(duration)}`;
time.textContent = `${formatTime(clampedCurrent)} / ${formatTime(duration)}`;
},
updatePlaying(playing: boolean) {
isPlaying = playing;