Say when a recording starts and when it cannot keep up

A 4K60 game view recording on the user's machine had 517 of 827
frames. The rest were skipped without a word: when the editor's tick
came late, FrameGate let the due slots pass, and only frames lost to a
busy encoder or read back counted as dropped. The file kept its time
but stuttered, and nothing said so.

- FrameGate counts the frames it took and the slots the recording's
  time has room for. The difference is skipped, next to dropped in
  __gm.recording().
- FramePace judges the missed frames over spans of 2 s. The first
  time more than 10 % are missed, the log warns once with the frames
  a second the recording really got.
- At the end, when more than 5 % are missed, a warning gives the
  frames of the whole file and what helps (30 fps or a smaller
  resolution). It comes after the upload's notice, which would cover
  it at once in the editor.
- The first second counts for neither warning. Without that, a short
  e2e recording warned about 3 frames lost while starting.
- A recording that starts logs an info line with its size, fps, codec
  and sound. TakeHost.log takes a level for it. The editor recording
  keeps the line to the console, its notice would be in the video.
- consoleLine is the one place with an eslint exception for
  console.info, so the console panel shows the line plain.

Found along the way: headless Chrome in WSL records the editor with
real stutter in e2e 67 (1280x800@60 27 %, 2304x1440@30 12-14 %). That
is the software encoder there, and only now shown.

Tests:
- Gate on melektaus-atx: green, 309 files, 4810 tests.
- Unit recording.test.ts: 36 green, 6 new for FrameGate.skipped and
  FramePace. With skipped stuck at 0 the test turns red.
- e2e 67 on melektaus-atx: ALL PASSED, 15 checks, run twice. The new
  67b-ii slows the editor to about 20 fps: a 640x360@60 recording
  says "about 19 of 60 fps" and saves with 95 of 192 frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-14 11:21:46 +02:00
1 parent 4d0a696327
commit 77365f17d3
9 files changed
+283 -17

No files matched your search

+53
View File
@@ -120,7 +120,60 @@ export async function recording(t) {
log.some((line) => line.includes('Start recording is ignored') && line.includes(during.takes[1].name)),
JSON.stringify({ log, takes: during.takes.map((take) => take.name) }),
);
check(
'a recording says in the log when it starts: its size, fps and sound',
log.some((line) => line === `Recording "${NAME}" started: 1920x1080 at 30 fps, H.264, with sound`) &&
log.some(
(line) =>
line.startsWith(`Recording "${during.takes[1].name}" started: `) && line.includes(' at 60 fps'),
),
JSON.stringify(log),
);
// 67b-ii. Frames 45 ms apart, about 22 a second: the second take, at 60 fps, skips most of its
// frames. It says so once while it records, and again when it is saved, with the count.
await evaluate(`(() => {
window.__rc67slow = true;
const slow = () => {
if (!window.__rc67slow) return;
const until = performance.now() + 45;
while (performance.now() < until);
requestAnimationFrame(slow);
};
requestAnimationFrame(slow);
})()`);
const behind = `(__gm.world()?.log ?? []).find((line) => line.includes("can't keep up"))`;
await waitUntil(`${behind} !== undefined`, {
timeout: 15_000,
label: 'the slow take says it falls behind',
});
await evaluate('window.__rc67slow = false; 0');
const behindLine = await json(behind);
const slowTake = (await recordings()).takes[1];
const got = Number(/about (\d+) of 60 fps/.exec(behindLine ?? '')?.[1]);
check(
'a recording that falls behind says so while it records, with the frames a second it gets',
behindLine?.startsWith(`Recording "${slowTake.name}" can't keep up`) &&
got > 5 &&
got < 40 &&
slowTake.skipped > 0 &&
slowTake.dropped + slowTake.skipped > slowTake.frames / 2,
JSON.stringify({ behindLine, slowTake }),
);
await stop();
await waitUntil(`__gm.recording().takes[1]?.state === 'done'`, { label: 'the slow take is saved' });
const consoleLines = (await json('__gm.console.lines()')).filter((line) =>
line.text.startsWith('[recording]'),
);
const summary = consoleLines.find((line) => line.text.includes(`"${slowTake.name}" has `));
check(
'and says when it is saved how many frames it missed; the start is an info line, the rest warns',
summary?.level === 'warn' &&
/has \d+ of \d+ frames: \d+ missed \(\d+ %\), so it stutters/.test(summary.text) &&
consoleLines.some((line) => line.level === 'info' && line.text.includes(`"${NAME}" started`)) &&
consoleLines.some((line) => line.level === 'warn' && line.text.includes("can't keep up")),
JSON.stringify(consoleLines.slice(-8)),
);
// 67c. Ending the run saved the second take; both became video assets once the server had them.
await waitUntil(