diff --git a/README.md b/README.md index 7d564d9a..8bfdb0a0 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2002 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2003 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/src/viewport/VideoLibrary.ts b/src/viewport/VideoLibrary.ts index c5704c12..45a06e94 100644 --- a/src/viewport/VideoLibrary.ts +++ b/src/viewport/VideoLibrary.ts @@ -148,7 +148,8 @@ export class VideoLibrary { target: drivenTimeOf(play, drive.time, element.duration), speed: drive.reading?.speed ?? 0, current: element.currentTime, - end: play.end, + // A video that ran to the end of its file starts over from its first picture on play(). + end: element.ended ? element.currentTime : Math.min(play.end, element.duration), }); if (step.seek !== null) this.seek(element, drive, step.seek); if (step.kind === 'hold') { diff --git a/tools/e2e/66-video.mjs b/tools/e2e/66-video.mjs index d0ecf7d0..61415be4 100644 --- a/tools/e2e/66-video.mjs +++ b/tools/e2e/66-video.mjs @@ -381,6 +381,35 @@ export async function video(t) { JSON.stringify(await rowKeys()) === keyed, JSON.stringify({ ran, steps, duration: uploaded.duration, undone: await rowKeys() }), ); + + // 66o. Played once to 1.95 s, the video runs on past the end of its file while the time still + // reads as moving. play() on an ended video starts it over from its first picture, and red + // flashed two or three times before the white of the last frame stood. Every frame is read in + // the page. + await mutate(` + const cut = __gm.state.project.assets.find((a) => a.id === '${CUT}'); + cut.props.playback = 'once'; + cut.tracks[0].keys = [{ frame: 0, value: 0.05 }, { frame: 19, value: 1.95 }];`); + await t.evaluate('__gm.animation.scrub(0)'); + await waitFor(previewColor, (color) => color === COLORS.red, 10); + await t.evaluate(`window.__vd66 = []; + (function read() { + const player = __gm.videos().find((v) => v.key.includes('@') && !v.key.includes('${EARLY}') && !v.key.includes('${LATE}')); + if (player) window.__vd66.push(Math.round(player.time * 1000) / 1000); + if (window.__vd66.length < 400) requestAnimationFrame(read); + })();`); + await click(playCut.x, playCut.y); + await sleep(2800); + const times = await json('window.__vd66.splice(0)'); + const endColor = await previewColor(); + if ((await json('__gm.animation.playAt()')).text !== 'Play') await click(playCut.x, playCut.y); + const reached = times.findIndex((time) => time >= 1.8); + const fellBack = reached < 0 ? [] : times.slice(reached).filter((time) => time < 1.5); + check( + 'a video played once to its last frame stays there and never shows its first picture again', + reached >= 0 && fellBack.length === 0 && endColor === COLORS.white, + JSON.stringify({ reached, fellBack, last: times.slice(-5), frames: times.length, endColor }), + ); await showTab('scene'); // 66m. Switched off, a driven sprite keeps its player ready at its Video time, longer than a player