{"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the hook is the recorder's lifecycle seen from React:\n * permission, the recorder handle, the clock, the level meter and the auto-stop\n * timer, all of which have to be torn down together on unmount. A second hook would\n * own half the teardown.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n    createAudioRecorder,\n    type AudioRecorderHandle,\n    type AudioRecorderOptions,\n    type AudioRecorderStatus,\n    type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n    /**\n     * Stop automatically after this many milliseconds.\n     *\n     * Worth setting on anything user-facing: a recording left running by accident is\n     * a memory buffer that grows until the tab dies.\n     */\n    maxDurationMs?: number;\n    /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n    onRecorded?: (recording: AudioRecording) => void;\n    /** How often the clock and the level are published, in ms. Default 100. */\n    tickMs?: number;\n    /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n    disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n    status: AudioRecorderStatus;\n    /** Recorded length so far, excluding paused time. */\n    durationMs: number;\n    /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n    level: number;\n    /** The finished recording, or `null` before the first `stop()`. */\n    recording: AudioRecording | null;\n    /** Recorder-level error (encoder failure, device pulled mid-recording). */\n    error: unknown;\n    /** `false` when there is no stream yet, or the browser cannot record. */\n    ready: boolean;\n    start: () => void;\n    pause: () => void;\n    resume: () => void;\n    /** Stop and resolve with the recording. Also published on `recording`. */\n    stop: () => Promise<AudioRecording | null>;\n    /** Stop and throw the audio away. */\n    cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n    stream: MediaStream | null,\n    options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n    const {\n        maxDurationMs,\n        onRecorded,\n        onChunk,\n        onError,\n        mimeType,\n        audioBitsPerSecond,\n        timesliceMs,\n        tickMs = 100,\n        disableLevelMeter = false,\n    } = options;\n\n    const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n    const [durationMs, setDurationMs] = useState(0);\n    const [level, setLevel] = useState(0);\n    const [recording, setRecording] = useState<AudioRecording | null>(null);\n    const [error, setError] = useState<unknown>(null);\n    const [ready, setReady] = useState(false);\n\n    const recorderRef = useRef<AudioRecorderHandle | null>(null);\n    const meterRef = useRef<LevelMeter | null>(null);\n\n    const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n    const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n    const emitError = useStableCallback((err: unknown) => {\n        setError(err);\n        onError?.(err);\n    });\n\n    /**\n     * Build the recorder and the meter for a stream, and tear both down when it\n     * changes.\n     *\n     * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n     * stream it was constructed with — swapping microphones mid-session hands us a\n     * new `MediaStream`, and reusing the old recorder would keep recording the device\n     * the user just switched away from. It is rebuilt on a container or bitrate change\n     * for the same reason: both are constructor arguments.\n     */\n    useEffect(() => {\n        if (!stream) {\n            setReady(false);\n            return;\n        }\n        let recorder: AudioRecorderHandle;\n        try {\n            recorder = createAudioRecorder(stream, {\n                mimeType,\n                audioBitsPerSecond,\n                timesliceMs,\n                onChunk: emitChunk,\n                onError: emitError,\n            });\n        } catch (err) {\n            setError(err);\n            setReady(false);\n            return;\n        }\n        recorderRef.current = recorder;\n        meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n        setReady(true);\n        setStatus(\"idle\");\n\n        return () => {\n            if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n                recorder.cancel();\n            }\n            meterRef.current?.stop();\n            meterRef.current = null;\n            recorderRef.current = null;\n            setReady(false);\n        };\n    }, [\n        stream,\n        disableLevelMeter,\n        mimeType,\n        audioBitsPerSecond,\n        timesliceMs,\n        emitChunk,\n        emitError,\n    ]);\n\n    const stop = useCallback(async (): Promise<AudioRecording | null> => {\n        const recorder = recorderRef.current;\n        if (!recorder) return null;\n        const current = recorder.status();\n        if (current !== \"recording\" && current !== \"paused\") return null;\n        const result = await recorder.stop();\n        setStatus(\"stopped\");\n        setDurationMs(result.durationMs);\n        setLevel(0);\n        setRecording(result);\n        emitRecorded(result);\n        return result;\n    }, [emitRecorded]);\n\n    /**\n     * Publish the clock and the level while something is running, and enforce\n     * `maxDurationMs`.\n     *\n     * The cap is checked on the same tick rather than with a `setTimeout` armed at\n     * `start()`, because a paused recording must not keep counting toward it — a\n     * timeout would fire while the user is paused mid-sentence.\n     */\n    useEffect(() => {\n        if (status !== \"recording\" && status !== \"paused\") {\n            setLevel(0);\n            return;\n        }\n        const id = setInterval(() => {\n            const recorder = recorderRef.current;\n            if (!recorder) return;\n            const elapsed = recorder.durationMs();\n            setDurationMs(elapsed);\n            setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n            if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n        }, tickMs);\n        return () => clearInterval(id);\n    }, [status, tickMs, maxDurationMs, stop]);\n\n    const start = useCallback((): void => {\n        const recorder = recorderRef.current;\n        if (!recorder) return;\n        setRecording(null);\n        setError(null);\n        setDurationMs(0);\n        recorder.start();\n        setStatus(recorder.status());\n    }, []);\n\n    const pause = useCallback((): void => {\n        const recorder = recorderRef.current;\n        if (!recorder) return;\n        recorder.pause();\n        setDurationMs(recorder.durationMs());\n        setStatus(recorder.status());\n    }, []);\n\n    const resume = useCallback((): void => {\n        const recorder = recorderRef.current;\n        if (!recorder) return;\n        recorder.resume();\n        setStatus(recorder.status());\n    }, []);\n\n    const cancel = useCallback((): void => {\n        const recorder = recorderRef.current;\n        if (!recorder) return;\n        recorder.cancel();\n        setStatus(\"idle\");\n        setDurationMs(0);\n        setLevel(0);\n        setRecording(null);\n    }, []);\n\n    return {\n        status,\n        durationMs,\n        level,\n        recording,\n        error,\n        ready,\n        start,\n        pause,\n        resume,\n        stop,\n        cancel,\n    };\n}\n"],"mappings":"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"}