fix(player): replay from start when play is pressed after video ends (#649)

* fix(player): replay from start when play is pressed after video ends

When a non-looping composition reaches its end, pressing play again
had no effect because the playhead stayed at the final frame. Now
play() detects the ended state and seeks to 0 before resuming.

* chore: fix pre-existing format issues in registry files
This commit is contained in:
Miguel Ángel
2026-05-06 20:44:48 +02:00
committed by GitHub
parent 7174c4cdcd
commit 632731b076
4 changed files with 111 additions and 18 deletions
@@ -1010,6 +1010,56 @@ describe("HyperframesPlayer loop end-state handling", () => {
expect(ended).toHaveBeenCalledTimes(1);
expect(player._paused).toBe(true);
});
it("play() seeks to 0 and replays when called after the video has ended", () => {
const seek = vi.spyOn(player, "seek");
player.loop = false;
player._duration = 4;
player._paused = false;
player._onMessage(
new MessageEvent("message", {
source: frameWindow,
data: {
source: "hf-preview",
type: "state",
frame: 120,
isPlaying: false,
},
}),
);
expect(player._paused).toBe(true);
seek.mockClear();
player.play();
expect(seek).toHaveBeenCalledWith(0);
expect(player._paused).toBe(false);
});
it("play() does not seek to 0 when called mid-playback", () => {
const seek = vi.spyOn(player, "seek");
player._duration = 4;
player._paused = true;
// Simulate mid-video position (frame 60 = 2s into a 4s video)
player._onMessage(
new MessageEvent("message", {
source: frameWindow,
data: {
source: "hf-preview",
type: "state",
frame: 60,
isPlaying: false,
},
}),
);
player.play();
expect(seek).not.toHaveBeenCalled();
expect(player._paused).toBe(false);
});
});
describe("HyperframesPlayer srcdoc attribute", () => {
@@ -402,6 +402,9 @@ class HyperframesPlayer extends HTMLElement {
play() {
this._hidePoster();
if (this._duration > 0 && this._currentTime >= this._duration) {
this.seek(0);
}
// Always drive the iframe runtime — it's the single source of timeline
// truth regardless of who owns audible output. When we own audio, the
// proxies join; when the runtime owns, they stay silent.