Record a game from its very first frame
Start recording has to set up its encoders first. It asks the browser whether it can encode H.264 at the size and hooks in the sound tap, both asynchronously, and it only took its size at the first frame. Meanwhile the game ran on unrecorded. A caption that fades in from alpha 0 at the start was already visible in the first recorded frame. - A run now waits while a recording sets up its encoders, at most 3 s, as it already waits for its videos at Play. That holds in the editor and in play.html. - The recording takes its size at Play, right after the game view has the world, rather than at the first frame. A Start recording in On start is so ready before the first tick. - A recording started in the middle of a game holds the picture for that moment. A sound that already plays goes on. Tests: - tsc, eslint, Prettier clean - e2e 67 on melektaus-atx: ALL PASSED, 11 checks (67a-ii new). The first recorded frame is at game time 0.0168 s, the first frame. - Without the wait 67a-ii is red: the first recorded frame is at 0.1667 s, so 10 frames were lost. - Gate on melektaus-atx: green, 305 files, 4740 tests, run on this commit and the one before together. - Not checked in the browser: a start in the middle of a game, and play.html. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ba41c1bd0
commit
3443368855
7 files changed
+68
-6
No files matched your search
@@ -1540,7 +1540,10 @@ export class Editor implements PlayHost {
|
||||
playing: boolean;
|
||||
paused: boolean;
|
||||
gamePaused: boolean;
|
||||
/** Held at the start until the Game tab's driven videos have a picture (#404). */
|
||||
/**
|
||||
* Held at the start until the Game tab's driven videos have a picture (#404), or while a
|
||||
* recording sets up its encoders.
|
||||
*/
|
||||
waiting: boolean;
|
||||
time: number;
|
||||
sceneId: string;
|
||||
|
||||
@@ -127,9 +127,12 @@ export class PlaySession {
|
||||
return this.world?.paused ?? false;
|
||||
}
|
||||
|
||||
/** Held at its start until the Game tab has a picture of its driven videos (#404). */
|
||||
/**
|
||||
* Held at its start until the Game tab has a picture of its driven videos (#404), and while a
|
||||
* recording sets up its encoders.
|
||||
*/
|
||||
get isWaiting(): boolean {
|
||||
return this.videoWait !== null;
|
||||
return this.videoWait !== null || this.recorder.isStarting;
|
||||
}
|
||||
|
||||
/** Pause or resume; before Play it switches whether Play starts paused. */
|
||||
@@ -235,6 +238,7 @@ export class PlaySession {
|
||||
this.host.gameView.setWorld(this.run.world);
|
||||
this.host.showGame();
|
||||
this.waitForVideos();
|
||||
this.recorder.prepare();
|
||||
if (paused) {
|
||||
this.state.setPaused(true);
|
||||
this.applyPause(false);
|
||||
@@ -331,7 +335,7 @@ export class PlaySession {
|
||||
const run = this.run;
|
||||
if (!run) return;
|
||||
const world = run.world;
|
||||
if ((this.state.paused && !this.stepping) || this.waitsForVideos()) {
|
||||
if ((this.state.paused && !this.stepping) || this.waitsForVideos() || this.recorder.isStarting) {
|
||||
// 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();
|
||||
|
||||
@@ -78,6 +78,7 @@ export class Player {
|
||||
this.input.attach();
|
||||
this.screen.setWorld(this.run.world);
|
||||
this.waitForVideos();
|
||||
this.recorder.prepare();
|
||||
if (this.run.world.stopRequested) this.stop();
|
||||
}
|
||||
|
||||
@@ -103,7 +104,8 @@ export class Player {
|
||||
tick(dt: number): void {
|
||||
const run = this.run;
|
||||
if (!run) return;
|
||||
if (this.waitsForVideos()) {
|
||||
// A recording that sets up its encoders holds the run too: its frames meanwhile would be lost.
|
||||
if (this.waitsForVideos() || this.recorder.isStarting) {
|
||||
this.input.endFrame();
|
||||
run.world.dropPointerEdges();
|
||||
this.screen.redraw();
|
||||
|
||||
@@ -44,6 +44,19 @@ export class GameRecorder implements RecordingSink {
|
||||
return this.take !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a recording still sets up its encoders: the run waits for it, or the frames meanwhile
|
||||
* are lost – the first of a game that starts recording in `On start` among them.
|
||||
*/
|
||||
get isStarting(): boolean {
|
||||
return this.take?.isStarting ?? false;
|
||||
}
|
||||
|
||||
/** The view shows the world now: a recording started in `On start` takes its size already. */
|
||||
prepare(): void {
|
||||
if (this.host.view()) this.take?.prepare();
|
||||
}
|
||||
|
||||
start(settings: RecordingSettings): string {
|
||||
if (this.take)
|
||||
return `Start recording is ignored: "${this.take.name}" is recording already – Stop recording ends it`;
|
||||
|
||||
@@ -21,6 +21,8 @@ const FOLD_BYTES = 16 * 1024 * 1024;
|
||||
const LAST_SOUND_MS = 300;
|
||||
/** How long a stop waits for the frames still on their way to the encoder. */
|
||||
const LAST_FRAMES_MS = 500;
|
||||
/** How long a run waits for the encoders to be set up, see `isStarting`. */
|
||||
const START_WAIT_MS = 3000;
|
||||
|
||||
/** A recording once it is made. */
|
||||
export interface MadeRecording {
|
||||
@@ -120,6 +122,8 @@ export class Take {
|
||||
private heldSince: number | null = null;
|
||||
private lastFrameAt: number | null = null;
|
||||
private failure: string | null = null;
|
||||
/** When the encoders began to be set up, by `performance.now()`. */
|
||||
private startingAt = 0;
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
@@ -148,6 +152,7 @@ export class Take {
|
||||
if (!size) return;
|
||||
this.size = size;
|
||||
this.state = 'starting';
|
||||
this.startingAt = performance.now();
|
||||
void this.openEncoders().then(
|
||||
() => {
|
||||
if (this.state !== 'starting') return;
|
||||
@@ -234,6 +239,19 @@ export class Take {
|
||||
tap.node.port.onmessage = (event: MessageEvent<Float32Array[]>) => this.heard(event.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the encoders are still being set up, at most `START_WAIT_MS`: no frame can be taken
|
||||
* meanwhile, so a run waits for it rather than playing its first moments unrecorded.
|
||||
*/
|
||||
get isStarting(): boolean {
|
||||
return this.state === 'starting' && performance.now() - this.startingAt < START_WAIT_MS;
|
||||
}
|
||||
|
||||
/** Takes the size now, when there is a picture, rather than at the first frame (`capture`). */
|
||||
prepare(): void {
|
||||
if (this.state === 'waiting') this.open();
|
||||
}
|
||||
|
||||
setPaused(paused: boolean): void {
|
||||
this.stand(() => (this.paused = paused));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user