{"version":3,"file":"use-video-recorder.cjs","names":[],"sources":["../../src/capture/use-video-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the same lifecycle as the audio recorder —\n * permission, recorder, clock, auto-stop — over a stream that also carries video,\n * with the preview element to attach and release.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport type { MediaRecorderHandle, MediaRecorderStatus } from \"./media-recorder\";\nimport {\n    createVideoRecorder,\n    type VideoRecorderOptions,\n    type VideoRecording,\n} from \"./video-recorder\";\n\n/** Options for {@link useVideoRecorder}. */\nexport interface UseVideoRecorderOptions extends VideoRecorderOptions {\n    /**\n     * Stop automatically after this many milliseconds.\n     *\n     * Worth setting on anything user-facing, and more so than for audio: a minute of\n     * 1080p at 2.5 Mbps is roughly 19 MB, so a recording left running by accident\n     * fills memory an order of magnitude faster than a voice note does.\n     */\n    maxDurationMs?: number;\n    /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n    onRecorded?: (recording: VideoRecording) => void;\n    /** How often the clock is published, in ms. Default 250. */\n    tickMs?: number;\n}\n\n/** Value returned by {@link useVideoRecorder}. */\nexport interface UseVideoRecorderResult {\n    status: MediaRecorderStatus;\n    /** Recorded length so far, excluding paused time. */\n    durationMs: number;\n    /** The finished recording, or `null` before the first `stop()`. */\n    recording: VideoRecording | null;\n    /** Recorder-level error (encoder failure, share revoked mid-recording). */\n    error: unknown;\n    /** `false` when there is no stream yet, or the browser cannot record video. */\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<VideoRecording | null>;\n    /** Stop and throw the video away. */\n    cancel: () => void;\n}\n\n/**\n * Record a video stream — from {@link useScreenCapture}, from a camera, or from a\n * canvas.\n *\n * Pass the stream in; the hook stays `ready: false` until there is one, so a page can\n * render the whole recorder UI before the user has picked a screen and simply have it\n * disabled.\n *\n * There is no level meter here, which is the one real difference from\n * `useAudioRecorder`: metering a screen share means opening an `AudioContext` on a\n * stream that usually has no audio track at all, and browsers cap the number of live\n * contexts. If you are recording a camera **and** want a level, run\n * `createLevelMeter` on the same stream yourself.\n *\n * The clock is published every `tickMs` (default 250 ms) rather than per frame: a\n * video recording UI shows `0:07`, and a hook that re-rendered its parent 60 times a\n * second to move a one-second counter would be the most expensive thing on the page.\n *\n * @param stream - The stream to record, or `null` while the picker is open.\n * @param options - See {@link UseVideoRecorderOptions}.\n *\n * @example\n * const screen = useScreenCapture();\n * const rec = useVideoRecorder(screen.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar tela</button>\n */\nexport function useVideoRecorder(\n    stream: MediaStream | null,\n    options: UseVideoRecorderOptions = {},\n): UseVideoRecorderResult {\n    const {\n        maxDurationMs,\n        onRecorded,\n        onChunk,\n        onError,\n        mimeType,\n        videoBitsPerSecond,\n        audioBitsPerSecond,\n        timesliceMs,\n        tickMs = 250,\n    } = options;\n\n    const [status, setStatus] = useState<MediaRecorderStatus>(\"idle\");\n    const [durationMs, setDurationMs] = useState(0);\n    const [recording, setRecording] = useState<VideoRecording | null>(null);\n    const [error, setError] = useState<unknown>(null);\n    const [ready, setReady] = useState(false);\n\n    const recorderRef = useRef<MediaRecorderHandle | null>(null);\n\n    const emitRecorded = useStableCallback((result: VideoRecording) => 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 for a stream, and tear it down when the stream changes.\n     *\n     * Rebuilt per stream because `MediaRecorder` is bound to the stream it was\n     * constructed with: a user who stops sharing and picks a different window hands us\n     * a new `MediaStream`, and reusing the old recorder would keep recording a surface\n     * that no longer exists. Same reason for a container or bitrate change — both are\n     * constructor arguments.\n     */\n    useEffect(() => {\n        if (!stream) {\n            setReady(false);\n            return;\n        }\n        let recorder: MediaRecorderHandle;\n        try {\n            recorder = createVideoRecorder(stream, {\n                mimeType,\n                videoBitsPerSecond,\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        setReady(true);\n        setStatus(\"idle\");\n\n        return () => {\n            if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n                recorder.cancel();\n            }\n            recorderRef.current = null;\n            setReady(false);\n        };\n    }, [\n        stream,\n        mimeType,\n        videoBitsPerSecond,\n        audioBitsPerSecond,\n        timesliceMs,\n        emitChunk,\n        emitError,\n    ]);\n\n    const stop = useCallback(async (): Promise<VideoRecording | 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        setRecording(result);\n        emitRecorded(result);\n        return result;\n    }, [emitRecorded]);\n\n    /**\n     * Publish the clock while something is running, and enforce `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 on a slide.\n     */\n    useEffect(() => {\n        if (status !== \"recording\" && status !== \"paused\") return;\n        const id = setInterval(() => {\n            const recorder = recorderRef.current;\n            if (!recorder) return;\n            const elapsed = recorder.durationMs();\n            setDurationMs(elapsed);\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        setRecording(null);\n    }, []);\n\n    return { status, durationMs, recording, error, ready, start, pause, resume, stop, cancel };\n}\n"],"mappings":"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"}