{"version":3,"file":"use-microphone.cjs","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n    classifyMediaError,\n    isMediaCaptureSupported,\n    missingCaptureApiError,\n    type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n    /**\n     * Open the stream on mount instead of waiting for `start()`. Default `false`.\n     *\n     * Left off on purpose. Opening on mount means prompting on mount, and a\n     * permission prompt the user did not provoke is the most reliable way to earn a\n     * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n     * again. Wire `start()` to the button that needs the microphone.\n     */\n    autoStart?: boolean;\n    /** Specific microphone, from `useMediaDevices().audioInputs`. */\n    deviceId?: string;\n    /**\n     * Browser voice processing. All three default to `true`, which is what speech\n     * wants; turn them off for music, where a gate chewing on a decaying note is\n     * worse than the room noise it removes.\n     */\n    echoCancellation?: boolean;\n    noiseSuppression?: boolean;\n    autoGainControl?: boolean;\n    /** Escape hatch: full constraints, replacing everything above. */\n    constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n    status: MicrophoneStatus;\n    /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n    stream: MediaStream | null;\n    /** Classified error, or `null` outside the `error` status. */\n    error: MediaAccessError | null;\n    /** Open the stream. Safe to call when already open — it is a no-op. */\n    start: () => void;\n    /** Release the microphone. The browser's recording indicator only clears here. */\n    stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n    const {\n        autoStart = false,\n        deviceId,\n        echoCancellation = true,\n        noiseSuppression = true,\n        autoGainControl = true,\n        constraints,\n    } = options;\n\n    const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n    const [stream, setStream] = useState<MediaStream | null>(null);\n    const [error, setError] = useState<MediaAccessError | null>(null);\n    const streamRef = useRef<MediaStream | null>(null);\n    const generation = useRef(0);\n\n    const release = useCallback((): void => {\n        streamRef.current?.getTracks().forEach((track) => track.stop());\n        streamRef.current = null;\n    }, []);\n\n    const stop = useCallback((): void => {\n        generation.current += 1;\n        release();\n        setStream(null);\n        setStatus(\"idle\");\n        setError(null);\n    }, [release]);\n\n    const start = useCallback((): void => {\n        if (streamRef.current) return;\n        const run = (generation.current += 1);\n        setStatus(\"requesting\");\n        setError(null);\n\n        if (!isMediaCaptureSupported()) {\n            setError(missingCaptureApiError(\"microphone\"));\n            setStatus(\"error\");\n            return;\n        }\n\n        const request: MediaStreamConstraints = constraints ?? {\n            audio: {\n                echoCancellation,\n                noiseSuppression,\n                autoGainControl,\n                ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n            },\n            video: false,\n        };\n\n        void navigator.mediaDevices\n            .getUserMedia(request)\n            .then((next) => {\n                if (run !== generation.current) {\n                    next.getTracks().forEach((track) => track.stop());\n                    return;\n                }\n                streamRef.current = next;\n                setStream(next);\n                setStatus(\"ready\");\n            })\n            .catch((err: unknown) => {\n                if (run !== generation.current) return;\n                setError(classifyMediaError(err, \"microphone\"));\n                setStatus(\"error\");\n            });\n    }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n    useEffect(() => {\n        if (autoStart) start();\n        // `start` is stable per constraint set; re-running on every identity change\n        // would reopen the device on each render.\n        // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [autoStart]);\n\n    useEffect(() => release, [release]);\n\n    return { status, stream, error, start, stop };\n}\n"],"mappings":"6DAqEA,SAAgB,EAAc,EAAgC,CAAC,EAAwB,CACnF,GAAM,CACF,YAAY,GACZ,WACA,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,eACA,EAEE,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA2B,MAAM,EACvD,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA6B,IAAI,EACvD,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAkC,IAAI,EAC1D,GAAA,EAAY,EAAA,OAAA,CAA2B,IAAI,EAC3C,GAAA,EAAa,EAAA,OAAA,CAAO,CAAC,EAErB,GAAA,EAAU,EAAA,YAAA,KAAwB,CACpC,EAAU,SAAS,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAC9D,EAAU,QAAU,IACxB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAO,EAAA,YAAA,KAAwB,CACjC,EAAW,SAAW,EACtB,EAAQ,EACR,EAAU,IAAI,EACd,EAAU,MAAM,EAChB,EAAS,IAAI,CACjB,EAAG,CAAC,CAAO,CAAC,EAEN,GAAA,EAAQ,EAAA,YAAA,KAAwB,CAClC,GAAI,EAAU,QAAS,OACvB,IAAM,EAAO,EAAW,SAAW,EAInC,GAHA,EAAU,YAAY,EACtB,EAAS,IAAI,EAET,CAAC,EAAA,wBAAwB,EAAG,CAC5B,EAAS,EAAA,uBAAuB,YAAY,CAAC,EAC7C,EAAU,OAAO,EACjB,MACJ,CAEA,IAAM,EAAkC,GAAe,CACnD,MAAO,CACH,mBACA,mBACA,kBACA,GAAI,EAAW,CAAE,SAAU,CAAE,MAAO,CAAS,CAAE,EAAI,CAAC,CACxD,EACA,MAAO,EACX,EAEA,UAAe,aACV,aAAa,CAAO,CAAC,CACrB,KAAM,GAAS,CACZ,GAAI,IAAQ,EAAW,QAAS,CAC5B,EAAK,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAChD,MACJ,CACA,EAAU,QAAU,EACpB,EAAU,CAAI,EACd,EAAU,OAAO,CACrB,CAAC,CAAC,CACD,MAAO,GAAiB,CACjB,IAAQ,EAAW,UACvB,EAAS,EAAA,mBAAmB,EAAK,YAAY,CAAC,EAC9C,EAAU,OAAO,EACrB,CAAC,CACT,EAAG,CAAC,EAAa,EAAU,EAAkB,EAAkB,CAAe,CAAC,EAW/E,OATA,EAAA,EAAA,UAAA,KAAgB,CACR,GAAW,EAAM,CAIzB,EAAG,CAAC,CAAS,CAAC,GAEd,EAAA,EAAA,UAAA,KAAgB,EAAS,CAAC,CAAO,CAAC,EAE3B,CAAE,SAAQ,SAAQ,QAAO,QAAO,MAAK,CAChD"}