/** * ClipCaptureLoop — frame source for local clip/video recording. * * Primary path (Playwright 1.59+): the official `page.screencast` API streams * JPEG frames through Chromium's screencast pipeline via `onFrame`. Frames * arrive on a more regular cadence than the previous pull-based * `Page.captureScreenshot` loop, and start/stop is precise — frames are only * delivered between `screencast.start()` and `screencast.stop()`, so an * off-camera setup reload before recording is never filmed. * * Fallback path: if `page.screencast` is missing or fails to start (older * Playwright, headless quirk), the recorder falls back to the legacy CDP * `Page.captureScreenshot` loop (`optimizeForSpeed` + `fromSurface`). * * Either way, frames are buffered in memory (raw JPEG Buffers) and flushed to * disk in parallel at `stop()`. `assembleMp4FromFrames` reads the per-frame * timestamps and encodes VFR via the concat demuxer so playback matches the * real capture cadence — essential when the compositor is CPU-bound on heavy * React UIs (frames arrive in bursts + gaps, not uniformly). */ import type { Page } from 'playwright'; export interface ClipCaptureLoopOptions { /** Playwright page used to derive the CDP session. */ page: Page; /** Absolute path to an existing directory where frames will be written. */ framesDir: string; /** * JPEG quality (0-100). Default 80. On 2880×1800, q=80 sustains ~40 FPS on * a modern Mac (vs 30 FPS at q=90) — the quality drop is invisible on screen * content (high-contrast text, flat colors) but unlocks a 33% fluidity gain. */ jpegQuality?: number; /** * Maximum capture attempts per second. The loop also yields after every frame * so Playwright input and page JS can make progress while a clip is recording. */ targetFps?: number; /** Minimum rest after each CDP screenshot, even when capture is already slow. */ minRestMs?: number; } export interface ClipCaptureLoopResult { framesDir: string; frameCount: number; /** Configured capture attempt ceiling. Actual FPS may be lower when CDP is slow. */ targetFps: number; /** Minimum idle time yielded after each CDP screenshot. */ minRestMs: number; /** (frameCount - 1) * 1000 / (lastTs - firstTs); 0 if < 2 frames. */ measuredFps: number; actualDurationMs: number; /** Delay between start() returning and the first frame being written. */ trimStartMs: number; /** * Wall-clock offset in milliseconds from the first frame, one entry per * captured frame. Used by assembleMp4FromFrames to preserve real timing * (VFR) instead of encoding at a uniform CFR — critical when capture * throughput varies (heavy page renders pause the compositor while the * loop keeps trying, producing bursts and gaps). */ frameOffsetsMs: number[]; /** CDP Page.captureScreenshot wall time in milliseconds (empty on screencast path). */ captureTimingMs: { p50: number; p95: number; max: number; }; /** True when frames came from the official page.screencast API; false on the CDP fallback. */ usedScreencast: boolean; } export declare class ClipCaptureLoop { private readonly page; private readonly framesDir; private readonly jpegQuality; private readonly targetFps; private readonly targetFrameIntervalMs; private readonly minRestMs; private cdp; private running; private loopPromise; private screencastActive; private usedScreencast; private frames; private frameTimestamps; private frameCaptureDurationsMs; private startedAt; private firstFrameAt; private lastFrameAt; constructor(opts: ClipCaptureLoopOptions); start(): Promise; stop(): Promise; /** * Common frame sink for both the screencast and CDP paths: stamps wall-clock * timing for VFR and buffers the raw JPEG in memory. */ private handleFrame; /** * Device-pixel dimensions of the current surface, so screencast frames keep * the HiDPI fidelity the CDP `fromSurface` path produced. Returns undefined * (let screencast pick its default) if the page can't be evaluated. */ private resolveCaptureSize; private loop; }