import type { EventKind } from "@a4anthony/proctorkit-types"; import { type CaptureClock } from "../internal/capture-clock.js"; import type { PhotoCaptureTiming, RecordingChunkTiming } from "./media-capture-timing.js"; export interface ObserverEmitter { emit(kind: EventKind, payload?: Record, timestamp?: number): void; } export type WebcamErrorKind = "declined" | "unavailable" | "in-use" | "device-not-found"; export interface WebcamObserverConfig { /** * Specific camera to use when the SDK acquires the stream itself. * Omit to let the browser pick the default. Customers building their * own device picker pass the chosen deviceId here. Mutually * exclusive with `stream` — if both are provided, `stream` wins and * deviceId is ignored. */ deviceId?: string; /** * Hand in a pre-acquired MediaStream instead of letting the SDK * call `getUserMedia`. Use this when the customer's app already * holds the camera (eg. for a video interview question that * coexists with proctoring). The SDK becomes a passive reader: * it draws frames from your stream for the photo loop but does NOT * stop the tracks on session end — your app owns the lifecycle. * * Trade-offs: * - One permission prompt instead of two. * - One camera light instead of two. * - No NotReadableError on Linux/iOS where double-acquire can fail. * - You must ensure the stream is live when you pass it in (a * stopped stream throws WebcamUnavailableError immediately). */ stream?: MediaStream; /** * Periodic photo capture. Pass `true` for defaults, or a config * object to tune. Default: off (the observer just starts the * camera and emits webcam.started; without photos there is no * forensic signal). */ photos?: boolean | WebcamPhotoConfig; /** * Continuous video recording of the candidate's webcam feed. Pass * `true` for defaults (10s VP9 WebM chunks at 500 kbps), or an * object to tune. Default: off. * * Coexists with `photos` — both read from the same MediaStream, so * there's no second `getUserMedia` and no second camera light. * * Storage cost note for customers: continuous recording at 500 kbps * is roughly 225 MB per hour per session. Photos at the default * cadence are ~16 MB per hour. Turn this on deliberately. */ recording?: boolean | WebcamRecordingConfig; } export interface WebcamRecordingConfig { /** * MediaRecorder chunk duration in milliseconds. Smaller = more rows * in the DB but quicker recovery if a chunk upload fails. Default: * 10_000 (10s) — matches screen-share defaults. */ timesliceMs?: number; /** * Target video bitrate in bits per second. Default: 500_000 * (500 kbps) — small enough that 10s chunks are ~625KB. */ videoBitrate?: number; /** * Called for each chunk the recorder produces. Customer-side hook * for visibility / logging. The SDK still uploads the chunk via * its internal pipeline either way. */ onChunkReady?: (chunkNumber: number, blob: Blob, timing: RecordingChunkTiming) => void; } export interface WebcamPhotoConfig { /** * Minimum / maximum seconds between captures. The actual delay * is uniformly random in [min, max] so a candidate can't time * around a fixed cadence. Defaults: 20s / 60s. */ minIntervalSeconds?: number; maxIntervalSeconds?: number; /** * JPEG quality 0..1 used when encoding the canvas. Default: 0.7 * (good balance of size vs. clarity for face identification). */ jpegQuality?: number; /** * Called when a photo is captured. Receives the photo number * (1-indexed) and a Blob. Customer-side hook for visibility/ * logging. SDK still uploads via the internal uploader either way. */ onPhotoReady?: (photoNumber: number, blob: Blob, timing: PhotoCaptureTiming) => void; } /** * Thrown from {@link WebcamObserver.start} when the camera can't be * acquired. Mirrors {@link ScreenShareDeclinedError} so the client * can route both via the same callback shape. */ export declare class WebcamUnavailableError extends Error { readonly kind: WebcamErrorKind; constructor(kind: WebcamErrorKind, message?: string); } /** * Requests the candidate's webcam via `getUserMedia`, holds the stream * for the session, and optionally captures still photos at random * intervals. Mirrors {@link ScreenShareObserver} in shape so the same * lifecycle and error patterns apply. * * Photos are produced by drawing the live `