import type { ClipDropCode } from "./clip-error-codes.js"; import { type AudioClipVolumeAnalysis } from "./audio-level-analysis.js"; export type { AudioClipVolumeAnalysis, AudioVolumeClassification, } from "./audio-level-analysis.js"; /** * Ad-hoc video clip recorder. Unlike screen-share (continuous chunks) * or webcam photos (passive sampling), this records a single blob * triggered by an explicit customer action — eg. answering a video * question — and uploads the whole file once on stop. * * Stream sourcing strategy: * 1. If the customer hands us a stream, use it directly. We don't own * the lifecycle; we just read frames. * 2. Otherwise, if the webcam observer is running, reuse its stream. * 3. Otherwise, call getUserMedia ourselves and own the stream. * * Why this matters: webcam photos + a video clip should share a single * camera handle so the candidate doesn't see two permission prompts or * two camera lights. Same principle as Option C in the webcam * observer. * * Auto-stop safety: a maxDurationMs cap (default 120s) prevents * runaway recordings if the customer forgets to call stop(). */ export interface VideoClipRecorderOptions { /** * Discriminator: `"video"` records video (+ optional audio * track) and uploads to /video-clips. `"audio"` records audio * only and uploads to /audio-clips, with a default MIME type * of `audio/webm;codecs=opus`. Both paths share this class * because the MediaRecorder pipeline is identical -- only the * track-type check + the MIME default + the upload URL differ. * Default: "video" (backward-compatible). */ kind?: "video" | "audio"; /** * sessionId is needed for the upload URL. */ sessionId: string; /** * Events ingest URL — the recorder derives the origin and joins the * clip path under it. */ ingestUrl: string; /** Public app key for org routing. */ appId?: string; /** * Per-device fingerprint. Forwarded to the upload as * `x-fingerprint-id` so the server can attribute the clip * to the device that recorded it. Optional. */ fingerprintId?: string; /** * 1-based clip number. The SDK assigns this sequentially across * recordings in the same session. Customers don't pick it. */ clipNumber: number; /** * The MediaStream to record from. The recorder will not * `addTrack`/`removeTrack` on this stream — doing so transitions * MediaRecorder to inactive. Callers that need to combine borrowed * + freshly-acquired tracks must build a fresh stream themselves * and pass it in. See `recordVideoClip` in `index.ts` for the * canonical pattern. */ stream: MediaStream; /** * Tracks the recorder will `.stop()` on finalise. The recorder * never stops other tracks on the stream, even when they're part * of `stream.getTracks()`. This lets callers mix borrowed and * owned tracks safely: * - Path 1 (customer-provided stream): pass [] — customer owns it. * - Path 2 (webcam observer's video + on-demand mic): pass [mic] * only. The webcam observer owns the video track lifetime. * - Path 3 (fresh getUserMedia): pass all tracks of the stream. */ tracksToStop: MediaStreamTrack[]; /** * Video MIME type to record. Default: video/webm with VP9 if the * browser supports it. */ mimeType?: string; /** * Bitrate hint for the video track. Default 1 Mbps — higher than * the continuous screen-share default because clips are short and * usually contain faces / voice rather than static screens. */ videoBitrate?: number; /** * Hard cap on recording duration (ms). Auto-stops at this point so * a forgotten stop() doesn't leak a multi-hour blob. Default: 120000. */ maxDurationMs?: number; /** Fired when the clip uploads successfully. */ onUploaded?: (clipNumber: number, byteSize: number, durationMs: number, volumeAnalysis?: AudioClipVolumeAnalysis) => void; /** * Fired when the clip can't be uploaded (network, 4xx, 5xx) or * recorded nothing. `code` is the stable classification; `reason` is * the human-readable detail. */ onDropped?: (clipNumber: number, reason: string, volumeAnalysis?: AudioClipVolumeAnalysis, code?: ClipDropCode) => void; /** * Best-effort sink for the finalized clip blob, fired in parallel * with the upload to the proctoring server. Lets a caller mirror the * clip to their own backend WITHOUT affecting the canonical outcome: * the server upload remains the source of truth, so this rejecting * never turns the clip into a drop. The recorder neither awaits this * before resolving stop() nor surfaces its rejection — the caller is * responsible for observing failure (we re-throw into it so it can). */ onClipData?: (clip: { blob: Blob; mimeType: string; durationMs: number; clipNumber: number; }) => Promise; } /** * Public handle returned to the customer. The only operation is * stop(); everything else is internal. */ export interface VideoClipHandle { /** 1-based identifier for this clip within the session. */ readonly clipNumber: number; /** Stops recording immediately and uploads the resulting blob. */ stop(): Promise; } export declare class VideoClipRecorder implements VideoClipHandle { readonly clipNumber: number; private readonly opts; private recorder; private blobs; private startedAt; private autoStopTimer; private audioLevelAnalyzer; private stopped; private finishPromise; constructor(opts: VideoClipRecorderOptions); /** * The stream being recorded while this clip is active, else null (once * stopped). The runtime checkpoint reads it to piggyback mic/camera * liveness onto an in-flight clip — no separate getUserMedia. */ activeStream(): MediaStream | null; /** * Begin recording. Called once by the client; do not call again. * Throws if MediaRecorder is unavailable or the stream has no * video tracks. */ start(): void; stop(): Promise; private finalize; private upload; private stopAudioLevelAnalyzer; } //# sourceMappingURL=video-clip-recorder.d.ts.map