mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
feat(slideshow): per-slide autoplay (manual-advance, opt-in) (#1708)
* feat(slideshow): per-slide autoplay (manual-advance, opt-in) Adds an opt-in `autoplay` flag to slideshow slides: when the presenter lands on a video slide, its `<video>` plays from the start. The slideshow still holds and never auto-advances — the presenter clicks Next when ready. This covers compositions whose own controls can't be clicked (the player renders the composition pointer-events:none). Plumbing (done, tested): - core: `SlideRef.autoplay?: boolean`, parsed + validated in parseSlideshow (a non-boolean autoplay rejects the manifest); carried through resolve. - controller: optional `PlayerPort.playSceneMedia(sceneId)`, fired only on forward `enterSlide` for autoplay slides (not resume/back/sync, so the audience — which mirrors the presenter's media events — isn't double-driven). - component: `playSceneDocumentMedia` reaches the same-origin composition iframe, finds the scene's `<video>`, and asserts playback; `stopMedia` (already wired on slide change) resets it. An autoplay token cancels a pending start when the slide changes. - tests: controller autoplay behavior + parser flag round-trip/validation (131 player + 22 core slideshow tests pass). KNOWN LIMITATION — runtime media-start needs the player media model (@vance): On current main the clip<->timeline binding from #1601 keeps every clip synced and *paused* to the held timeline frame, which wins against playSceneMedia's play() — so the clip does not actually start on main yet (it does on the pre-#1601 player). The correct fix is a sanctioned "let this clip free-run while the timeline holds" path in the player/runtime media controller. Flagging for Vance to wire the start into the #1601 media model (or rebase onto it) when back. The plumbing above is the stable surface that hook plugs into. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(slideshow): address code-review on per-slide autoplay - guard playSceneDocumentMedia behind resolveMode() !== "audience": the audience mirrors the presenter's media events, so it must not independently drive its own copy of the clip. - drop the per-enter window pointerdown/keydown "gesture retry" listeners, which leaked when muted autoplay succeeded without a gesture. The poll already re-asserts play(), so a gesture within the window is picked up next tick. - stop polling once the clip is advancing across two ticks (was re-asserting play() for the full window even after playback was confirmed). - cancel any in-flight autoplay loop on disconnectedCallback (bump the token). - split the poll into findSceneVideo + stepAutoplay helpers (keeps each small). - fix the enterSlide comment: autoplay fires from enterSlide (next/prev/ goToSlide), not resumeSlide (back/backToMain/syncTo). - parser: isOptionalBoolean type guard instead of a one-off helper; drop `as` assertions in the new controller test. 131 player + 22 core slideshow tests pass; lint/format/typecheck/fallow clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(slideshow): autoplay skill guidance + address review nits Addresses review feedback on #1708: - skill: document per-slide `autoplay` in the slideshow standalone-harness reference — when to use it (video is the slide's primary content, its end is the advance cue) vs not (background/ambient loops, footage talked over), per Vance's guidance, before merge. - play() rejection is no longer blanket-swallowed: AbortError (timeline-sync seek interrupt) and NotAllowedError (gesture-gated autoplay) are expected and ignored; any other rejection is surfaced once via console.warn (Via nit 1). - clarify in the SlideRef.autoplay doc that it plays the scene's FIRST <video> (Via nit 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
041f2fa196
commit
7517f6ac86
@@ -13,6 +13,7 @@ function fakePlayer() {
|
||||
play: vi.fn(() => {}),
|
||||
pause: vi.fn(() => {}),
|
||||
stopMedia: vi.fn(() => {}),
|
||||
playSceneMedia: vi.fn((_sceneId: string) => {}),
|
||||
onTimeUpdate: (fn: (t: number) => void) => {
|
||||
cb = fn;
|
||||
return () => {
|
||||
@@ -683,3 +684,47 @@ describe("SlideshowController syncTo", () => {
|
||||
expect(c.position.slideIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SlideshowController autoplay", () => {
|
||||
// "v" autoplays; "w" does not. Both sit on the main line.
|
||||
const AUTOPLAY_SHOW: ResolvedSlideshow = {
|
||||
slides: [
|
||||
{ sceneId: "v", start: 0, end: 5, fragments: [], hotspots: [], autoplay: true },
|
||||
{ sceneId: "w", start: 5, end: 10, fragments: [], hotspots: [] },
|
||||
],
|
||||
sequences: {},
|
||||
};
|
||||
|
||||
it("plays the slide's media on enter when autoplay is set", () => {
|
||||
const p = fakePlayer();
|
||||
new SlideshowController(p, AUTOPLAY_SHOW); // constructs on slide "v"
|
||||
expect(p.playSceneMedia).toHaveBeenCalledWith("v");
|
||||
expect(p.playSceneMedia).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not play media when entering a non-autoplay slide", () => {
|
||||
const p = fakePlayer();
|
||||
const c = new SlideshowController(p, AUTOPLAY_SHOW);
|
||||
p.playSceneMedia.mockClear();
|
||||
c.next(); // v → w (w is not autoplay)
|
||||
expect(c.position.slideIndex).toBe(1);
|
||||
expect(p.playSceneMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops prior media and plays again when navigating back into an autoplay slide", () => {
|
||||
const p = fakePlayer();
|
||||
const c = new SlideshowController(p, AUTOPLAY_SHOW);
|
||||
p.playSceneMedia.mockClear();
|
||||
c.next(); // → w
|
||||
expect(p.stopMedia).toHaveBeenCalled(); // leaving v stops its clip
|
||||
p.playSceneMedia.mockClear();
|
||||
c.prev(); // back into v (enterSlide) → replays
|
||||
expect(p.playSceneMedia).toHaveBeenCalledWith("v");
|
||||
});
|
||||
|
||||
it("does not require autoplay support on the port (optional hook)", () => {
|
||||
// A port without playSceneMedia must not throw when entering an autoplay slide.
|
||||
const { playSceneMedia: _omitted, ...port } = fakePlayer();
|
||||
expect(() => new SlideshowController(port, AUTOPLAY_SHOW)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user