import type { EventKind } from "@a4anthony/proctorkit-types"; import { type CaptureClock } from "../internal/capture-clock.js"; import type { RecordingChunkTiming } from "./media-capture-timing.js"; export interface ObserverEmitter { emit(kind: EventKind, payload?: Record, timestamp?: number): void; } export interface ScreenShareObserverConfig { /** * Pre-acquired display stream. Use this for staged flows that need * to open the browser picker before the runtime SDK starts, then * hand the granted stream to the SDK for recording. * * When omitted, the observer calls getDisplayMedia() itself. */ stream?: MediaStream; /** * Whether stop() should release a stream supplied via `stream`. * Defaults to false so customer-owned streams are not killed unless * the caller explicitly transfers lifecycle ownership to the SDK. */ stopExternalStreamOnStop?: boolean; /** * Reject anything other than `displaySurface === "monitor"`. When the * candidate picks a window or browser tab the SDK stops the stream * immediately, emits `screen-share.wrong-surface`, and the start() * promise rejects with {@link ScreenShareDeclinedError}. Default: true. * * Set to false to accept any shared surface (the customer can filter * on the `surface` field of `screen-share.started` themselves). */ enforceEntireScreen?: boolean; /** * MediaRecorder chunk duration in milliseconds. The recorder emits one * Blob per timeslice. Smaller = more rows in the DB but quicker * recovery if a chunk upload fails. Default: 10_000 (10s). */ timesliceMs?: number; /** * Target bitrate for the video track in bits per second. Default: * 500_000 (500 kbps) — small enough that 10s chunks are ~625KB. */ videoBitrate?: number; /** * Acquire and validate the browser screen-share stream, but do not * attach MediaRecorder until startRecording() is called. Useful for * flows that want the candidate to grant screen-share first, then * start actual recording only after the test enters fullscreen. * Default: false. */ deferRecording?: boolean; /** * Called with each chunk the recorder produces. In Phase 1 this is * where the SDK demo logs metadata; Phase 2 hooks the worker upload * channel into this. Receives the chunk number (1-indexed) and the * Blob itself. */ onChunkReady?: (chunkNumber: number, blob: Blob, timing: RecordingChunkTiming) => void; } export type ScreenShareErrorKind = "activation-required" | "permission-denied" | "unsupported" | "wrong-surface" | "track-ended"; export interface RequestScreenShareOptions { /** Reject a window or browser-tab selection. Default: true. */ enforceEntireScreen?: boolean; } /** * Thrown when screen sharing cannot start. The `kind` identifies activation, * permission, browser support, selected surface, or track-lifecycle failures. */ export declare class ScreenShareDeclinedError extends Error { readonly kind: ScreenShareErrorKind; constructor(kind: ScreenShareErrorKind, message?: string, options?: { cause?: unknown; }); } /** * Opens the browser screen picker immediately. Call this as the first action * inside the candidate's click handler, then pass the returned stream through * `observers.screenShare.stream`. */ export declare function requestScreenShare(options?: RequestScreenShareOptions): Promise; /** * Accepts a pre-acquired screen stream or requests one through * `getDisplayMedia`, enforces the chosen surface, and records it into chunks * via `MediaRecorder`. Prefer the exported `requestScreenShare()` helper so * acquisition happens directly from the candidate's action. * * The observer DOES NOT paint any UI — the browser's native picker is * the only user-facing element. Customers own the surrounding UX: * explainer copy before the click, retry/decline handling after. */ export declare class ScreenShareObserver { private readonly emitter; private readonly clock; private readonly config; private stream; private recorder; private recordingChunkStartedAt; private chunkCount; private stopped; private stopPromise; private trackEndedListener; constructor(emitter: ObserverEmitter, config: ScreenShareObserverConfig | boolean | undefined, clock?: CaptureClock); /** * Prompts the browser picker and validates the selected surface. * Unless `deferRecording` is enabled, this also starts recording. * Resolves once the stream is up (and recording has started in the * default path). Rejects with * {@link ScreenShareDeclinedError} on decline or wrong surface. * * When no stream is supplied, this must run during transient user * activation. `ProctoringClient` preserves that activation by opening its * compatibility picker synchronously before worker startup. */ start(): Promise; /** * Attach MediaRecorder to the already-active screen-share stream. * Returns false when the stream is unavailable or this browser cannot * record it. Idempotent once recording is active. */ startRecording(): boolean; /** * Ask MediaRecorder to emit its current partial segment while continuing * to record. Used on lifecycle transitions where the page may disappear. */ flushRecordingData(): void; /** Capture the current shared-screen frame without interrupting recording. */ captureEvidenceFrame(): Promise; /** * Stops the recorder and the underlying tracks. Idempotent. `reason` * is purely informational — recording always terminates the same way. * * Awaits the recorder's final `dataavailable` + `stop` events before * resolving. MediaRecorder's spec guarantees a final dataavailable * fires after `stop()` is called, carrying whatever frames were * buffered since the last timeslice tick. Without this await, a * candidate hitting Stop session 4 seconds into a 10-second chunk * would lose those 4 seconds — the JS context would tear down before * the chunk reached the uploader. The cost is a one-tick wait on * end-of-session; the win is that mid-chunk stops don't drop the * tail. */ stop(reason?: "manual" | "track-ended"): Promise; private stopInternal; private startRecorder; } //# sourceMappingURL=screen-share-observer.d.ts.map