// fallow-ignore-file complexity code-duplication /** * Frame Capture Service * * Uses Puppeteer to capture frames from any web page implementing the * window.__hf seek protocol. Navigates to a file server URL, waits for * the page to expose window.__hf, then captures frames deterministically * via Chrome's BeginFrame API or Page.captureScreenshot fallback. */ import { type Browser, type Page, type Viewport, type ConsoleMessage } from "puppeteer-core"; import { existsSync, mkdirSync, writeFileSync } from "fs"; import { join } from "path"; import { quantizeTimeToFrame, fpsToNumber } from "@hyperframes/core"; // ── Extracted modules ─────────────────────────────────────────────────────── import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, resolveHeadlessShellPath, type CaptureMode, } from "./browserManager.js"; import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js"; import { detectSwiftShader, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, } from "./drawElementService.js"; import { initThreeDProjection, detectCssEffectRisk } from "./threeDProjection.js"; import { DEFAULT_CONFIG, type EngineConfig } from "../config.js"; import type { CaptureOptions, CaptureVideoMetadataHint, CaptureResult, CaptureBufferResult, CapturePerfSummary, } from "../types.js"; export type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSummary }; /** Called after seeking, before screenshot. Use for video frame injection or other pre-capture work. */ export type BeforeCaptureHook = (page: Page, time: number) => Promise; export interface CaptureSession { browser: Browser; page: Page; options: CaptureOptions; serverUrl: string; outputDir: string; onBeforeCapture: BeforeCaptureHook | null; isInitialized: boolean; /** * Static-frame dedup (default-on; opt out with `HF_STATIC_DEDUP=false`): indices of frames byte-identical * to their predecessor (no GSAP tween / clip cut active in either), predicted from * window.__timelines and empirically anchor-verified. These reuse `lastFrameBuffer` * instead of re-seeking + re-screenshotting. Undefined when disabled or ineligible. */ staticFrames?: Set; /** Last non-deduped frame buffer, reused for every `staticFrames` index in its run. */ lastFrameBuffer?: Buffer; /** Count of frames served from a reused buffer (dedup telemetry). */ staticDedupCount?: number; // ── Static-dedup observability (set by armStaticDedup; surfaced via // getCapturePerfSummary → RenderPerfSummary → the render_complete event) ── // NOTE: `armed` and `predicted` are NOT stored — they derive from // `staticFrames` (armed ⟺ non-empty set; predicted === size) in // getCapturePerfSummary, so they can't desync from the actual reuse set. /** Dedup was enabled for this render (default-on; opt out with `HF_STATIC_DEDUP=false`). */ staticDedupEnabled?: boolean; /** * Short machine code for WHY dedup did not arm, for a low-cardinality breakdown. * One of: `capture_mode` | `video_injection` | `page_composite` | * `ineligible` | `verification_failed` | `verification_budget`. Undefined when armed or disabled. */ staticDedupSkipReason?: string; // Tracks whether the page/browser handles have already been released by // closeCaptureSession. Used to make closeCaptureSession idempotent under // browser-pool semantics (see the function body for the full invariant). pageReleased?: boolean; browserReleased?: boolean; browserConsoleBuffer: string[]; initTelemetry?: { initDurationMs: number; tweenCount: number; }; capturePerf: { frames: number; seekMs: number; beforeCaptureMs: number; screenshotMs: number; totalMs: number; /** Per-frame capture durations (batch frames get the batch mean). Basis for * the warmup-robust p50 in the perf summary. */ frameMs: number[]; }; captureMode: CaptureMode; /** * Browser LAUNCH mode, immutable after createCaptureSession. `captureMode` * is reassigned by initializeSession (e.g. to "drawelement"), so callers * that need to know whether this browser actually drives BeginFrame (the * SwiftShader liveness probe) read this field instead. */ launchCaptureMode: CaptureMode; // BeginFrame state beginFrameTimeTicks: number; beginFrameIntervalMs: number; beginFrameHasDamageCount: number; beginFrameNoDamageCount: number; /** Optional producer config — when set, overrides module-level env var constants. */ config?: Partial; /** True if running on SwiftShader (detected at init). Undefined before init. */ isSwiftShader?: boolean; /** drawElementImage canvas was injected and is ready for capture. */ drawElementReady?: boolean; /** * Worker-encode pipeline is active for this session. Set by * `initDrawElementOrTransparentBackground` when `enableDrawElementWorkerEncode` * is true and capture mode resolved to "drawelement". */ workerEncodeEnabled?: boolean; /** * Frame indices that must be captured via screenshot rather than drawElement. * Populated at init by the clip-cut boundary predictor (Lim 6): frames where * the outgoing clip is dropped a frame before the incoming clip's paint record * is ready → black frame. Controlled by `HF_FAST_CAPTURE_BOUNDARY_SS=false`. * Empty/undefined when the predictor produces no frames. */ clipBoundaryFrames?: Set; /** Rolling drawElement frame byte-sizes (last ~60), for silent-blank-drop detection: * drawElement intermittently returns an anomalously small (blank) frame with no * throw; a frame far below the running median is re-captured via screenshot. */ deFrameSizes?: number[]; /** Last non-deduped encode result, reused for a static frame on the drawElement * worker-encode path (mirrors `lastFrameBuffer` on the screenshot path). Only set * when static-frame dedup is armed on the drawElement path. */ lastEncodeResult?: Promise; /** Per-render self-verification ground truth (ungated-release safety net): * K screenshot frames captured at init BEFORE the drawElement canvas is * injected (the only window where a page screenshot shows the live DOM, not * the capture canvas's stale bitmap). The producer drain compares the DE * frame at each index against these; a breach aborts the render with * DrawElementVerificationError and the orchestrator re-renders via the * screenshot path. */ deVerifyFrames?: Map; /** Low-cardinality init-gate reason when drawElement routed to baseline (telemetry). */ deGateReason?: string; /** Wall-clock ms spent capturing self-verification ground truth at init (telemetry). */ deVerifyInitMs?: number; /** Count of per-frame "No cached paint record" screenshot fallbacks (telemetry). */ deNcprFallbacks?: number; /** * drawElement init passed every gate but stopped before verification + * canvas injection: the session has no video-frame injector yet (probe * sessions initialize before extraction) and the comp has