mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
811f309ea5
commit
dd375e2784
@@ -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;
|
||||
|
||||
@@ -1028,6 +1028,7 @@ describe("HyperframesPlayer loop end-state handling", () => {
|
||||
seek: (timeInSeconds: number) => void;
|
||||
loop: boolean;
|
||||
_duration: number;
|
||||
_currentTime: number;
|
||||
_paused: boolean;
|
||||
_onMessage: (event: MessageEvent) => void;
|
||||
};
|
||||
@@ -1152,6 +1153,30 @@ describe("HyperframesPlayer loop end-state handling", () => {
|
||||
expect(seek).not.toHaveBeenCalled();
|
||||
expect(player._paused).toBe(false);
|
||||
});
|
||||
|
||||
it("clamps _currentTime to _duration when a state message reports a frame past the end", () => {
|
||||
// Regression test: the postMessage state path previously set _currentTime
|
||||
// without clamping, while the direct timeline path already clamped. A frame
|
||||
// count slightly past the end (common on final-frame messages) would set
|
||||
// _currentTime > _duration, causing the progress bar to overflow the
|
||||
// scrubber track and the time display to show e.g. "0:05 / 0:04".
|
||||
player._duration = 4; // 4s = 120 frames at 30fps
|
||||
player._paused = false;
|
||||
|
||||
player._onMessage(
|
||||
new MessageEvent("message", {
|
||||
source: frameWindow,
|
||||
data: {
|
||||
source: "hf-preview",
|
||||
type: "state",
|
||||
frame: 150, // 5s — past the 4s duration
|
||||
isPlaying: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(player._currentTime).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HyperframesPlayer srcdoc attribute", () => {
|
||||
|
||||
@@ -1030,7 +1030,8 @@ class HyperframesPlayer extends HTMLElement {
|
||||
}
|
||||
|
||||
if (data.type === "state") {
|
||||
this._currentTime = (data.frame ?? 0) / DEFAULT_FPS;
|
||||
const rawTime = (data.frame ?? 0) / DEFAULT_FPS;
|
||||
this._currentTime = this._duration > 0 ? Math.min(rawTime, this._duration) : rawTime;
|
||||
const wasPlaying = !this._paused;
|
||||
const nextPaused = !data.isPlaying;
|
||||
const completedPlayback =
|
||||
|
||||
@@ -247,11 +247,13 @@ export const PLAYER_STYLES = /* css */ `
|
||||
|
||||
.hfp-scrubber {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: var(--hfp-scrubber-height, 4px);
|
||||
background: var(--hfp-scrubber-bg, rgba(255, 255, 255, 0.3));
|
||||
border-radius: var(--hfp-scrubber-radius, 2px);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hfp-scrubber:hover {
|
||||
@@ -264,7 +266,6 @@ export const PLAYER_STYLES = /* css */ `
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--hfp-accent, #fff);
|
||||
border-radius: var(--hfp-scrubber-radius, 2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -401,6 +402,7 @@ export const PLAYER_STYLES = /* css */ `
|
||||
border-radius: var(--hfp-scrubber-radius, 2px);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -411,7 +413,6 @@ export const PLAYER_STYLES = /* css */ `
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--hfp-accent, #fff);
|
||||
border-radius: var(--hfp-scrubber-radius, 2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user