import type { CandidateIdentity, EventKind, MainToWorkerMessage, PartialEvent, ProctoringConfig, SessionEvent, WorkerToMainMessage } from "@a4anthony/proctorkit-types"; import { type DomObserversConfig } from "./observers/dom-observers.js"; import { type ScreenShareErrorKind } from "./observers/screen-share-observer.js"; import { type WebcamErrorKind } from "./observers/webcam-observer.js"; import { type AudioClipVolumeAnalysis, type VideoClipHandle } from "./video-clips/video-clip-recorder.js"; import type { ClipDropCode } from "./video-clips/clip-error-codes.js"; import { type TextAnswerHandle } from "./text-answer/text-answer-recorder.js"; import { type AudioFilePlaybackHandle, type AudioFilePlaybackOptions } from "./audio-playback/audio-file-player.js"; import { type VideoFilePlaybackHandle, type VideoFilePlaybackOptions } from "./video-playback/video-file-player.js"; import { type FingerprintResult } from "./fingerprint/index.js"; import { type CheckpointConfig, type CheckpointOptions, type CheckpointResult } from "./checkpoint/index.js"; export { AssessmentReplayController, type AssessmentReplayControllerOptions, type AssessmentReplayEndReason, } from "./assessment-replay/assessment-replay-controller.js"; export type { AssessmentReplayPhase } from "./assessment-replay/assessment-replay-recorder.js"; export type { CandidateIdentity, DeliveryReadinessErrorCode, EventKind, ProctoringConfig, SessionEvent, WorkerToMainMessage, } from "@a4anthony/proctorkit-types"; export { DeliveryReadinessError } from "./delivery-readiness.js"; export { measureRecordingDeliveryReadiness } from "./recording-delivery-readiness.js"; export type { RecordingDeliveryMeasurement, RecordingDeliveryMeasurementConfig, RecordingReadinessConfig, } from "./recording-delivery-readiness.js"; export type { DomObserversConfig } from "./observers/dom-observers.js"; export type { RequestScreenShareOptions, ScreenShareErrorKind, ScreenShareObserverConfig, } from "./observers/screen-share-observer.js"; export type { WebcamObserverConfig, WebcamErrorKind } from "./observers/webcam-observer.js"; export { requestScreenShare, ScreenShareDeclinedError } from "./observers/screen-share-observer.js"; export { WebcamUnavailableError } from "./observers/webcam-observer.js"; export { enumerateMediaDevices } from "./media-devices.js"; export type { MediaDeviceInfoLite, MediaDeviceList, EnumerateOptions } from "./media-devices.js"; export type { CheckpointResult, CheckpointChange, CheckpointChangeKind, CheckpointDeviceKind, CheckpointOptions, CheckpointConfig, CheckpointTrigger, } from "./checkpoint/index.js"; export { isDevicePresentLoose } from "./checkpoint/index.js"; export type { DeviceRef, EnumeratedDevice } from "./checkpoint/index.js"; export { deviceVerdict, displayVerdict, fullscreenVerdict, screenShareVerdict, connectionVerdict, } from "./verification-core/index.js"; export type { DeviceFacts, DeviceVerdict, PermissionFact, PresenceFact, TrackFact, DisplayFacts, DisplayVerdict, DisplayFact, FullscreenFacts, FullscreenVerdict, ScreenShareVerdict, ConnectionFacts, ConnectionVerdict, } from "./verification-core/index.js"; export { computeDeviceFingerprint, hashFingerprint, getTabLockToken } from "./fingerprint/index.js"; export type { DeviceFingerprint, FingerprintResult } from "./fingerprint/index.js"; export type { AudioFilePlaybackHandle, AudioFilePlaybackOptions, } from "./audio-playback/audio-file-player.js"; export type { VideoFilePlaybackHandle, VideoFilePlaybackOptions, } from "./video-playback/video-file-player.js"; export type { MediaFilePlaybackHandle, MediaFilePlaybackOptions, MediaPlaybackKindPrefix, } from "./media-playback/media-file-player.js"; export type { AudioClipVolumeAnalysis, AudioVolumeClassification, } from "./video-clips/video-clip-recorder.js"; export type SdkTelemetryEventKind = "sdk.initialized" | "sdk.ready" | "sdk.error" | "sdk.upload.failed" | "sdk.media.failed" | "sdk.permission.denied" | "sdk.worker.failed" | "sdk.storage.fallback" | "sdk.recording.buffer-degraded" | "sdk.recording.buffer-evicted" | "sdk.stop.drain-timeout"; export type SdkTelemetryCode = "sdk.initialized" | "sdk.ready" | "sdk.init.failed" | "sdk.worker.init_failed" | "sdk.worker.ready_timeout" | "sdk.storage.memory_fallback" | "sdk.worker.message_upload_failed" | "runtime.screen_share.permission_denied" | "runtime.screen_share.activation_required" | "runtime.screen_share.unsupported" | "runtime.screen_share.track_ended" | "runtime.screen_share.wrong_surface" | "runtime.webcam.permission_denied" | "runtime.webcam.no_device" | "runtime.webcam.in_use" | "runtime.webcam.unavailable" | "runtime.media_recorder.unavailable" | "runtime.video_clip.permission_denied" | "runtime.video_clip.media_failed" | "runtime.audio_clip.permission_denied" | "runtime.audio_clip.media_failed" | "runtime.assessment_replay.unavailable" | "upload.events.failed" | "upload.media.chunk_dropped" | "upload.media.request_timeout" | "upload.media.complete_deferred" | "upload.media.clip_dropped" | "runtime.event_evidence.capture_failed" | "upload.event_evidence.failed" | "recording.durable-buffer.degraded" | "recording.durable-buffer.evicted" | "session.stop.drain_timeout"; export interface SdkTelemetryOptions { /** * Structured SDK health events are enabled by default. Set false * for customers that need to suppress all SDK diagnostics. */ enabled?: boolean; /** * Off by default. When true, the SDK may include a truncated stack * trace in sdk.* payloads. Keep false in production candidate flows * unless the customer explicitly opts into debug diagnostics. */ debug?: boolean; /** * @deprecated Ignored. Full page URLs are never collected because they can * contain credentials, candidate identifiers, and customer routing data. * Retained temporarily so existing integrations continue to typecheck. */ includePageUrl?: boolean; } /** * Options for constructing a {@link ProctoringClient}. * * The constructor auto-starts. There is no `start()` to await — the * client begins running on the same tick the constructor returns and * continues until `endSignal.abort()` is called. Page unloads only * flush pending events; the backend infers abandonment from missing * heartbeat/activity. * * Provide exactly one of `workerUrl` or `workerFactory`. In production * the `workerUrl` form is the only sane choice; `workerFactory` exists * so tests can drive the worker in-process. */ export interface ProctoringClientOptions extends ProctoringConfig { /** * Always-on DOM replay is enabled by default. Set this to `false` only when * a wrapper owns an AssessmentReplayController spanning preflight and runtime. */ assessmentReplay?: boolean; /** * URL of the bundled worker entry. Resolve at the host app build time * (eg. `new URL("@a4anthony/proctorkit-sdk/worker", import.meta.url)`) * so the bundler emits the worker as a separate asset. */ workerUrl?: string | URL; /** * Test seam: construct the worker yourself. Mutually exclusive with * `workerUrl`. */ workerFactory?: () => WorkerLike; /** * Identity of the candidate this session belongs to. Equivalent to * LogRocket's `identify()` but passed at construction so the very * first batch carries the right identity. Without this, the server * records anonymous events. */ candidate?: CandidateIdentity; /** * Complete resolved assessment policy applied to this SDK instance. Emitted * once as `session.policy` so post-session workers can enforce analysis * opt-outs even when the host uses runtime-only mode without preflight. */ policySnapshot?: Record; /** * AbortSignal that ends the session when aborted. The customer * controls the lifecycle externally — typically via an AbortController * stored alongside other "this exam is in progress" state. * * If omitted, there is no programmatic clean end. `pagehide` only * flushes queued events; abandonment is inferred by the backend from * missing heartbeat/activity after the stale timeout. */ endSignal?: AbortSignal; /** * Called when screen sharing cannot start. Distinguishes missing user * activation, denied permission, unsupported browsers, wrong-surface * selection, and an already-ended track. The session does not start. * * Without this callback, the failure is logged and the session * silently doesn't start. Set this to drive your retry UI. */ onScreenShareError?: (kind: ScreenShareErrorKind) => void; /** * Called when the webcam can't be acquired: candidate denied * permission (`"declined"`), no camera attached (`"device-not-found"`), * camera in use by another app (`"in-use"`), or otherwise unavailable. * The session does not start when this fires. * * Without this callback the failure is logged and the session * silently doesn't start. Set this to drive your retry UI. */ onWebcamError?: (kind: WebcamErrorKind) => void; /** * Called when init fails for reasons other than screen-share or * webcam: worker bundle fails to load, IndexedDB is denied (some * private-browsing modes), ready signal times out, etc. * * Without this callback, the failure surfaces as an unhandled promise * rejection in the browser console. */ onError?: (err: Error) => void; /** * Worker→main message stream. Use for upload health observability * (`queued`, `uploaded`, `upload-failed`, `dropped`, `init-failed`). */ onEvent?: (msg: WorkerToMainMessage) => void; /** * Main-thread session event stream before events are handed to the * worker queue. Use this for immediate UI reactions such as locking * an assessment when `screen-share.stopped` fires. */ onSessionEvent?: (event: PartialEvent) => void; /** * Privacy-safe SDK health telemetry. Enabled by default and emitted * as bounded `sdk.*` events through the normal ingest pipeline. It * never includes DOM, cookies, localStorage, media, screenshots, or * candidate page text. */ telemetry?: boolean | SdkTelemetryOptions; /** * Configure which built-in DOM/media observers run. The default DOM * baseline enables focus, visibility, fullscreen, network, pointer and * clipboard metadata; keyboard, screenshot, screen share, webcam and media * capture require explicit configuration. Pass `false` to disable all * observers, or a config object to override individual observers. */ observers?: DomObserversConfig | false; /** * Emit periodic `session.heartbeat` activity events. Default: true. * Set false only when the host's backend-owned policy explicitly * disables activity pings. */ heartbeat?: boolean; /** * Preferred audio output device id for URL-backed media prompts. * `playAudioFile()` and `playVideoFile()` pass this through to * HTMLMediaElement.setSinkId() when the browser supports output * routing. Per-call `sinkId` wins. */ audioOutputDeviceId?: string; /** * Preferred camera device id for runtime video capture. The webcam * observer takes its own `deviceId`; this covers the standalone * video-clip path (no webcam observer owns the stream) so the * recorded camera matches the candidate's preflight selection rather * than the OS default. */ videoDeviceId?: string; /** * Preferred microphone device id for runtime audio capture (video + * audio clips). When set, the SDK acquires this exact input instead * of the OS default, so recordings match the candidate's preflight * selection. */ audioInputDeviceId?: string; /** * Runtime integrity checkpoint. Re-verifies the live environment against * the state that passed preflight (permission status, selected-device * availability, external monitor, and — with `liveness` — camera/mic * capture liveness) and reports what drifted. * * Omit it and the feature is dormant except that `client.checkpoint()` * still works on demand. Provide `on: ["visibility", "interval"]` to have * the SDK run it on tab-return / a heartbeat and hand each result to * `onCheckpoint` / `onDrift`. It never pauses or fails the session — you * decide what to do with a result. */ checkpoint?: CheckpointConfig; } /** * Minimal Worker contract the SDK depends on. Subset of the DOM * `Worker` interface, so a real `new Worker(...)` satisfies it; tests * provide in-process shims that do too. */ export interface WorkerLike { postMessage(msg: MainToWorkerMessage): void; terminate(): void; addEventListener(type: "message", listener: (event: MessageEvent) => void): void; removeEventListener(type: "message", listener: (event: MessageEvent) => void): void; } /** * Host-app entry point. Auto-starts on construction: synchronously opens the * screen picker when configured without a supplied stream, spawns the * proctoring Worker, starts DOM observers, and registers a `pagehide` flush so * a closing tab or refresh still asks the worker to drain queued events. * * The customer never calls `.start()` or `.identify()` — both are folded * into the constructor options. Lifecycle ends cleanly via `end()` or * `endSignal.abort()`. Page unloads are not terminal events; stale * sessions are detected by missing heartbeat/activity. Use `.emit()` for * custom event signals. * * Construct inside a user-activation event handler (typically the * "Start session" button click) when `observers.screenShare` is on — * browsers require user activation for `getDisplayMedia`. */ /** One device selected during preflight, resolved to a human label. */ export interface SelectedDevice { /** deviceId, or null when the candidate never overrode the browser default. */ id: string | null; /** * Human label ("FaceTime HD Camera"), or "System default" when `id` is null, * or the raw id as a fallback when the device is no longer enumerable (e.g. * unplugged since preflight). */ label: string; } /** The camera/mic/speaker a session is capturing with. See `getSelectedDevices`. */ export interface SelectedDevices { camera: SelectedDevice; mic: SelectedDevice; speaker: SelectedDevice; } /** A device category that can be changed while a session is running. */ export type RuntimeDeviceKind = "microphone" | "camera" | "speaker"; /** Stable reason codes for candidate-facing device recovery UI. */ export type RuntimeDeviceSwitchErrorCode = "recording-active" | "switch-in-progress" | "session-unavailable" | "camera-external-stream" | "camera-unavailable" | "camera-rollback-failed"; /** Result of applying a new session device selection. */ export interface RuntimeDeviceSwitchResult { kind: RuntimeDeviceKind; previousDeviceId: string | null; device: SelectedDevice; /** Speakers can be remembered even when the browser cannot enforce routing. */ routing?: "applied" | "remembered"; } /** Typed failure returned by the runtime device-switching API. */ export declare class RuntimeDeviceSwitchError extends Error { readonly kind: RuntimeDeviceKind; readonly code: RuntimeDeviceSwitchErrorCode; constructor(kind: RuntimeDeviceKind, code: RuntimeDeviceSwitchErrorCode, message: string, options?: { cause?: unknown; }); } export declare class ProctoringClient { private worker; private readonly config; private readonly workerUrl; private readonly workerFactory; private readonly candidate; private readonly endSignal; private readonly onScreenShareError; private readonly onWebcamError; private readonly onError; private readonly onEvent; private readonly onSessionEvent; private readonly telemetry; private readonly heartbeatEnabled; private audioOutputDeviceId; private videoDeviceId; private audioInputDeviceId; private readonly telemetryDedupe; private readonly observersConfig; private observers; private screenShare; private readonly eventEvidenceCapture; private initialScreenShareGrant; private initialScreenShareStream; private screenShareChunkCount; /** * How recordings upload, decided by the SERVER, not the host. Fetched from * GET /public/upload-config during boot(), before any recording starts. * "direct" = browser → S3 multipart (so the completed object is one valid, * stitchable WebM); "post" = per-chunk relay to the server. Defaults to * "post" until the probe resolves; recordings don't begin until the host * calls startScreenShareRecording(), which is after boot completes, so the * resolved mode is always in place first. */ private recordingUploadMode; /** In-flight (or settled) upload-mode probe; awaited before recording. */ private uploadModeProbe; private chunkUploader; private webcam; private webcamPhotoUploader; private webcamRecordingUploader; /** Server-facing identities must survive webcam observer replacement. */ private webcamPhotoCount; private webcamRecordingChunkCount; private screenDurableStore; private webcamDurableStore; private screenSegmentStore; private webcamSegmentStore; /** Always-on, privacy-minimised assessment DOM replay. Independent of screen share. */ private assessmentReplay; private assessmentReplayFailureEmitted; private readonly assessmentReplayEnabled; private assessmentReplayStartTimer; /** * Sequence allocator for ad-hoc video clips recorded via * `recordVideoClip()`. 1-based, increments per call. */ private videoClipCounter; /** * Sequence allocator for ad-hoc audio clips recorded via * `recordAudioClip()`. Independent of `videoClipCounter` -- * the two streams have separate per-session counters because * they land in separate tables / dashboard tabs. */ private audioClipCounter; /** * Active clip recorders (video + audio). Allows stop() to * flush them all on session end so half-recorded clips still * upload. The recorder's `stop()` is kind-agnostic so both * kinds share the same Set. */ private videoClipRecorders; /** * Active URL-backed playback handles (audio + video prompts). * Disposed on stop()/teardown so detached players can't keep * playing after the session ends. */ private mediaFilePlayers; private readonly checkpointConfig; private readonly policySnapshot; /** Environment snapshot at session start; the point later checkpoints drift from. */ private checkpointBaseline; /** Initial baseline capture, awaited before an intentional device rebase. */ private checkpointBaselineReady; /** Prevent overlapping device mutations and let checkpoints wait for rollback. */ private deviceSwitchPromise; private checkpointTimer; private checkpointVisibilityListener; private checkpointDeviceChangeListener; /** Debounce timer for the `devicechange` trigger (browsers fire it in bursts). */ private checkpointDeviceChangeDebounce; /** Stored, uploader-wrapped webcam config so restartWebcam() can re-acquire in place. */ private webcamObserverCfg; private ready; private stopped; private endRequestedEmitted; private endedEmitted; private stopPromise; private pagehideListener; private recordingVisibilityListener; private abortListener; private heartbeatTimer; /** * Per-device fingerprint computed once at construction. Same * value flows through every event (via the worker's init * config), every chunk upload (via ChunkUploader config), and * every clip upload (via VideoClipRecorder + uploadClip). The * Vue wizard reads the matching `fingerprintId` from * `computeDeviceFingerprint()` so the server sees the same id * across preflight + session. */ readonly fingerprint: FingerprintResult; constructor(options: ProctoringClientOptions); /** * Microphone constraint for runtime clip capture — the candidate's * selected input when one was chosen in preflight, else the default. */ private audioInputConstraint; /** * Camera constraint for the standalone video-clip path (Path 3 — when * no webcam observer owns the stream). The observer applies its own * deviceId; this keeps the no-observer path on the same camera. */ private videoInputConstraint; /** * Cleanly end the proctoring session. * * This stops observers, drains queued media chunks, emits the terminal * `session.ended` event, flushes the worker queue, exits fullscreen, and * then terminates the worker. It is safe to call more than once; concurrent * callers receive the same in-flight promise. */ end(): Promise; /** * Fire-and-forget. The event is structured-cloned into the worker * and persisted to IndexedDB before being uploaded. Throws if the * client isn't ready yet — rare, only happens if `emit()` is called * synchronously in the same tick as construction. * * Use this for custom signals from your exam app (eg. * `client.emit("exam.submitted", { score: 87 })`). Observer-captured * events flow through the same pipeline automatically. */ emit(kind: EventKind, payload?: SessionEvent["payload"]): void; private emitCaptured; /** * The camera, mic, and speaker this session is capturing with — the * candidate's preflight picks resolved to human labels. Async because it * enumerates the browser's device list to map each id → label; safe to call * anytime after construction. * * A kind whose `id` is null ("System default") means the candidate never * explicitly overrode the browser default — we can't resolve the default * device's id without opening a stream, so it's reported as such. A pick * whose device is no longer enumerable falls back to its raw id as the label. * * No probe stream is opened: a live session already holds device permission, * so labels are populated. Speakers depend on `enumerateDevices` exposing * `audiooutput` (Safari does not), so `speaker` may be System default there. */ getSelectedDevices(): Promise; /** Change the microphone used by future clip recordings. */ switchMicrophone(deviceId?: string | null): Promise; /** * Change the output used by active and future SDK-owned media playback. * Browsers without setSinkId remember the choice but keep routing under OS control. */ switchSpeaker(deviceId?: string | null): Promise; /** Change the camera used by webcam capture and future standalone clips. */ switchCamera(deviceId?: string | null): Promise; private runDeviceSwitch; private assertInputSwitchAvailable; private resolveSelectedDevice; /** Accept only the intentionally changed device without masking other drift. */ private rebaseCheckpointDevice; /** * Runtime integrity checkpoint. Re-verifies the live environment against * the state that passed preflight and returns what has drifted — permission * revoked, selected device gone, external monitor connected, and (unless * `options.liveness === false`) camera/mic capture liveness. Read-only and * idempotent: it never prompts, opens a device, or changes session state, * and it never pauses the session — the returned {@link CheckpointResult} * is yours to act on. * * Reads the streams the session already holds (no second getUserMedia, no * second camera light). Every run also emits one `session.checkpoint` event * as an audit record. */ checkpoint(options?: CheckpointOptions): Promise; /** * A live capture track the session already holds — from the webcam observer * (video) or an in-flight clip recording (audio/video). Lets mic/camera * liveness run without opening a new device. Null when nothing is capturing. */ private liveCaptureTrack; /** * Snapshot the environment as it is now — the point every later checkpoint * drifts from. Only devices the candidate explicitly selected in preflight * (a non-default id) are compared; a "System default" pick has no id to * match, so it's not tracked for availability. */ private captureCheckpointBaseline; /** Emit the audit record for a checkpoint run (best-effort; never throws). */ private emitCheckpoint; /** * Start the SDK-owned checkpoint schedule (tab-return / heartbeat), if the * host configured `checkpoint.on`. No triggers → purely manual. Scheduled * runs hand each result to `onCheckpoint`, and drifting runs also to * `onDrift`. Wired at the end of boot; torn down in {@link stopCheckpoint}. */ private startCheckpoint; private runScheduledCheckpoint; private stopCheckpoint; /** * Re-open the browser screen-share picker for an already-running * session. Intended for the candidate-clicked-Stop-Sharing recovery * path: the host UI can lock the assessment, then call this from a * fresh user click to restore capture without ending the session or * creating a new attempt. * * Returns true when a valid share is active again. Returns false when * the candidate cancels the picker or selects a non-monitor surface. */ restartScreenShare(): Promise; /** * Start MediaRecorder on an already-active screen-share stream. * Used by staged flows that acquire/validate screen-share first, * then begin actual recording only when the assessment truly starts. */ startScreenShareRecording(): boolean; private emitTelemetry; private sendTelemetryDirect; private sendEventDirect; private emitSessionEndRequested; /** * Emit the terminal `session.ended` event. Unlike the public emit(), * this is called from clean programmatic teardown where it must * bypass the `ready` guard — the worker is still alive and able to * enqueue + flush, but `this.ready` may already be on its way down. * Posting the emit message BEFORE the subsequent flush/stop message * guarantees in-order handling in the worker: the event is enqueued * first, then drained by the same flush. * * Fired exactly once (guarded by `endedEmitted`). The server folds * this event into endedAt + terminal status. Clean stops run the * post-session analysis decision pass. */ private emitSessionEnded; private postSessionEndedToWorker; private notifySessionEvent; /** * Request fullscreen entry on the document. The Fullscreen API * REQUIRES a fresh user gesture, so this must be called inside * a click / keypress / pointerup handler — same as the call * site that opens screen-share / camera permission. Returns a * promise that resolves true on entry, false on rejection * (browser blocked, user dismissed, already in another * fullscreen target). * * Use cases: * - Customer's "Start session" button handler: call this * alongside ProctoringClient construction so the candidate * enters fullscreen immediately. * - "Return to fullscreen" modal button: when the SDK fires * `fullscreen.exited` mid-session, the host UI shows a * modal whose button click calls this method to restore * the lock. * * The DomObservers' fullscreenchange listener will pick up * the entry and emit `fullscreen.entered` through the normal * event pipeline. */ requestFullscreen(): Promise; private exitFullscreenOnSessionEnd; /** * Start recording an ad-hoc video clip. Use this for customer-driven * recordings — eg. answering a video question — that don't fit the * passive observer model. Returns a handle whose only method is * `stop()`. The clip uploads automatically on stop. * * Stream sourcing in priority order: * 1. Customer-provided MediaStream (`options.stream`) — shared with * the customer's app, the SDK does not own its lifecycle. * 2. The webcam observer's stream — reuses the camera handle the * observer is already holding for photos. No second permission * prompt, no second camera light. * 3. Fresh `getUserMedia({ video: true, audio: true })` — the SDK * owns this stream and releases it when the recorder stops. * * Must be called inside a user-activation handler when path 3 will * be taken (the customer's button click satisfies this). Paths 1 * and 2 already have an active stream so user activation doesn't * matter. * * Caps recording at 2 minutes by default to prevent runaway blobs * if the customer forgets to call stop(). */ recordVideoClip(options?: { stream?: MediaStream; /** * When a `stream` is supplied, stop its tracks on finalise. Default * false — a caller-provided stream's lifecycle is theirs to manage * (it may feed a preview element or another recorder). Set true when * the caller acquired a stream solely for this clip and wants the * SDK to release it (camera/mic light goes dark) on stop. No effect * when the SDK acquires the stream itself — it always owns those. */ stopStreamOnFinalize?: boolean; maxDurationMs?: number; videoBitrate?: number; /** * Whether to include a microphone in the clip. Default: true. * When the resolved stream has no audio track, the SDK acquires a * mic-only stream just for the clip and releases it on stop — * keeping the mic light dark for the rest of the session. */ audio?: boolean; onUploaded?: (clipNumber: number, byteSize: number, durationMs: number) => void; onDropped?: (clipNumber: number, reason: string) => void; /** * Best-effort mirror of the finalized clip to a second backend, * fired in parallel with the upload to the proctoring server. * Resolve = mirror succeeded; reject/throw = mirror failed. A * failure here NEVER changes the clip outcome — `onUploaded` / * `onDropped` still reflect the proctoring-server result. Observe * mirror failures via `onClipDataError`. */ onClipData?: (clip: { blob: Blob; mimeType: string; durationMs: number; clipNumber: number; }) => Promise; /** * Notified when `onClipData` rejects. Non-fatal: the clip is still * uploaded to the proctoring server. Exists so callers can log / * surface the mirror failure without it masquerading as a drop. */ onClipDataError?: (clipNumber: number, error: Error) => void; }): Promise; /** * Record an ad-hoc audio-only clip. Mic gets acquired on demand * (or borrowed from the caller's stream), recorded to opus-in- * WebM by default, uploaded to /audio-clips on stop, and * surfaces on the dashboard's Audio tab. * * Stream sourcing strategy mirrors recordVideoClip: * 1. If the customer hands in a stream, use it directly. * They own the lifecycle; the recorder never stops their * tracks unless `stopStreamOnFinalize` is true. * 2. Otherwise call getUserMedia({ audio: true }) for a * fresh mic-only stream and own it (stopped on finalise * so the mic light goes dark). * * `recordAudioClip` is independent of `recordVideoClip` -- the * two can run simultaneously (eg. a video clip for "show me * your workspace" plus a parallel audio clip for narration). * Clip numbers come from a separate counter; see * `audioClipCounter` below. * * Caps at 2 minutes by default like its video sibling. */ recordAudioClip(options?: { stream?: MediaStream; stopStreamOnFinalize?: boolean; maxDurationMs?: number; onUploaded?: (clipNumber: number, byteSize: number, durationMs: number, volumeAnalysis?: AudioClipVolumeAnalysis) => void; onDropped?: (clipNumber: number, reason: string, volumeAnalysis?: AudioClipVolumeAnalysis, code?: ClipDropCode) => void; /** * Best-effort mirror of the finalized clip to a second backend, * fired in parallel with the upload to the proctoring server. * Resolve = mirror succeeded; reject/throw = mirror failed. A * failure here NEVER changes the clip outcome — `onUploaded` / * `onDropped` still reflect the proctoring-server result. Observe * mirror failures via `onClipDataError`. */ onClipData?: (clip: { blob: Blob; mimeType: string; durationMs: number; clipNumber: number; }) => Promise; /** * Notified when `onClipData` rejects. Non-fatal: the clip is still * uploaded to the proctoring server. Exists so callers can log / * surface the mirror failure without it masquerading as a drop. */ onClipDataError?: (clipNumber: number, error: Error) => void; }): Promise; /** * Begin recording the integrity signals for a written answer. Returns * a {@link TextAnswerHandle} whose methods emit the `text.*` events * (paste, typing dynamics, checkpoints, focus, synthetic input, save * outcome, submit). Used by the Vue . * * Note there is NO upload here — a writing answer's text is owned by * the integrator and saved through their own backend. This only ships * the integrity signals to the proctoring server via the normal event * pipeline. The emit swallows the pre-ready throw so a keystroke can * never crash the field. */ recordTextAnswer(options: { questionId: string; }): TextAnswerHandle; /** * Upload a video clip the customer recorded themselves. Use this * when your app has its own preview / retake / accept UX and you * just want the SDK to ship the finished Blob to the proctoring * server. The clip lands in the same dashboard surface as one * recorded via `recordVideoClip()`. * * Returns a {@link VideoClipHandle} whose `clipNumber` is the * server-assigned identifier (1-based per session) and whose * `stop()` is a no-op — the recording is already done. * * Emits `video-clip.uploaded` or `video-clip.dropped` on the * timeline. Throws if the upload itself fails synchronously * (network unavailable); 4xx/5xx responses route through * `video-clip.dropped` and resolve normally. */ uploadVideoClip(blob: Blob, options?: { durationMs?: number; onUploaded?: (clipNumber: number, byteSize: number, durationMs: number) => void; onDropped?: (clipNumber: number, reason: string) => void; }): Promise; /** * Play an assessment-provided audio file, for example a listening * question prompt. Pass only the URL for a detached player, or pass * your own