{"version":3,"file":"use-media-devices.cjs","names":[],"sources":["../../src/hooks/use-media-devices.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** One device the browser is willing to tell us about. */\nexport interface MediaDeviceOption {\n    deviceId: string;\n    /**\n     * Human label, or `\"\"` until permission has been granted.\n     *\n     * See {@link UseMediaDevicesResult.labelsAvailable} — an empty label is not a\n     * bug, it is the browser refusing to fingerprint the machine.\n     */\n    label: string;\n    kind: MediaDeviceKind;\n    groupId: string;\n}\n\n/** Value returned by {@link useMediaDevices}. */\nexport interface UseMediaDevicesResult {\n    /** Every device, in the order the browser listed them. */\n    devices: MediaDeviceOption[];\n    /** Microphones and other audio sources. */\n    audioInputs: MediaDeviceOption[];\n    /** Speakers and headsets. Empty on browsers with no output routing (Safari, Firefox). */\n    audioOutputs: MediaDeviceOption[];\n    /** Cameras. */\n    videoInputs: MediaDeviceOption[];\n    /**\n     * Whether labels are filled in.\n     *\n     * `false` means the list is real but anonymous — render a picker only after a\n     * capture permission has been granted, or the user sees \"\" repeated N times.\n     */\n    labelsAvailable: boolean;\n    /** `enumerateDevices` is unavailable in this browser. */\n    supported: boolean;\n    /** Re-enumerate. The hook already does this on `devicechange`. */\n    refresh: () => void;\n}\n\nconst EMPTY: MediaDeviceOption[] = [];\n\n/**\n * List capture and playback devices, and keep the list fresh.\n *\n * Two things about `enumerateDevices` that decide how a picker must be built:\n *\n * 1. **Labels are gated behind permission.** Before the user grants a capture\n *    permission, every `label` is `\"\"` — the list length and ids are real, the names\n *    are not. A device picker rendered at that point is a column of blanks, so\n *    `labelsAvailable` is exposed to gate it. Ask for the microphone first, then\n *    show the picker.\n * 2. **The list changes while the page is open.** Plugging in a headset mid-recording\n *    is the normal case, not an edge case, so the hook subscribes to `devicechange`\n *    instead of enumerating once on mount.\n *\n * `audioOutputs` is empty on browsers with no output routing at all (Safari and\n * Firefox do not implement `setSinkId`); an empty array there means \"you cannot\n * offer this choice\", not \"no speakers\".\n *\n * @returns The device lists, whether labels are filled in, and a `refresh()`.\n *\n * @example\n * const { audioInputs, labelsAvailable } = useMediaDevices();\n * // ...after the mic permission is granted:\n * {labelsAvailable && <Select options={audioInputs.map((d) => ({ value: d.deviceId, label: d.label }))} />}\n */\nexport function useMediaDevices(): UseMediaDevicesResult {\n    const [devices, setDevices] = useState<MediaDeviceOption[]>(EMPTY);\n    const [supported, setSupported] = useState(false);\n    const [token, setToken] = useState(0);\n\n    const refresh = useCallback(() => setToken((value) => value + 1), []);\n\n    useEffect(() => {\n        let cancelled = false;\n\n        if (\n            typeof navigator === \"undefined\" ||\n            !navigator.mediaDevices ||\n            typeof navigator.mediaDevices.enumerateDevices !== \"function\"\n        ) {\n            setSupported(false);\n            setDevices(EMPTY);\n            return;\n        }\n        setSupported(true);\n        const media = navigator.mediaDevices;\n\n        async function read(): Promise<void> {\n            try {\n                const list = await media.enumerateDevices();\n                if (cancelled) return;\n                setDevices(\n                    list.map((device) => ({\n                        deviceId: device.deviceId,\n                        label: device.label,\n                        kind: device.kind,\n                        groupId: device.groupId,\n                    })),\n                );\n            } catch {\n                if (!cancelled) setDevices(EMPTY);\n            }\n        }\n\n        void read();\n        const onChange = (): void => void read();\n        media.addEventListener?.(\"devicechange\", onChange);\n\n        return () => {\n            cancelled = true;\n            media.removeEventListener?.(\"devicechange\", onChange);\n        };\n    }, [token]);\n\n    return {\n        devices,\n        audioInputs: devices.filter((device) => device.kind === \"audioinput\"),\n        audioOutputs: devices.filter((device) => device.kind === \"audiooutput\"),\n        videoInputs: devices.filter((device) => device.kind === \"videoinput\"),\n        labelsAvailable: devices.length > 0 && devices.some((device) => device.label !== \"\"),\n        supported,\n        refresh,\n    };\n}\n"],"mappings":"uBAuCA,IAAM,EAA6B,CAAC,EA2BpC,SAAgB,GAAyC,CACrD,GAAM,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAA8B,CAAK,EAC3D,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,EAAK,EAC1C,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,CAAC,EAE9B,GAAA,EAAU,EAAA,YAAA,KAAkB,EAAU,GAAU,EAAQ,CAAC,EAAG,CAAC,CAAC,EA4CpE,OA1CA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GAEhB,GACI,OAAO,UAAc,KACrB,CAAC,UAAU,cACX,OAAO,UAAU,aAAa,kBAAqB,WACrD,CACE,EAAa,EAAK,EAClB,EAAW,CAAK,EAChB,MACJ,CACA,EAAa,EAAI,EACjB,IAAM,EAAQ,UAAU,aAExB,eAAe,GAAsB,CACjC,GAAI,CACA,IAAM,EAAO,MAAM,EAAM,iBAAiB,EAC1C,GAAI,EAAW,OACf,EACI,EAAK,IAAK,IAAY,CAClB,SAAU,EAAO,SACjB,MAAO,EAAO,MACd,KAAM,EAAO,KACb,QAAS,EAAO,OACpB,EAAE,CACN,CACJ,MAAQ,CACC,GAAW,EAAW,CAAK,CACpC,CACJ,CAEA,EAAU,EACV,IAAM,MAAuB,KAAK,EAAK,EAGvC,OAFA,EAAM,mBAAmB,eAAgB,CAAQ,MAEpC,CACT,EAAY,GACZ,EAAM,sBAAsB,eAAgB,CAAQ,CACxD,CACJ,EAAG,CAAC,CAAK,CAAC,EAEH,CACH,UACA,YAAa,EAAQ,OAAQ,GAAW,EAAO,OAAS,YAAY,EACpE,aAAc,EAAQ,OAAQ,GAAW,EAAO,OAAS,aAAa,EACtE,YAAa,EAAQ,OAAQ,GAAW,EAAO,OAAS,YAAY,EACpE,gBAAiB,EAAQ,OAAS,GAAK,EAAQ,KAAM,GAAW,EAAO,QAAU,EAAE,EACnF,YACA,SACJ,CACJ"}