/** * Stable, machine-readable codes for clip recording/upload failures. * * These accompany (never replace) the human-readable message so callers * can branch on a value that does not change with browser locale or * wording. Shared by the audio + video clip paths. * * The code SETS below are the single source of truth: the union types * are derived from them, and the `normalize*`/`is*` guards validate any * runtime value against them. This is what makes the codes safe across * every scenario — SDK-emitted, local-fallback, classifier output, or a * value smuggled in by an untyped custom client: anything off-union is * coerced to a defined fallback before it reaches a consumer. */ /** A clip recorded but could not be stored. */ export const CLIP_DROP_CODES = [ /** MediaRecorder produced zero bytes (instant stop, dead track). */ "empty-recording", /** Upload rejected with a 4xx (auth, quota, origin, validation). */ "rejected", /** Upload failed with a 5xx. */ "server-error", /** The upload request threw (offline, CORS, DNS, connection reset). */ "network-error", /** The upload did not settle before the SDK's size-aware deadline. */ "timeout", ] as const; /** A recording could not be started or produced no clip at all. */ export const CLIP_START_ERROR_CODES = [ /** Microphone/camera permission was denied. */ "permission-denied", /** No matching input device was found. */ "no-device", /** The device is already in use by another consumer. */ "device-in-use", /** A supplied stream had no audio track. */ "no-audio-track", /** A supplied stream had no video track. */ "no-video-track", /** getUserMedia or MediaRecorder is unavailable (insecure context, old browser). */ "unsupported", /** Anything not matched above. */ "unknown", ] as const; export type ClipDropCode = (typeof CLIP_DROP_CODES)[number]; export type ClipStartErrorCode = (typeof CLIP_START_ERROR_CODES)[number]; /** Fallbacks used when a value is not a recognised code. */ const DROP_FALLBACK: ClipDropCode = "network-error"; const START_FALLBACK: ClipStartErrorCode = "unknown"; const DROP_SET: ReadonlySet = new Set(CLIP_DROP_CODES); const START_SET: ReadonlySet = new Set(CLIP_START_ERROR_CODES); export function isClipDropCode(value: unknown): value is ClipDropCode { return typeof value === "string" && DROP_SET.has(value); } export function isClipStartErrorCode( value: unknown, ): value is ClipStartErrorCode { return typeof value === "string" && START_SET.has(value); } /** * Coerce any value to a valid {@link ClipDropCode}. In-union values pass * through; anything else (undefined, a stray string from an untyped * client, a future code this build doesn't know) becomes the fallback. */ export function normalizeClipDropCode(value: unknown): ClipDropCode { return isClipDropCode(value) ? value : DROP_FALLBACK; } /** Coerce any value to a valid {@link ClipStartErrorCode}. */ export function normalizeClipStartErrorCode( value: unknown, ): ClipStartErrorCode { return isClipStartErrorCode(value) ? value : START_FALLBACK; } /** * Classify a thrown recording/acquisition error into a stable code. * Mirrors the name-checks used for SDK telemetry so the two stay in * step. The audio/video no-track distinction comes from the error * message itself, so the kind doesn't need to be passed in. The return * is always a valid {@link ClipStartErrorCode} (worst case "unknown"). */ export function classifyClipStartError(err: unknown): ClipStartErrorCode { const error = err instanceof Error ? err : new Error(String(err)); const name = error.name; const message = error.message; if ( name === "NotAllowedError" || name === "SecurityError" || /permission|denied/i.test(message) ) { return "permission-denied"; } if (name === "NotFoundError" || name === "OverconstrainedError") { return "no-device"; } if (name === "NotReadableError" || name === "AbortError") { return "device-in-use"; } if (/no audio tracks/i.test(message)) return "no-audio-track"; if (/no video tracks/i.test(message)) return "no-video-track"; if ( /not available|is not available|unsupported|MediaRecorder/i.test(message) ) { return "unsupported"; } return "unknown"; }