shots record (web): frames at device pixels through Page.captureScreenshot (clip scale = dpr, long side capped at 1920, 3 requests in flight, midpoint timestamps into the ffmpeg concat); Chromium's screencast stayed at CSS size. Warnings for frames smaller than asked and under 20 frames/s; record-web browser test, pitfall 85

This commit is contained in:
CRTkafa 2026-10-21 03:59:39 +07:00
parent 2d7cf6ec2b
commit dda21d2570
10 changed files with 144 additions and 29 deletions

View file

@ -207,7 +207,7 @@ export const trimArgs = (inp: string, out: string, start: number, seconds: numbe
export interface ScreencastFrame { file: string; t: number }
/** ffmpeg concat-demuxer list for screencast frames (Chromium sends a frame only when the page changes): each frame lasts
/** ffmpeg concat-demuxer list for timestamped frames (`t` in seconds, frames come at an uneven rate): each frame lasts
* until the next one; the last one fills up to `seconds`. Pure. */
export function concatList(frames: ScreencastFrame[], seconds: number): string {
const t0 = frames[0]?.t ?? 0;
@ -226,11 +226,25 @@ export const concatArgs = (list: string, out: string, seconds: number, fps = 30)
['-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', list, '-t', seconds.toFixed(3),
'-vf', `fps=${fps},scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p`, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '16', '-an', '-movflags', '+faststart', out];
/** Screencast size for a frame: the frame at 2x (capped at 1600 px on the long side), even numbers. */
export function recordSize(d: { w: number; h: number }): { width: number; height: number } {
const k = Math.min(2, 1600 / Math.max(d.w, d.h));
const even = (n: number) => Math.max(2, Math.round(n * k / 2) * 2);
return { width: even(d.w), height: even(d.h) };
/** Long side cap of a web recording: the tallest store video (886 x 1920 iPhone preview, 1080 x 1920 Play promo), so
* `shots video` only scales down; a 3x phone (2706 px) or a 13" iPad (2752 px) would cost capture speed for pixels
* every output throws away. */
export const RECORD_MAX_SIDE = 1920;
/** Web capture for a frame: CDP `Page.captureScreenshot` with clip = the viewport in CSS px and `scale` = the frame's
* dpr (capped at RECORD_MAX_SIDE on the long side). Chromium returns CSS size x clip.scale whatever the context's
* deviceScaleFactor (checked on Chromium 153); its screencast stays at CSS size (maxWidth only shrinks), which is why
* the recorder does not use it. width / height = the expected image size. Pure. */
export function recordSize(d: { w: number; h: number; dpr?: number }): { width: number; height: number; scale: number } {
const scale = Math.min(d.dpr ?? 2, RECORD_MAX_SIDE / Math.max(d.w, d.h));
return { width: Math.round(d.w * scale), height: Math.round(d.h * scale), scale };
}
/** Guardrail: a first frame smaller than requested means the browser ignored the capture scale (the clip would be an
* upscale in `shots video`). Message or null. Pure. */
export function captureShortfall(got: { w: number; h: number }, want: { width: number; height: number }): string | null {
if (got.w >= want.width - 2 && got.h >= want.height - 2) return null;
return `frames are ${got.w}x${got.h}, asked for ${want.width}x${want.height}: this browser ignores the capture scale, so the clip is upscaled later (set STOREBOUND_CHROME to a current Chromium or record with --source folder)`;
}
// ---- frames ----