added a new Feature that allows user to pause/resume while screen recording,
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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<number | null>(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 */}
|
||||
<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 hover:bg-amber-500/15"
|
||||
: "animate-record-pulse bg-red-500/10"
|
||||
: "bg-white/5 hover:bg-white/[0.08]"
|
||||
}`}
|
||||
onClick={toggleRecording}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
@@ -455,9 +443,11 @@ export function LaunchWindow() {
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
{getIcon("stop", "text-red-400")}
|
||||
<span className="text-red-400 text-xs font-semibold tabular-nums">
|
||||
{formatTimePadded(elapsed)}
|
||||
{getIcon("stop", paused ? "text-amber-400" : "text-red-400")}
|
||||
<span
|
||||
className={`${paused ? "text-amber-400" : "text-red-400"} text-xs font-semibold tabular-nums`}
|
||||
>
|
||||
{formatTimePadded(elapsedSeconds)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -465,6 +455,17 @@ export function LaunchWindow() {
|
||||
)}
|
||||
</button>
|
||||
|
||||
{recording && (
|
||||
<Tooltip content={paused ? "Resume recording" : "Pause recording"}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={togglePaused}
|
||||
>
|
||||
{getIcon(paused ? "resume" : "pause", paused ? "text-amber-400" : "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Restart recording */}
|
||||
{recording && (
|
||||
<Tooltip content={t("tooltips.restartRecording")}>
|
||||
|
||||
+111
-10
@@ -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<string | undefined>(undefined);
|
||||
const [webcamDeviceId, setWebcamDeviceId] = useState<string | undefined>(undefined);
|
||||
@@ -97,13 +102,22 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const microphoneStream = useRef<MediaStream | null>(null);
|
||||
const webcamStream = useRef<MediaStream | null>(null);
|
||||
const mixingContext = useRef<AudioContext | null>(null);
|
||||
const startTime = useRef<number>(0);
|
||||
const recordingId = useRef<number>(0);
|
||||
const accumulatedDurationMs = useRef(0);
|
||||
const segmentStartedAt = useRef<number | null>(null);
|
||||
const finalizingRecordingId = useRef<number | null>(null);
|
||||
const allowAutoFinalize = useRef(false);
|
||||
const discardRecordingId = useRef<number | null>(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<void>((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,
|
||||
|
||||
Reference in New Issue
Block a user