diff --git a/src/viewport/videoFollow.ts b/src/viewport/videoFollow.ts index 5fa32e3d..14039475 100644 --- a/src/viewport/videoFollow.ts +++ b/src/viewport/videoFollow.ts @@ -59,7 +59,11 @@ export function readSpeed(reading: SpeedReading | null, time: number, now: numbe while (samples.length > 2 && now - samples[1]!.at >= SPEED_SPAN_MS) samples.shift(); const first = samples[0]!; const span = now - first.at; - const pace = ((time - first.time) * 1000) / Math.max(span, 1); + // Asked twice in one frame, the time moved in no time at all: the first picture of a video + // redraws the view at once, then the game takes its first step in the same frame. Read over a + // millisecond that step was a jump and the video held a frame at its start (CI task 712), so it + // counts over a frame at 60 fps. + const pace = ((time - first.time) * 1000) / (span > 0 ? span : 1000 / 60); if (span >= SPEED_WINDOW_MS) return { samples, speed: pace }; // A start from standing plays at 1x until the window has its reading: held and sought frame by // frame instead, the first pictures of a cut stuck and were skipped. A step faster than a video diff --git a/tests/viewport/video-follow.test.ts b/tests/viewport/video-follow.test.ts index 3b79dc30..e8a9f1c9 100644 --- a/tests/viewport/video-follow.test.ts +++ b/tests/viewport/video-follow.test.ts @@ -44,6 +44,16 @@ describe('the speed of the asked time', () => { expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 + 2 / 60, 16).speed).toBe(1); }); + it('counts a first step in the same frame as the ask before it as a start', () => { + // The first picture redraws the view at once, then the game steps within the same frame. + let reading = readSpeed(null, 0.8, 0); + reading = readSpeed(reading, 0.8, 16); + reading = readSpeed(reading, 0.8 + 1 / 60, 16); + expect(reading.speed).toBe(1); + // Within one frame a jump is still a jump. + expect(readSpeed(readSpeed(null, 0.8, 0), 0.9, 0).speed).toBe(0); + }); + it('does not count a jump faster than a video plays, or a time running back, as a start', () => { expect(readSpeed(readSpeed(null, 0.8, 0), 0.9, 16).speed).toBe(0); expect(readSpeed(readSpeed(null, 0.8, 0), 0.8 - 1 / 60, 16).speed).toBe(0); diff --git a/tools/e2e/34-sound.mjs b/tools/e2e/34-sound.mjs index e57b5d2a..56fdde14 100644 --- a/tools/e2e/34-sound.mjs +++ b/tools/e2e/34-sound.mjs @@ -3986,7 +3986,15 @@ export async function sound(t) { resumedLong.streaming.skipped === 0 && stoppedLong.streaming.playing === 0 && stoppedLong.playing === 0, - JSON.stringify({ paused, pausedLater, resumedLong: resumedLong.streaming, stoppedLong }), + // Held before the pause too: it tells a stream that fell behind from the start from one the + // pause made lose its place (CI task 712). + JSON.stringify({ + held: heldStreamed.streaming, + paused, + pausedLater, + resumedLong: resumedLong.streaming, + stoppedLong, + }), ); // The counter-proof: Stream off, the game plays it decoded, and the renderer holds all of it. diff --git a/tools/e2e/68-composition.mjs b/tools/e2e/68-composition.mjs index fbee340e..cb3a9c35 100644 --- a/tools/e2e/68-composition.mjs +++ b/tools/e2e/68-composition.mjs @@ -244,20 +244,28 @@ export async function composition(t) { const p = __gm.state.project; p.assets.find((a) => a.id === 'cp68-video').clips = [{ id: 'cp68-late', name: 'Late', props: { start: 0.8, end: 0 } }]; p.assets.find((a) => a.id === 'cp68-screen').props.texture = 'cp68-late';`); - await play(); - const rows = JSON.parse( - await evaluate(`new Promise((done) => { - const rows = []; + // Sampled from before the click on Play, as in 68d: under load play() settles after the wait is + // over, and the first row was drawn already with the clock running (CI tasks 697, 703, 712). + await evaluate(`{ + const rows = []; + const until = performance.now() + 20000; + window.cp68Rows = new Promise((done) => { const step = () => { - const player = __gm.gameVideo('${id('v')}'); - rows.push({ time: ${clock}, drawn: __gm.gameDrawnRect('${id('v')}') !== null, - at: player?.time ?? null, paused: player?.paused ?? null }); - if (rows.length >= 240 || !__gm.world()) return done(JSON.stringify(rows)); + if (__gm.state.mode === 'play' && __gm.world()) { + const player = __gm.gameVideo('${id('v')}'); + rows.push({ time: ${clock}, drawn: __gm.gameDrawnRect('${id('v')}') !== null, + at: player?.time ?? null, paused: player?.paused ?? null }); + } + if (rows.length >= 240 || (rows.length > 0 && !__gm.world()) || performance.now() > until) + return done(JSON.stringify(rows)); requestAnimationFrame(step); }; step(); - })`), - ); + }); + null; + }`); + await play(); + const rows = JSON.parse(await evaluate('window.cp68Rows')); const first = rows.findIndex((row) => row.drawn); const moving = rows.filter((row) => row.drawn && row.time > 0).slice(0, 3); check(