{"version":3,"file":"use-camera-stream.cjs","names":[],"sources":["../../src/vision/use-camera-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\n\nimport {\n    classifyMediaError,\n    missingCaptureApiError,\n    type MediaAccessError,\n} from \"@/audio/media-access\";\n\n/** Lifecycle status of the camera stream. */\nexport type CameraStreamStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n/** Classified reason a camera stream could not be acquired. */\nexport type CameraStreamErrorKind =\n    \"unsupported\" | \"permission-denied\" | \"no-camera\" | \"in-use\" | \"insecure\" | \"unknown\";\n\n/** A classified camera error with a human-readable, English message. */\nexport interface CameraStreamError {\n    kind: CameraStreamErrorKind;\n    message: string;\n}\n\n/** Options for {@link useCameraStream}. */\nexport interface UseCameraStreamOptions {\n    /**\n     * Constraints passed to `getUserMedia`. Defaults to the rear\n     * (\"environment\") camera at Full-HD ideal resolution with audio off.\n     * Read when the stream (re)starts — change it and call `retry()` to apply.\n     */\n    constraints?: MediaStreamConstraints;\n    /**\n     * Hold off acquiring the camera until this is `true`. Default `true`.\n     *\n     * The point is to be able to *not* prompt. A permission prompt costs the user a\n     * decision and, if they refuse, costs the app the feature permanently — so a\n     * surface that already knows it cannot do its job (a barcode scanner in a browser\n     * with no decoder) must not open the camera to then say so. Flipping this to `false`\n     * also releases a stream that is already live.\n     */\n    enabled?: boolean;\n}\n\n/** Value returned by {@link useCameraStream}. */\nexport interface UseCameraStreamApi {\n    /** Current lifecycle status. */\n    status: CameraStreamStatus;\n    /** The classified error, or `null` while not in the `error` status. */\n    error: CameraStreamError | null;\n    /** Attach to a `<video ref={…} />`. The stream is wired to it once ready. */\n    videoRef: RefObject<HTMLVideoElement | null>;\n    /**\n     * The live stream, or `null`.\n     *\n     * Exposed for the things that need the **track** rather than the picture — the LED\n     * torch (`useTorch`), the real frame size from `getSettings()`, recording it with\n     * `useVideoRecorder`. Do not stop it yourself: the hook owns its lifetime and\n     * releases it on unmount and on `retry()`.\n     */\n    stream: MediaStream | null;\n    /** Manually re-attempt after an error (e.g. the user changed permissions). */\n    retry: () => void;\n}\n\n/** Rear-camera Full-HD defaults used when no `constraints` are supplied. */\nconst DEFAULT_CONSTRAINTS: MediaStreamConstraints = {\n    video: {\n        facingMode: { ideal: \"environment\" },\n        width: { ideal: 1920 },\n        height: { ideal: 1080 },\n    },\n    audio: false,\n};\n\n/**\n * Re-label a {@link MediaAccessError} as a {@link CameraStreamError}.\n *\n * The classification itself lives in one place for the whole SDK\n * (`classifyMediaError`), so a microphone and a camera failure are never explained\n * two different ways. Only the name of one kind differs: this surface shipped\n * `\"no-camera\"` where the shared taxonomy says `\"not-found\"`, and renaming a\n * published union member would break every consumer switching on it.\n *\n * @param error - The shared classification.\n * @returns The same error under this module's kind names.\n */\nfunction toCameraError({ kind, message }: MediaAccessError): CameraStreamError {\n    return { kind: kind === \"not-found\" ? \"no-camera\" : kind, message };\n}\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link CameraStreamError}. Secure-context\n * and environment checks run first (they are the reason `getUserMedia` is missing or\n * rejects), then the `DOMException.name` is mapped to a stable `kind`.\n */\nfunction classifyError(err: unknown): CameraStreamError {\n    return toCameraError(classifyMediaError(err, \"camera\"));\n}\n\n/**\n * Acquire a `MediaStream` via `getUserMedia`, attach it to a `<video>` element,\n * and expose status/error so the page can render permission and error states.\n * The stream is automatically released on unmount or retry.\n *\n * Defaults to the rear (\"environment\") camera; desktops fall back to whatever\n * single camera they expose. Pass `options.constraints` to override, or\n * `options.enabled: false` to render the surface without prompting for the camera\n * at all.\n *\n * Implementation notes:\n * - Cleanup detaches the stream from a *snapshotted* video node, so it releases\n *   the same element it attached to even if the page remounts the `<video>`.\n * - When `getUserMedia` is missing, an insecure context is the usual cause, so\n *   the hook prefers that (actionable) error; otherwise it reports `unsupported`.\n * - `video.play()` rejections are swallowed: autoplay may be blocked, but the\n *   user gesture that opened the camera usually counts and playback resumes on\n *   the next interaction.\n *\n * @param options - optional configuration (see {@link UseCameraStreamOptions}).\n * @returns The stream status, classified error, a `videoRef` to attach, the live\n *   `stream` for whatever needs the track itself, and a `retry()` to re-attempt\n *   acquisition.\n */\nexport function useCameraStream(options: UseCameraStreamOptions = {}): UseCameraStreamApi {\n    const [status, setStatus] = useState<CameraStreamStatus>(\"loading\");\n    const [error, setError] = useState<CameraStreamError | null>(null);\n    const [live, setLive] = useState<MediaStream | null>(null);\n    const [retryToken, setRetryToken] = useState(0);\n    const videoRef = useRef<HTMLVideoElement | null>(null);\n    const constraintsRef = useLatestRef(options.constraints ?? DEFAULT_CONSTRAINTS);\n\n    const enabled = options.enabled ?? true;\n\n    useEffect(() => {\n        let cancelled = false;\n        let stream: MediaStream | null = null;\n        let attachedVideo: HTMLVideoElement | null = null;\n\n        async function start(): Promise<void> {\n            if (!enabled) {\n                setStatus(\"idle\");\n                setError(null);\n                return;\n            }\n            setStatus(\"loading\");\n            setError(null);\n\n            if (\n                typeof navigator === \"undefined\" ||\n                !navigator.mediaDevices ||\n                typeof navigator.mediaDevices.getUserMedia !== \"function\"\n            ) {\n                if (!cancelled) {\n                    setError(toCameraError(missingCaptureApiError(\"camera\")));\n                    setStatus(\"error\");\n                }\n                return;\n            }\n\n            try {\n                stream = await navigator.mediaDevices.getUserMedia(constraintsRef.current);\n                if (cancelled) {\n                    stream.getTracks().forEach((track) => track.stop());\n                    return;\n                }\n                const video = videoRef.current;\n                if (!video) {\n                    stream.getTracks().forEach((track) => track.stop());\n                    return;\n                }\n                attachedVideo = video;\n                video.srcObject = stream;\n                setLive(stream);\n                await video.play().catch(() => undefined);\n                if (!cancelled) setStatus(\"ready\");\n            } catch (err) {\n                if (!cancelled) {\n                    setError(classifyError(err));\n                    setStatus(\"error\");\n                }\n            }\n        }\n\n        void start();\n\n        return () => {\n            cancelled = true;\n            if (stream) {\n                stream.getTracks().forEach((track) => track.stop());\n            }\n            if (attachedVideo) {\n                attachedVideo.srcObject = null;\n            }\n            setLive(null);\n        };\n    }, [retryToken, enabled, constraintsRef]);\n\n    return {\n        status,\n        error,\n        videoRef,\n        stream: live,\n        retry: () => setRetryToken((n) => n + 1),\n    };\n}\n"],"mappings":"6GAgEA,IAAM,EAA8C,CAChD,MAAO,CACH,WAAY,CAAE,MAAO,aAAc,EACnC,MAAO,CAAE,MAAO,IAAK,EACrB,OAAQ,CAAE,MAAO,IAAK,CAC1B,EACA,MAAO,EACX,EAcA,SAAS,EAAc,CAAE,OAAM,WAAgD,CAC3E,MAAO,CAAE,KAAM,IAAS,YAAc,YAAc,EAAM,SAAQ,CACtE,CAOA,SAAS,EAAc,EAAiC,CACpD,OAAO,EAAc,EAAA,mBAAmB,EAAK,QAAQ,CAAC,CAC1D,CA0BA,SAAgB,EAAgB,EAAkC,CAAC,EAAuB,CACtF,GAAM,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA6B,SAAS,EAC5D,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAmC,IAAI,EAC3D,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAA6B,IAAI,EACnD,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAS,CAAC,EACxC,GAAA,EAAW,EAAA,OAAA,CAAgC,IAAI,EAC/C,EAAiB,EAAA,aAAa,EAAQ,aAAe,CAAmB,EAExE,EAAU,EAAQ,SAAW,GAkEnC,OAhEA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GACZ,EAA6B,KAC7B,EAAyC,KAE7C,eAAe,GAAuB,CAClC,GAAI,CAAC,EAAS,CACV,EAAU,MAAM,EAChB,EAAS,IAAI,EACb,MACJ,CAIA,GAHA,EAAU,SAAS,EACnB,EAAS,IAAI,EAGT,OAAO,UAAc,KACrB,CAAC,UAAU,cACX,OAAO,UAAU,aAAa,cAAiB,WACjD,CACO,IACD,EAAS,EAAc,EAAA,uBAAuB,QAAQ,CAAC,CAAC,EACxD,EAAU,OAAO,GAErB,MACJ,CAEA,GAAI,CAEA,GADA,EAAS,MAAM,UAAU,aAAa,aAAa,EAAe,OAAO,EACrE,EAAW,CACX,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,IAAM,EAAQ,EAAS,QACvB,GAAI,CAAC,EAAO,CACR,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,EAAgB,EAChB,EAAM,UAAY,EAClB,EAAQ,CAAM,EACd,MAAM,EAAM,KAAK,CAAC,CAAC,UAAY,IAAA,EAAS,EACnC,GAAW,EAAU,OAAO,CACrC,OAAS,EAAK,CACL,IACD,EAAS,EAAc,CAAG,CAAC,EAC3B,EAAU,OAAO,EAEzB,CACJ,CAIA,OAFA,EAAW,MAEE,CACT,EAAY,GACR,GACA,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAElD,IACA,EAAc,UAAY,MAE9B,EAAQ,IAAI,CAChB,CACJ,EAAG,CAAC,EAAY,EAAS,CAAc,CAAC,EAEjC,CACH,SACA,QACA,WACA,OAAQ,EACR,UAAa,EAAe,GAAM,EAAI,CAAC,CAC3C,CACJ"}