Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 73 additions & 18 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChevronDown, Languages } from "lucide-react";
import { useEffect, useState } from "react";
import { BsRecordCircle } from "react-icons/bs";
import { FaRegStopCircle } from "react-icons/fa";
import { FaPauseCircle, FaPlayCircle, FaRegStopCircle } from "react-icons/fa";
import { FaFolderOpen } from "react-icons/fa6";
import { FiMinus, FiX } from "react-icons/fi";
import {
Expand Down Expand Up @@ -41,6 +41,8 @@ const ICON_CONFIG = {
micOff: { icon: MdMicOff, size: ICON_SIZE },
webcamOn: { icon: MdVideocam, size: ICON_SIZE },
webcamOff: { icon: MdVideocamOff, size: ICON_SIZE },
pause: { icon: FaPauseCircle, size: ICON_SIZE },
resume: { icon: FaPlayCircle, size: ICON_SIZE },
stop: { icon: FaRegStopCircle, size: ICON_SIZE },
restart: { icon: MdRestartAlt, size: ICON_SIZE },
record: { icon: BsRecordCircle, size: ICON_SIZE },
Expand Down Expand Up @@ -77,7 +79,10 @@ export function LaunchWindow() {

const {
recording,
paused,
toggleRecording,
pauseRecording,
resumeRecording,
restartRecording,
microphoneEnabled,
setMicrophoneEnabled,
Expand All @@ -93,6 +98,9 @@ export function LaunchWindow() {
const [recordingStart, setRecordingStart] = useState<number | null>(null);
const [elapsed, setElapsed] = useState(0);

const [pausedAt, setPausedAt] = useState<number | null>(null);
const [pausedTotalMs, setPausedTotalMs] = useState(0);

const showMicControls = microphoneEnabled && !recording;
const showWebcamControls = webcamEnabled && !recording;

Expand Down Expand Up @@ -147,24 +155,48 @@ export function LaunchWindow() {
}, [selectedCameraId, setWebcamDeviceId]);

useEffect(() => {
let timer: NodeJS.Timeout | null = null;
if (recording) {
if (!recordingStart) setRecordingStart(Date.now());
timer = setInterval(() => {
if (recordingStart) {
setElapsed(Math.floor((Date.now() - recordingStart) / 1000));
}
}, 1000);
} else {
setRecordingStart(null);
setElapsed(0);
if (timer) clearInterval(timer);
if (!recordingStart) {
setRecordingStart(Date.now());
setElapsed(0);
}
return;
}
return () => {
if (timer) clearInterval(timer);
};

setRecordingStart(null);
setElapsed(0);
setPausedAt(null);
setPausedTotalMs(0);
}, [recording, recordingStart]);

useEffect(() => {
if (!recording || !recordingStart) return;

if (paused) {
if (!pausedAt) {
setPausedAt(Date.now());
}
return;
}

if (pausedAt) {
setPausedTotalMs((prev) => prev + (Date.now() - pausedAt));
setPausedAt(null);
}
}, [recording, paused, recordingStart, pausedAt]);

useEffect(() => {
if (!recording || !recordingStart || paused) return;

const timer = setInterval(() => {
const livePaused = pausedAt ? Date.now() - pausedAt : 0;
const effectiveMs = Math.max(0, Date.now() - recordingStart - pausedTotalMs - livePaused);
setElapsed(Math.floor(effectiveMs / 1000));
}, 250);

return () => clearInterval(timer);
}, [recording, paused, recordingStart, pausedAt, pausedTotalMs]);

useEffect(() => {
if (!import.meta.env.DEV) {
return;
Expand Down Expand Up @@ -274,7 +306,10 @@ export function LaunchWindow() {
onMouseLeave={() => setIsMicHovered(false)}
onFocus={() => setIsMicFocused(true)}
onBlur={() => setIsMicFocused(false)}
style={{ width: micExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
style={{
width: micExpanded ? "240px" : "140px",
transition: "width 300ms ease",
}}
>
<div className="relative flex-1 min-w-0">
{!micExpanded && (
Expand Down Expand Up @@ -318,7 +353,10 @@ export function LaunchWindow() {
onMouseLeave={() => setIsWebcamHovered(false)}
onFocus={() => setIsWebcamFocused(true)}
onBlur={() => setIsWebcamFocused(false)}
style={{ width: webcamExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
style={{
width: webcamExpanded ? "240px" : "140px",
transition: "width 300ms ease",
}}
>
<div className="relative flex-1 min-w-0">
{!webcamExpanded && (
Expand Down Expand Up @@ -444,10 +482,27 @@ export function LaunchWindow() {
</button>
</div>

{/* Resume/Pause recording */}
{recording && (
<Tooltip content={paused ? "Resume recording" : "Pause recording"}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={paused ? resumeRecording : pauseRecording}
style={{ flex: "0 0 auto" }}
>
{paused ? getIcon("resume", "text-amber-300") : getIcon("pause", "text-amber-300")}
</button>
</Tooltip>
)}

{/* Record/Stop group */}
<button
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
recording
? paused
? "bg-amber-500/10"
: "animate-record-pulse bg-red-500/10"
: "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={toggleRecording}
disabled={!hasSelectedSource && !recording}
Expand Down
74 changes: 68 additions & 6 deletions src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ const WEBCAM_TARGET_FRAME_RATE = 30;

type UseScreenRecorderReturn = {
recording: boolean;
paused: boolean;
toggleRecording: () => void;
pauseRecording: () => void;
resumeRecording: () => void;
restartRecording: () => void;
microphoneEnabled: boolean;
setMicrophoneEnabled: (enabled: boolean) => void;
Expand Down Expand Up @@ -85,6 +88,7 @@ function createRecorderHandle(stream: MediaStream, options: MediaRecorderOptions
export function useScreenRecorder(): UseScreenRecorderReturn {
const t = useScopedT("editor");
const [recording, setRecording] = useState(false);
const [paused, setPaused] = useState(false);
const [microphoneEnabled, setMicrophoneEnabled] = useState(false);
const [microphoneDeviceId, setMicrophoneDeviceId] = useState<string | undefined>(undefined);
const [webcamDeviceId, setWebcamDeviceId] = useState<string | undefined>(undefined);
Expand All @@ -98,6 +102,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const webcamStream = useRef<MediaStream | null>(null);
const mixingContext = useRef<AudioContext | null>(null);
const startTime = useRef<number>(0);
const pausedStartedAt = useRef<number | null>(null);
const pausedDurationMs = useRef(0);
const recordingId = useRef<number>(0);
const finalizingRecordingId = useRef<number | null>(null);
const allowAutoFinalize = useRef(false);
Expand Down Expand Up @@ -200,8 +206,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamRecorder.current = null;
}

pausedStartedAt.current = null;
pausedDurationMs.current = 0;

teardownMedia();
setRecording(false);
setPaused(false);
window.electronAPI?.setRecordingState(false);

void (async () => {
Expand Down Expand Up @@ -273,7 +283,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}

const activeWebcamRecorder = webcamRecorder.current;
const duration = Date.now() - startTime.current;

if (pausedStartedAt.current) {
pausedDurationMs.current += Date.now() - pausedStartedAt.current;
pausedStartedAt.current = null;
}

const duration = Date.now() - startTime.current - pausedDurationMs.current;
const activeRecordingId = recordingId.current;

finalizeRecording(
Expand All @@ -283,15 +299,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
activeRecordingId,
);

if (activeScreenRecorder.recorder.state === "recording") {
if (
activeScreenRecorder.recorder.state === "recording" ||
activeScreenRecorder.recorder.state === "paused"
) {
try {
activeScreenRecorder.recorder.stop();
} catch {
// Recorder may already be stopping.
}
}
if (activeWebcamRecorder) {
if (activeWebcamRecorder.recorder.state === "recording") {
if (
activeWebcamRecorder.recorder.state === "recording" ||
activeWebcamRecorder.recorder.state === "paused"
) {
try {
activeWebcamRecorder.recorder.stop();
} catch {
Expand All @@ -316,14 +338,20 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
restarting.current = false;
discardRecordingId.current = null;

if (screenRecorder.current?.recorder.state === "recording") {
if (
screenRecorder.current?.recorder.state === "recording" ||
screenRecorder.current?.recorder.state === "paused"
) {
try {
screenRecorder.current.recorder.stop();
} catch {
// Ignore recorder teardown errors during cleanup.
}
}
if (webcamRecorder.current?.recorder.state === "recording") {
if (
webcamRecorder.current?.recorder.state === "recording" ||
webcamRecorder.current?.recorder.state === "paused"
) {
try {
webcamRecorder.current.recorder.stop();
} catch {
Expand Down Expand Up @@ -519,8 +547,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {

recordingId.current = Date.now();
startTime.current = recordingId.current;
pausedStartedAt.current = null;
pausedDurationMs.current = 0;
allowAutoFinalize.current = true;
setRecording(true);
setPaused(false);
window.electronAPI?.setRecordingState(true);

const activeScreenRecorder = screenRecorder.current;
Expand All @@ -536,7 +567,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
finalizeRecording(
activeScreenRecorder,
activeWebcamRecorder ?? null,
Math.max(0, Date.now() - startTime.current),
Math.max(0, Date.now() - startTime.current - pausedDurationMs.current),
activeRecordingId,
);
},
Expand All @@ -552,12 +583,40 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
toast.error(errorMsg);
}
setRecording(false);
setPaused(false);
pausedStartedAt.current = null;
pausedDurationMs.current = 0;
screenRecorder.current = null;
webcamRecorder.current = null;
teardownMedia();
}
};

const pauseRecording = () => {
if (screenRecorder.current?.recorder.state === "recording") {
screenRecorder.current.recorder.pause();
if (webcamRecorder.current?.recorder.state === "recording") {
webcamRecorder.current.recorder.pause();
}
pausedStartedAt.current = Date.now();
setPaused(true);
}
};

const resumeRecording = () => {
if (screenRecorder.current?.recorder.state === "paused") {
if (pausedStartedAt.current) {
pausedDurationMs.current += Date.now() - pausedStartedAt.current;
pausedStartedAt.current = null;
}
if (webcamRecorder.current?.recorder.state === "paused") {
webcamRecorder.current.recorder.resume();
}
screenRecorder.current.recorder.resume();
setPaused(false);
}
};

const toggleRecording = () => {
recording ? stopRecording.current() : startRecording();
};
Expand Down Expand Up @@ -603,7 +662,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {

return {
recording,
paused,
toggleRecording,
pauseRecording,
resumeRecording,
restartRecording,
microphoneEnabled,
setMicrophoneEnabled,
Expand Down