/** A completed recording segment surfaced by the backend (a rolled or finalized-on-stop MP4 file). */ export interface CaptureSegment { /** Read the segment's bytes as base64 (for the artifact upload). */ read: () => Promise; /** Delete the segment file after it's uploaded (bulk bytes stay off the snapshot + guest disk). */ discard: () => Promise; /** Wall-clock bounds of the captured span (best-effort, the backend's clock). */ startedAtMs: number; endedAtMs: number; } /** A running capture: emits completed segments and exposes the latest live frame. */ export interface CaptureSession { /** Register the completed-segment callback (fires per rolled segment + the final one on stop). */ onSegment: (cb: (segment: CaptureSegment) => void) => void; /** The most recent live frame (base64 JPEG), or null before the first frame is written. */ latestFrame: () => Promise; /** A small 16:10 JPEG for durable run-list thumbnails, or null before one is available. */ latestThumbnail: () => Promise; /** Stop ffmpeg and finalize + emit the last in-flight segment. The finalized thumbnail remains * readable after this resolves so an ultrashort run can persist its first complete frame. */ stop: () => Promise; } /** The guest-coupled half: starts ffmpeg on the display. Production impl in screen_capture_backend.ts. */ export interface CaptureBackend { /** Pixel dimensions of the captured display — stamped into segment metadata. */ readonly width: number; readonly height: number; /** Pixel dimensions of the separately encoded, bandwidth-bounded thumbnail frame. */ readonly thumbnailWidth: number; readonly thumbnailHeight: number; /** Interval (ms) between live-frame pushes while a viewer is attached. */ readonly liveFrameIntervalMs: number; /** How often (ms) to poll the broker for whether a viewer is attached. */ readonly wantedPollIntervalMs: number; /** Longest a viewer goes without a frame while the screen is unchanged — the dedupe's release * valve (see the push loop). Must stay under any proxy idle timeout on the live-view stream. */ readonly liveKeepaliveMs: number; start: () => Promise; } /** Upload a recording segment as a run artifact (the broker holds the S3 credential). */ export type SegmentArtifactWriter = (name: string, contentType: string, base64: string, metadata: Record) => Promise<{ id: string; }>; export interface ScreenCaptureDeps { backend: CaptureBackend; /** Store a completed segment as a `recording-segment` artifact. */ writeArtifact: SegmentArtifactWriter; /** Push encoded live frames to the broker's live-view channel. */ publishLiveFrames: (frames: string[]) => Promise; /** Whether a browser is currently watching (gates the live push loop). */ liveViewWanted: () => Promise; /** Injected clock (deterministic in tests). */ now: () => number; /** Bounds `stopAndFlush()` so a hung upload can't stall a suspend indefinitely. Default 20s. */ flushTimeoutMs?: number; } export declare class ScreenCapture { private readonly deps; private session; /** An in-flight `start()` — post-wake capture is kicked off fire-and-forget, so `stopAndFlush()` must * be able to await a spawn that hasn't landed yet. */ private starting; /** Monotonic across the whole run, so segments stay contiguous across suspend/resume epochs. */ private segmentIndex; /** Serializes segment uploads so `stopAndFlush()` can await the whole in-flight tail. */ private uploadTail; /** Serializes thumbnail reads so an initial timer racing a flush cannot land after the final frame. */ private thumbnailCaptureTail; private liveLoop; private initialThumbnailTimer; /** The initial thumbnail is run-scoped, so a suspend/resume does not create another copy. */ private initialThumbnailStored; constructor(deps: ScreenCaptureDeps); /** Begin capturing. Idempotent-safe: a second call while running — or while a first one is still * spawning — joins that start instead of racing a second recorder onto the display. */ start(): Promise; /** Post-wake: start a fresh capture (new segment files), keeping the monotonic segment index. */ startFresh(): Promise; /** * Pre-freeze / terminal: stop capture, finalize + upload the in-flight segment, and drain the upload * tail — so the recorder never spans a snapshot and the last committed segment is always playable. * Bounded by `flushTimeoutMs` so a stuck upload delays (never blocks) the suspend. Safe to call with * no active session. */ stopAndFlush(): Promise; /** Chain a segment upload onto the tail (serialized, best-effort). */ private enqueueSegment; private uploadSegment; /** Store one small representative desktop frame. Thumbnails are observability, so every failure * is best-effort and joins the same serialized upload tail as recording segments. */ private enqueueThumbnail; private readAndEnqueueThumbnail; /** Give the desktop a moment to become representative, then make running rows visual without * waiting for the first recording roll or terminal flush. */ private scheduleInitialThumbnail; /** Lazy live-view push: poll whether a viewer is attached; while attached, push the latest frame at * the backend's cadence. Fully best-effort — a failed poll/push never disturbs the run. */ private startLiveLoop; }