From b002f2a485af147969584b5efdcd4890d69e6a35 Mon Sep 17 00:00:00 2001 From: maniesh6900 Date: Sat, 4 Apr 2026 00:56:14 +0530 Subject: [PATCH] added a new Feature that allows user to pause/resume while screen recording, --- package-lock.json | 4 +- src/components/launch/LaunchWindow.tsx | 53 +++++------ src/hooks/useScreenRecorder.ts | 121 +++++++++++++++++++++++-- 3 files changed, 140 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70e3395..fdbd6b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f1b66b8..c189b80 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,6 +1,6 @@ import { ChevronDown, Languages } from "lucide-react"; import { useEffect, useState } from "react"; -import { BsRecordCircle } from "react-icons/bs"; +import { BsPauseCircle, BsPlayCircle, BsRecordCircle } from "react-icons/bs"; import { FaRegStopCircle } from "react-icons/fa"; import { FaFolderOpen } from "react-icons/fa6"; import { FiMinus, FiX } from "react-icons/fi"; @@ -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: BsPauseCircle, size: ICON_SIZE }, + resume: { icon: BsPlayCircle, size: ICON_SIZE }, stop: { icon: FaRegStopCircle, size: ICON_SIZE }, restart: { icon: MdRestartAlt, size: ICON_SIZE }, record: { icon: BsRecordCircle, size: ICON_SIZE }, @@ -77,7 +79,10 @@ export function LaunchWindow() { const { recording, + paused, + elapsedSeconds, toggleRecording, + togglePaused, restartRecording, microphoneEnabled, setMicrophoneEnabled, @@ -90,8 +95,6 @@ export function LaunchWindow() { webcamDeviceId, setWebcamDeviceId, } = useScreenRecorder(); - const [recordingStart, setRecordingStart] = useState(null); - const [elapsed, setElapsed] = useState(0); const showMicControls = microphoneEnabled && !recording; const showWebcamControls = webcamEnabled && !recording; @@ -146,25 +149,6 @@ 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); - } - return () => { - if (timer) clearInterval(timer); - }; - }, [recording, recordingStart]); - useEffect(() => { if (!import.meta.env.DEV) { return; @@ -447,7 +431,11 @@ export function LaunchWindow() { {/* Record/Stop group */} + {recording && ( + + + + )} + {/* Restart recording */} {recording && ( diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 0c418c1..8e92962 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -41,7 +41,10 @@ const WEBCAM_TARGET_FRAME_RATE = 30; type UseScreenRecorderReturn = { recording: boolean; + paused: boolean; + elapsedSeconds: number; toggleRecording: () => void; + togglePaused: () => void; restartRecording: () => void; microphoneEnabled: boolean; setMicrophoneEnabled: (enabled: boolean) => void; @@ -85,6 +88,8 @@ 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 [elapsedSeconds, setElapsedSeconds] = useState(0); const [microphoneEnabled, setMicrophoneEnabled] = useState(false); const [microphoneDeviceId, setMicrophoneDeviceId] = useState(undefined); const [webcamDeviceId, setWebcamDeviceId] = useState(undefined); @@ -97,13 +102,22 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const microphoneStream = useRef(null); const webcamStream = useRef(null); const mixingContext = useRef(null); - const startTime = useRef(0); const recordingId = useRef(0); + const accumulatedDurationMs = useRef(0); + const segmentStartedAt = useRef(null); const finalizingRecordingId = useRef(null); const allowAutoFinalize = useRef(false); const discardRecordingId = useRef(null); const restarting = useRef(false); + const getRecordingDurationMs = useCallback(() => { + const segmentDuration = + screenRecorder.current?.recorder.state === "recording" && segmentStartedAt.current + ? Date.now() - segmentStartedAt.current + : 0; + return accumulatedDurationMs.current + segmentDuration; + }, []); + const selectMimeType = () => { const preferred = [ "video/webm;codecs=av1", @@ -202,6 +216,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { teardownMedia(); setRecording(false); + setPaused(false); + setElapsedSeconds(0); + accumulatedDurationMs.current = 0; + segmentStartedAt.current = null; window.electronAPI?.setRecordingState(false); void (async () => { @@ -273,7 +291,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } const activeWebcamRecorder = webcamRecorder.current; - const duration = Date.now() - startTime.current; + const duration = getRecordingDurationMs(); const activeRecordingId = recordingId.current; finalizeRecording( @@ -283,7 +301,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { activeRecordingId, ); - if (activeScreenRecorder.recorder.state === "recording") { + if ( + activeScreenRecorder.recorder.state === "recording" || + activeScreenRecorder.recorder.state === "paused" + ) { try { activeScreenRecorder.recorder.stop(); } catch { @@ -291,7 +312,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } if (activeWebcamRecorder) { - if (activeWebcamRecorder.recorder.state === "recording") { + if ( + activeWebcamRecorder.recorder.state === "recording" || + activeWebcamRecorder.recorder.state === "paused" + ) { try { activeWebcamRecorder.recorder.stop(); } catch { @@ -316,14 +340,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 { @@ -518,9 +548,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } recordingId.current = Date.now(); - startTime.current = recordingId.current; + accumulatedDurationMs.current = 0; + segmentStartedAt.current = Date.now(); allowAutoFinalize.current = true; setRecording(true); + setPaused(false); + setElapsedSeconds(0); window.electronAPI?.setRecordingState(true); const activeScreenRecorder = screenRecorder.current; @@ -536,7 +569,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { finalizeRecording( activeScreenRecorder, activeWebcamRecorder ?? null, - Math.max(0, Date.now() - startTime.current), + Math.max(0, getRecordingDurationMs()), activeRecordingId, ); }, @@ -552,12 +585,56 @@ export function useScreenRecorder(): UseScreenRecorderReturn { toast.error(errorMsg); } setRecording(false); + setPaused(false); + setElapsedSeconds(0); + accumulatedDurationMs.current = 0; + segmentStartedAt.current = null; screenRecorder.current = null; webcamRecorder.current = null; teardownMedia(); } }; + const togglePaused = () => { + const activeScreenRecorder = screenRecorder.current?.recorder; + if (!activeScreenRecorder || activeScreenRecorder.state === "inactive") { + return; + } + + const activeWebcamRecorder = webcamRecorder.current?.recorder; + + if (activeScreenRecorder.state === "paused") { + try { + activeScreenRecorder.resume(); + if (activeWebcamRecorder?.state === "paused") { + activeWebcamRecorder.resume(); + } + segmentStartedAt.current = Date.now(); + setPaused(false); + } catch (error) { + console.error("Failed to resume recording:", error); + } + return; + } + + if (activeScreenRecorder.state !== "recording") { + return; + } + + try { + accumulatedDurationMs.current = getRecordingDurationMs(); + segmentStartedAt.current = null; + setElapsedSeconds(Math.floor(accumulatedDurationMs.current / 1000)); + activeScreenRecorder.pause(); + if (activeWebcamRecorder?.state === "recording") { + activeWebcamRecorder.pause(); + } + setPaused(true); + } catch (error) { + console.error("Failed to pause recording:", error); + } + }; + const toggleRecording = () => { recording ? stopRecording.current() : startRecording(); }; @@ -566,7 +643,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (restarting.current) return; const activeScreenRecorder = screenRecorder.current; - if (!activeScreenRecorder || activeScreenRecorder.recorder.state !== "recording") return; + if (!activeScreenRecorder || activeScreenRecorder.recorder.state === "inactive") return; const activeWebcamRecorder = webcamRecorder.current; const activeRecordingId = recordingId.current; @@ -581,7 +658,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }), ]; - if (activeWebcamRecorder?.recorder.state === "recording") { + if ( + activeWebcamRecorder?.recorder.state === "recording" || + activeWebcamRecorder?.recorder.state === "paused" + ) { stopPromises.push( new Promise((resolve) => { activeWebcamRecorder.recorder.addEventListener("stop", () => resolve(), { @@ -601,9 +681,30 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; + useEffect(() => { + if (!recording) { + setElapsedSeconds(0); + return; + } + + setElapsedSeconds(Math.floor(getRecordingDurationMs() / 1000)); + if (paused) { + return; + } + + const interval = window.setInterval(() => { + setElapsedSeconds(Math.floor(getRecordingDurationMs() / 1000)); + }, 250); + + return () => window.clearInterval(interval); + }, [getRecordingDurationMs, paused, recording]); + return { recording, + paused, + elapsedSeconds, toggleRecording, + togglePaused, restartRecording, microphoneEnabled, setMicrophoneEnabled,