import { useEffect, useRef, useState } from "react"; import { Monitor, Square, MousePointer2 } from "lucide-react"; import { useStore } from "../store"; import { startScreenCapture, type CaptureController, type CaptureResult, type CaptureQuality } from "../lib/screenCapture"; import { ZOOM_STYLES, type ZoomStyle } from "../lib/screencastZoom"; // Record the screen (preferring this tab) → upload as a video asset → drop it on the // timeline at the playhead, carrying the captured screencast metadata so the renderer // can drive the auto-zoom "explainer" camera. Mirrors VoiceRecorder's upload + place flow. export function ScreenRecorder({ onClose }: { onClose: () => void }) { const projectId = useStore((s) => s.projectId); const addAsset = useStore((s) => s.addAsset); const [state, setState] = useState<"idle" | "recording" | "saving">("idle"); const [elapsed, setElapsed] = useState(0); const [err, setErr] = useState(""); const [quality, setQuality] = useState("1440p"); const [fps, setFps] = useState<30 | 60>(30); // how eagerly the explainer camera zooms — default "calm" so it commits to a few // meaningful shots instead of pushing in on every click const [cameraStyle, setCameraStyle] = useState("calm"); // default OFF: keep the user's REAL recorded cursor and only paint click effects on it const [animatedCursor, setAnimatedCursor] = useState(false); const ctrl = useRef(null); const startedAt = useRef(0); const timer = useRef | null>(null); const release = () => { ctrl.current?.cancel(); ctrl.current = null; }; // teardown (unmount / Cancel / re-toggle): stop the timer and free the capture, discarding any in-flight take useEffect(() => () => { if (timer.current) clearInterval(timer.current); release(); }, []); async function start() { setErr(""); try { const c = await startScreenCapture({ quality, frameRate: fps, animatedCursor, cameraStyle }); ctrl.current = c; startedAt.current = Date.now(); setState("recording"); timer.current = setInterval(() => setElapsed(Date.now() - startedAt.current), 200); // if the user ends sharing via the browser UI, the controller auto-stops — but the // resulting CaptureResult is only delivered through our own stop(), so we don't await it here. } catch (e) { release(); setErr(e instanceof Error && e.name === "NotAllowedError" ? "Screen-share permission denied." : "Couldn't start screen capture."); } } async function stop() { if (timer.current) clearInterval(timer.current); const c = ctrl.current; if (!c) { onClose(); return; } setState("saving"); try { const result = await c.stop(); ctrl.current = null; await upload(result); } catch (e) { setErr(e instanceof Error ? e.message : "Couldn't save the recording."); setState("idle"); } } async function upload(result: CaptureResult) { if (!projectId || result.blob.size === 0) { onClose(); return; } try { const name = `Screen recording ${new Date().toLocaleTimeString()}.webm`; const measured = Math.max(500, Math.round(result.durationMs)); const res = await fetch(`/api/projects/${projectId}/assets?name=${encodeURIComponent(name)}&kind=video&durationMs=${measured}`, { method: "POST", headers: { "Content-Type": result.blob.type || "video/webm" }, body: result.blob }); const a = await res.json(); if (!a.id) throw new Error(a.error || "upload failed"); const asset = { ...a, kind: "video" as const, durationMs: measured, width: a.width || result.width, height: a.height || result.height }; addAsset(asset); // place at the playhead exactly like an imported video; placeAsset returns the new clip id const newClipId = useStore.getState().placeAsset(asset.id, useStore.getState().playheadMs); // attach the captured screencast metadata so the explainer camera can render if (newClipId) useStore.getState().setScreencast(newClipId, result.screencast); // requires store.setScreencast onClose(); } catch (e) { setErr(e instanceof Error ? e.message : "Couldn't save the recording."); setState("idle"); } } const secs = (elapsed / 1000).toFixed(1); return (
{state === "idle" && <>Record your screen — adds an auto-zoom explainer camera } {state === "recording" && <> Recording {secs}s } {state === "saving" && Saving…} {err && {err}}
); }