diff --git a/src/editor/PlaySession.ts b/src/editor/PlaySession.ts index fbefd24e..6236a54b 100644 --- a/src/editor/PlaySession.ts +++ b/src/editor/PlaySession.ts @@ -317,6 +317,8 @@ export class PlaySession { /** The Game tab has drawn the new world once by now, which opened the players it shows. */ private waitForVideos(): void { this.videoWait = this.host.gameView.videosLoading() ? performance.now() + VIDEO_WAIT_MS : null; + // From now, not from the next tick: a free video started at Play would be a frame ahead. + videoLibrary.setHeld(this.videoWait !== null); } /** Whether the run still waits for its videos; the frame it stops waiting, the notice says so. */ @@ -395,12 +397,10 @@ export class PlaySession { if (!run) return; const world = run.world; this.tellPace(); - if ( - (this.state.paused && !this.stepping) || - this.waitsForVideos() || - this.recorder.isStarting || - this.recordStep === 0 - ) { + // The world does not tick while it waits, and the free videos wait with it (#437). + const waits = this.waitsForVideos() || this.recorder.isStarting; + videoLibrary.setHeld(waits); + if ((this.state.paused && !this.stepping) || waits || this.recordStep === 0) { // Keys and clicks that happen while paused are dropped instead of piling up – otherwise // resuming would deliver every one of them in a single frame. this.input.endFrame(); diff --git a/src/player/Player.ts b/src/player/Player.ts index bc3c5e10..6ed21144 100644 --- a/src/player/Player.ts +++ b/src/player/Player.ts @@ -112,6 +112,8 @@ export class Player { // A recording that sets up its encoders holds the run too: its frames meanwhile would be lost. // One not in real time (#423) moves it by one of its frames, or holds it until it has taken one. const waits = this.waitsForVideos() || this.recorder.isStarting; + // The world does not tick while it waits, and the free videos wait with it (#437). + videoLibrary.setHeld(waits); const recorded = waits ? null : this.recorder.step(); if (waits || recorded === 0) { this.input.endFrame(); @@ -143,6 +145,8 @@ export class Player { /** The screen has drawn the new world once by now, which opened the players it shows. */ private waitForVideos(): void { this.videoWait = this.screen.videosLoading() ? performance.now() + VIDEO_WAIT_MS : null; + // From now, not from the next tick: a free video started at Play would be a frame ahead. + videoLibrary.setHeld(this.videoWait !== null); } private waitsForVideos(): boolean { diff --git a/src/viewport/VideoLibrary.ts b/src/viewport/VideoLibrary.ts index 09fb3aec..a86794b5 100644 --- a/src/viewport/VideoLibrary.ts +++ b/src/viewport/VideoLibrary.ts @@ -4,7 +4,8 @@ * same clip share a player and show the same picture. * * In the editor a video plays without sound, as a preview. A run starts every player over at its - * clip's start and lets it be heard along the master routing of its video; a pause holds them, and + * clip's start and lets it be heard along the master routing of its video; a pause holds them, as + * does the run waiting for its driven videos or its recording (see `setHeld`), and * the end of the run makes them quiet previews again. * * A sprite whose Video clock is driven (#404) has a player of its own instead, which shows the @@ -118,6 +119,8 @@ export class VideoLibrary { private readonly listeners = new Set<() => void>(); private heard = false; private paused = false; + /** The free players wait with a run that waits for its driven videos or its recording (#437). */ + private held = false; /** Driven players hold at every picture asked rather than play: a recording takes each (#423). */ private stepped = false; private sweeper: ReturnType | null = null; @@ -201,6 +204,7 @@ export class VideoLibrary { startRun(): void { this.heard = true; this.paused = false; + this.held = false; for (const player of this.freePlayers()) { if (!player.hushed) this.hear(player); this.rewind(player); @@ -211,6 +215,7 @@ export class VideoLibrary { endRun(): void { this.heard = false; this.paused = false; + this.held = false; for (const player of this.freePlayers()) { player.element.muted = true; this.rewind(player); @@ -219,9 +224,34 @@ export class VideoLibrary { setPaused(paused: boolean): void { this.paused = paused; + this.settle(); + } + + /** + * While a run waits at its start or after a Load scene (#437), its world does not tick, and the + * clock of every free sprite stands. Its player holds too, so the picture keeps to that clock: + * else On video finished came late by the wait, and a Pause video sent the picture back. + */ + setHeld(held: boolean): void { + if (this.held === held) return; + this.held = held; + this.settle(); + } + + /** Whether the free players stand: paused by the toolbar or held by a waiting run. */ + private get still(): boolean { + return this.paused || this.held; + } + + /** + * Holds or plays the free players as `still` says; one without its first picture plays on to get + * it. One held at the end of a clip without Loop stays there: play() would start it over. + */ + private settle(): void { for (const player of this.freePlayers()) - if (paused) player.element.pause(); - else if (!player.hushed) void player.element.play().catch(() => {}); + if (this.still) { + if (player.texture) player.element.pause(); + } else if (!player.hushed && !atEnd(player)) void player.element.play().catch(() => {}); } private freePlayers(): Player[] { @@ -320,7 +350,7 @@ export class VideoLibrary { private unhush(player: Player): void { player.hushed = false; if (this.heard) this.hear(player); - if (!this.paused) void player.element.play().catch(() => {}); + if (!this.still) void player.element.play().catch(() => {}); } private stopSweeping(): void { @@ -371,8 +401,8 @@ export class VideoLibrary { if (player.gone) return texture.destroy(true); player.texture = texture; if (drive) this.follow(player, drive); - // Started even in a paused run or when nothing wears it, and held once there is a picture. - else if (this.paused || player.hushed) player.element.pause(); + // Started even in a paused or waiting run or when nothing wears it, and held once there is a picture. + else if (this.still || player.hushed) player.element.pause(); this.notify(); }), texture: null, @@ -449,7 +479,7 @@ export class VideoLibrary { private rewind(player: Player): void { player.element.currentTime = player.play.start; - if (!this.paused && !player.hushed) void player.element.play().catch(() => {}); + if (!this.still && !player.hushed) void player.element.play().catch(() => {}); } private drop(player: Player): void { @@ -482,6 +512,11 @@ export class VideoLibrary { export const videoLibrary = new VideoLibrary(); +/** Whether a free player stands at the end of a clip without Loop, as `watch` holds it. */ +function atEnd({ element, play }: Player): boolean { + return !play.loop && (element.ended || element.currentTime >= play.end - END_SLACK); +} + /** * A `