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:
1 parent
4d0a696327
commit
77365f17d3
9 files changed
+283
-17
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
elapsedText,
|
||||
fitPicture,
|
||||
FrameGate,
|
||||
FramePace,
|
||||
i420Layout,
|
||||
isKeyFrame,
|
||||
pictureTransform,
|
||||
@@ -165,6 +166,84 @@ describe('FrameGate', () => {
|
||||
it('takes about 60 a second from a 144 Hz screen', () => {
|
||||
expect(Math.abs(taken(60, 1000 / 144, 10) - 600)).toBeLessThanOrEqual(6);
|
||||
});
|
||||
|
||||
const gateAfter = (fps: number, gaps: number[], seconds: number) => {
|
||||
const gate = new FrameGate(fps);
|
||||
for (let i = 0, ms = 0; ms < seconds * 1000; ms += gaps[i++ % gaps.length]!) gate.take(ms);
|
||||
return gate;
|
||||
};
|
||||
|
||||
it('counts no frame skipped while the screen keeps up', () => {
|
||||
expect(gateAfter(60, [1000 / 60], 10).skipped).toBe(0);
|
||||
expect(gateAfter(30, [1000 / 60], 10).skipped).toBe(0);
|
||||
expect(gateAfter(60, [1000 / 144], 10).skipped).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('counts the frames a slow screen skips, as many as the recording misses', () => {
|
||||
// As in a 4K recording on the user's machine: 18 and 35 ms in turn, about 38 a second.
|
||||
const slow = gateAfter(60, [18, 35], 10);
|
||||
expect(slow.slots).toBeGreaterThanOrEqual(599);
|
||||
expect(slow.slots).toBeLessThanOrEqual(601);
|
||||
expect(slow.skipped).toBe(slow.slots - 378);
|
||||
// A 50 Hz screen leaves out one in six of 60, however evenly the frames come.
|
||||
const fifty = gateAfter(60, [20], 10);
|
||||
expect(Math.abs(fifty.skipped - 100)).toBeLessThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FramePace', () => {
|
||||
/** What `behind` said, frame by frame, of a recording that misses `missedPerSecond`. */
|
||||
const said = (fps: number, missedPerSecond: (s: number) => number, seconds: number) => {
|
||||
const pace = new FramePace(fps);
|
||||
const told: { at: number; got: number }[] = [];
|
||||
let missed = 0;
|
||||
for (let ms = 0; ms <= seconds * 1000; ms += 1000 / fps) {
|
||||
missed += missedPerSecond(ms / 1000) / fps;
|
||||
const got = pace.behind(ms, Math.round(missed));
|
||||
if (got !== null) told.push({ at: Math.round(ms), got });
|
||||
}
|
||||
return told;
|
||||
};
|
||||
|
||||
it('says once that a recording falls behind, with the frames a second it gets', () => {
|
||||
const told = said(60, () => 22, 20);
|
||||
expect(told).toHaveLength(1);
|
||||
expect(told[0]!.got).toBe(38);
|
||||
// It has settled for a second and then judged two.
|
||||
expect(told[0]!.at).toBeGreaterThanOrEqual(3000);
|
||||
expect(told[0]!.at).toBeLessThan(3100);
|
||||
});
|
||||
|
||||
it('lets a stall while it starts and a few frames missed pass', () => {
|
||||
// Half a second lost at the start, then 5 % missed: never more than a tenth over a span.
|
||||
expect(said(60, (s) => (s < 0.5 ? 60 : 3), 20)).toEqual([]);
|
||||
});
|
||||
|
||||
it('says at the end that a recording stutters only when it missed frames after it settled', () => {
|
||||
const after = (missedPerSecond: (s: number) => number, seconds: number) => {
|
||||
const pace = new FramePace(60);
|
||||
let missed = 0;
|
||||
for (let ms = 0; ms <= seconds * 1000; ms += 1000 / 60) {
|
||||
missed += missedPerSecond(ms / 1000) / 60;
|
||||
pace.behind(ms, Math.round(missed));
|
||||
}
|
||||
return pace.stutters;
|
||||
};
|
||||
// A stall while it starts, in a short recording too: e2e 67 missed 3 of 37 frames so.
|
||||
expect(after((s) => (s < 0.3 ? 60 : 0), 1.2)).toBe(false);
|
||||
expect(after((s) => (s < 0.5 ? 60 : 1), 20)).toBe(false);
|
||||
expect(after(() => 22, 13.8)).toBe(true);
|
||||
expect(after(() => 6, 20)).toBe(true);
|
||||
// Shorter than its first second: nothing to judge.
|
||||
expect(after(() => 30, 0.9)).toBe(false);
|
||||
});
|
||||
|
||||
it('says it when a recording falls behind only later', () => {
|
||||
const told = said(30, (s) => (s < 7 ? 0 : 12), 20);
|
||||
expect(told).toHaveLength(1);
|
||||
expect(told[0]!.got).toBe(18);
|
||||
expect(told[0]!.at).toBeGreaterThan(7000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pictureTransform', () => {
|
||||
|
||||
Reference in new issue
Block a user