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.
+57 -17
View File
@@ -52,20 +52,34 @@
overflow: hidden;
}
.text-source::before {
content: '';
content: "";
position: absolute;
width: 700px; height: 700px; border-radius: 50%;
background: radial-gradient(circle, rgba(0,212,255,0.06) 0%, transparent 70%);
top: -150px; right: -100px; pointer-events: none;
width: 700px;
height: 700px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 212, 255, 0.06) 0%, transparent 70%);
top: -150px;
right: -100px;
pointer-events: none;
}
.text-source::after {
content: '';
content: "";
position: absolute;
width: 500px; height: 500px; border-radius: 50%;
background: radial-gradient(circle, rgba(124,58,237,0.05) 0%, transparent 70%);
bottom: -100px; left: -50px; pointer-events: none;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(124, 58, 237, 0.05) 0%, transparent 70%);
bottom: -100px;
left: -50px;
pointer-events: none;
}
.badge {
font-size: 14px;
font-weight: 600;
letter-spacing: 3px;
text-transform: uppercase;
color: rgba(0, 212, 255, 0.8);
}
.badge { font-size: 14px; font-weight: 600; letter-spacing: 3px; text-transform: uppercase; color: rgba(0,212,255,0.8); }
.text-source h1 {
font-size: 148px;
font-weight: 900;
@@ -83,7 +97,7 @@
.subtitle {
font-size: 28px;
font-weight: 400;
color: rgba(255,255,255,0.35);
color: rgba(255, 255, 255, 0.35);
max-width: 800px;
line-height: 1.5;
}
@@ -92,9 +106,23 @@
gap: 48px;
margin-top: 16px;
}
.stat { text-align: center; }
.stat-val { font-size: 42px; font-weight: 800; color: #00d4ff; letter-spacing: -1px; }
.stat-label { font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.3); text-transform: uppercase; letter-spacing: 2px; margin-top: 4px; }
.stat {
text-align: center;
}
.stat-val {
font-size: 42px;
font-weight: 800;
color: #00d4ff;
letter-spacing: -1px;
}
.stat-label {
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.3);
text-transform: uppercase;
letter-spacing: 2px;
margin-top: 4px;
}
</style>
</head>
<body>
@@ -125,11 +153,23 @@
<div class="text-source">
<div class="badge">Write HTML → Render Video</div>
<h1>Ship videos <span>10x faster</span></h1>
<div class="subtitle">HTML is the source of truth for video. No timeline editors, no After Effects — just code.</div>
<div class="subtitle">
HTML is the source of truth for video. No timeline editors, no After Effects — just
code.
</div>
<div class="stats">
<div class="stat"><div class="stat-val">47x</div><div class="stat-label">Faster than AE</div></div>
<div class="stat"><div class="stat-val">12.4K</div><div class="stat-label">Creators</div></div>
<div class="stat"><div class="stat-val">2.4M</div><div class="stat-label">Videos Rendered</div></div>
<div class="stat">
<div class="stat-val">47x</div>
<div class="stat-label">Faster than AE</div>
</div>
<div class="stat">
<div class="stat-val">12.4K</div>
<div class="stat-label">Creators</div>
</div>
<div class="stat">
<div class="stat-val">2.4M</div>
<div class="stat-label">Videos Rendered</div>
</div>
</div>
</div>
</canvas>
+1 -1
View File
@@ -252,4 +252,4 @@
"type": "hyperframes:block"
}
]
}
}