mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 08:15:42 +00:00
- tests driven
- language support - webcame pause/resume sync - native recording pause - cancel (clears chunks and stops both recoders and webcame discarded on native cancel - full lifecycle with webcame - pause - resume - stop keeps webcame in sync throughout
This commit is contained in:
Vendored
+3
@@ -121,6 +121,9 @@ interface Window {
|
||||
}>;
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>;
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>;
|
||||
deleteRecordingFile: (
|
||||
filePath: string,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
saveProjectFile: (
|
||||
projectData: unknown,
|
||||
suggestedName?: string,
|
||||
|
||||
@@ -3139,6 +3139,25 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-recording-file', async (_, filePath: string) => {
|
||||
try {
|
||||
if (!filePath || !isAutoRecordingPath(filePath)) {
|
||||
return { success: false, error: 'Only auto-generated recordings can be deleted' };
|
||||
}
|
||||
await fs.unlink(filePath);
|
||||
// Also delete the cursor telemetry sidecar if it exists
|
||||
const telemetryPath = getTelemetryPathForVideo(filePath);
|
||||
await fs.unlink(telemetryPath).catch(() => {});
|
||||
if (currentVideoPath === filePath) {
|
||||
currentVideoPath = null;
|
||||
currentRecordingSession = null;
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-platform', () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
@@ -150,6 +150,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
clearCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke("clear-current-video-path");
|
||||
},
|
||||
deleteRecordingFile: (filePath: string) => {
|
||||
return ipcRenderer.invoke("delete-recording-file", filePath);
|
||||
},
|
||||
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
||||
return ipcRenderer.invoke("save-project-file", projectData, suggestedName, existingProjectPath);
|
||||
},
|
||||
|
||||
@@ -489,7 +489,7 @@ export function LaunchWindow() {
|
||||
<>
|
||||
{screenSources.length > 0 && (
|
||||
<>
|
||||
<div className={styles.ddLabel}>Screens</div>
|
||||
<div className={styles.ddLabel}>{t("recording.screens")}</div>
|
||||
{screenSources.map((source) => (
|
||||
<DropdownItem
|
||||
key={source.id}
|
||||
@@ -505,7 +505,7 @@ export function LaunchWindow() {
|
||||
{windowSources.length > 0 && (
|
||||
<>
|
||||
<div className={styles.ddLabel} style={screenSources.length > 0 ? { marginTop: 4 } : undefined}>
|
||||
Windows
|
||||
{t("recording.windows")}
|
||||
</div>
|
||||
{windowSources.map((source) => (
|
||||
<DropdownItem
|
||||
@@ -523,7 +523,7 @@ export function LaunchWindow() {
|
||||
)}
|
||||
{screenSources.length === 0 && windowSources.length === 0 && (
|
||||
<div className="text-center text-xs text-[#6b6b78] py-4">
|
||||
No sources found
|
||||
{t("recording.noSourcesFound")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -533,18 +533,18 @@ export function LaunchWindow() {
|
||||
|
||||
{activeDropdown === "mic" && (
|
||||
<>
|
||||
<div className={styles.ddLabel}>Microphone</div>
|
||||
<div className={styles.ddLabel}>{t("recording.microphone")}</div>
|
||||
{microphoneEnabled && (
|
||||
<DropdownItem
|
||||
icon={<MicOff size={16} />}
|
||||
onClick={() => { setMicrophoneEnabled(false); setActiveDropdown("none"); }}
|
||||
>
|
||||
Turn Off Microphone
|
||||
{t("recording.turnOffMicrophone")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{!microphoneEnabled && (
|
||||
<div className="px-3 py-2 text-xs text-[#6b6b78]">
|
||||
Select a microphone to enable
|
||||
{t("recording.selectMicToEnable")}
|
||||
</div>
|
||||
)}
|
||||
{devices.map((device) => (
|
||||
@@ -561,7 +561,7 @@ export function LaunchWindow() {
|
||||
))}
|
||||
{devices.length === 0 && (
|
||||
<div className="text-center text-xs text-[#6b6b78] py-4">
|
||||
No microphones found
|
||||
{t("recording.noMicrophonesFound")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -569,18 +569,18 @@ export function LaunchWindow() {
|
||||
|
||||
{activeDropdown === "webcam" && (
|
||||
<>
|
||||
<div className={styles.ddLabel}>Webcam</div>
|
||||
<div className={styles.ddLabel}>{t("recording.webcam")}</div>
|
||||
{webcamEnabled && (
|
||||
<DropdownItem
|
||||
icon={<VideoOff size={16} />}
|
||||
onClick={() => { setWebcamEnabled(false); setActiveDropdown("none"); }}
|
||||
>
|
||||
Turn Off Webcam
|
||||
{t("recording.turnOffWebcam")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{!webcamEnabled && (
|
||||
<div className="px-3 py-2 text-xs text-[#6b6b78]">
|
||||
Select a webcam to enable
|
||||
{t("recording.selectWebcamToEnable")}
|
||||
</div>
|
||||
)}
|
||||
{showWebcamControls && (
|
||||
@@ -612,7 +612,7 @@ export function LaunchWindow() {
|
||||
))}
|
||||
{videoDevices.length === 0 && (
|
||||
<div className="text-center text-xs text-[#6b6b78] py-4">
|
||||
No webcams found
|
||||
{t("recording.noWebcamsFound")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -637,7 +637,7 @@ export function LaunchWindow() {
|
||||
{activeDropdown === "more" && (
|
||||
<>
|
||||
<DropdownItem icon={<FolderOpen size={16} />} onClick={chooseRecordingsDirectory}>
|
||||
Recordings Folder
|
||||
{t("recording.recordingsFolder")}
|
||||
</DropdownItem>
|
||||
<DropdownItem icon={<VideoIcon size={16} />} onClick={openVideoFile}>
|
||||
{t("recording.openVideoFile")}
|
||||
@@ -645,7 +645,7 @@ export function LaunchWindow() {
|
||||
<DropdownItem icon={<FolderOpen size={16} />} onClick={openProjectFile}>
|
||||
{t("recording.openProject")}
|
||||
</DropdownItem>
|
||||
<div className={styles.ddLabel} style={{ marginTop: 4 }}>Language</div>
|
||||
<div className={styles.ddLabel} style={{ marginTop: 4 }}>{t("recording.language")}</div>
|
||||
{SUPPORTED_LOCALES.map((code) => (
|
||||
<DropdownItem
|
||||
key={code}
|
||||
@@ -674,7 +674,7 @@ export function LaunchWindow() {
|
||||
<div className="flex items-center gap-[5px]">
|
||||
<div className={`w-[7px] h-[7px] rounded-full ${paused ? "bg-[#fbbf24]" : `bg-[#f43f5e] ${styles.recDotBlink}`}`} />
|
||||
<span className={`text-[10px] font-bold tracking-[0.06em] ${paused ? "text-[#fbbf24]" : "text-[#f43f5e]"}`}>
|
||||
{paused ? "PAUSED" : "REC"}
|
||||
{paused ? t("recording.paused") : t("recording.rec")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -690,15 +690,15 @@ export function LaunchWindow() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<IconButton onClick={paused ? resumeRecording : pauseRecording} title={paused ? "Resume" : "Pause"} className={paused ? styles.ibGreen : ""}>
|
||||
<IconButton onClick={paused ? resumeRecording : pauseRecording} title={paused ? t("recording.resume") : t("recording.pause")} className={paused ? styles.ibGreen : ""}>
|
||||
{paused ? <Play size={18} fill="currentColor" strokeWidth={0} /> : <Pause size={18} />}
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={toggleRecording} title="Stop" className={styles.ibRed}>
|
||||
<IconButton onClick={toggleRecording} title={t("recording.stop")} className={styles.ibRed}>
|
||||
<Square size={16} fill="currentColor" strokeWidth={0} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={cancelRecording} title="Cancel">
|
||||
<IconButton onClick={cancelRecording} title={t("recording.cancel")}>
|
||||
<X size={18} />
|
||||
</IconButton>
|
||||
</>
|
||||
@@ -772,7 +772,7 @@ export function LaunchWindow() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<IconButton onClick={() => toggleDropdown("more")} title="More">
|
||||
<IconButton onClick={() => toggleDropdown("more")} title={t("recording.more")}>
|
||||
<MoreVertical size={18} />
|
||||
</IconButton>
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* Tests for the MediaRecorder pause/stop state machine logic
|
||||
* extracted from useScreenRecorder.
|
||||
*
|
||||
* These verify that:
|
||||
* - Stop works from both "recording" and "paused" states
|
||||
* - Resume is called before stop when stopping from paused state
|
||||
* - Pause is a no-op when already paused or not recording
|
||||
* - Resume is a no-op when not paused
|
||||
*/
|
||||
type RecordingState = "inactive" | "recording" | "paused";
|
||||
|
||||
function createMockMediaRecorder(initialState: RecordingState = "inactive") {
|
||||
let _state: RecordingState = initialState;
|
||||
@@ -32,15 +23,15 @@ function createMockMediaRecorder(initialState: RecordingState = "inactive") {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted state machine logic matching useScreenRecorder's stopRecording,
|
||||
* pauseRecording, and resumeRecording implementations.
|
||||
*/
|
||||
function stopRecording(
|
||||
recorder: ReturnType<typeof createMockMediaRecorder>,
|
||||
isNativeRecording: boolean,
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
) {
|
||||
if (isNativeRecording) {
|
||||
if (webcamRecorder && webcamRecorder.state !== "inactive") {
|
||||
webcamRecorder.stop();
|
||||
}
|
||||
return { stopped: true, wasNative: true };
|
||||
}
|
||||
|
||||
@@ -49,6 +40,9 @@ function stopRecording(
|
||||
if (recorderState === "paused") {
|
||||
recorder.resume();
|
||||
}
|
||||
if (webcamRecorder && webcamRecorder.state !== "inactive") {
|
||||
webcamRecorder.stop();
|
||||
}
|
||||
recorder.stop();
|
||||
return { stopped: true, wasNative: false };
|
||||
}
|
||||
@@ -60,11 +54,20 @@ function pauseRecording(
|
||||
recording: boolean,
|
||||
paused: boolean,
|
||||
isNativeRecording: boolean,
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): boolean {
|
||||
if (!recording || paused) return false;
|
||||
if (isNativeRecording) return false;
|
||||
if (isNativeRecording) {
|
||||
if (webcamRecorder?.state === "recording") {
|
||||
webcamRecorder.pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (recorder.state === "recording") {
|
||||
recorder.pause();
|
||||
if (webcamRecorder?.state === "recording") {
|
||||
webcamRecorder.pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -74,15 +77,49 @@ function resumeRecording(
|
||||
recorder: ReturnType<typeof createMockMediaRecorder>,
|
||||
recording: boolean,
|
||||
paused: boolean,
|
||||
isNativeRecording: boolean,
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
): boolean {
|
||||
if (!recording || !paused) return false;
|
||||
if (isNativeRecording) {
|
||||
if (webcamRecorder?.state === "paused") {
|
||||
webcamRecorder.resume();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (recorder.state === "paused") {
|
||||
recorder.resume();
|
||||
if (webcamRecorder?.state === "paused") {
|
||||
webcamRecorder.resume();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function cancelRecording(
|
||||
recorder: ReturnType<typeof createMockMediaRecorder>,
|
||||
isNativeRecording: boolean,
|
||||
chunks: { current: Blob[] },
|
||||
webcamRecorder?: ReturnType<typeof createMockMediaRecorder> | null,
|
||||
webcamChunks?: { current: Blob[] },
|
||||
) {
|
||||
if (webcamChunks) webcamChunks.current = [];
|
||||
if (webcamRecorder && webcamRecorder.state !== "inactive") {
|
||||
webcamRecorder.stop();
|
||||
}
|
||||
|
||||
if (isNativeRecording) {
|
||||
return { cancelled: true, wasNative: true };
|
||||
}
|
||||
|
||||
chunks.current = [];
|
||||
if (recorder.state !== "inactive") {
|
||||
recorder.stop();
|
||||
}
|
||||
return { cancelled: true, wasNative: false };
|
||||
}
|
||||
|
||||
describe("useScreenRecorder state machine", () => {
|
||||
let recorder: ReturnType<typeof createMockMediaRecorder>;
|
||||
|
||||
@@ -143,6 +180,24 @@ describe("useScreenRecorder state machine", () => {
|
||||
expect(result.wasNative).toBe(true);
|
||||
expect(recorder.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops webcam when stopping browser recording", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
stopRecording(recorder, false, webcam);
|
||||
|
||||
expect(webcam.stop).toHaveBeenCalled();
|
||||
expect(webcam.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("stops webcam when stopping native recording", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
stopRecording(recorder, true, webcam);
|
||||
|
||||
expect(webcam.stop).toHaveBeenCalled();
|
||||
expect(webcam.state).toBe("inactive");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pauseRecording", () => {
|
||||
@@ -171,11 +226,36 @@ describe("useScreenRecorder state machine", () => {
|
||||
expect(recorder.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing for native recordings", () => {
|
||||
it("allows pause for native recordings", () => {
|
||||
const result = pauseRecording(recorder, true, false, true);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(recorder.pause).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("pauses webcam alongside browser recording", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
pauseRecording(recorder, true, false, false, webcam);
|
||||
|
||||
expect(recorder.state).toBe("paused");
|
||||
expect(webcam.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("pauses webcam during native recording pause", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
const result = pauseRecording(recorder, true, false, true, webcam);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(webcam.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("skips webcam pause when webcam is not recording", () => {
|
||||
const webcam = createMockMediaRecorder("inactive");
|
||||
|
||||
pauseRecording(recorder, true, false, false, webcam);
|
||||
|
||||
expect(webcam.pause).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,7 +263,7 @@ describe("useScreenRecorder state machine", () => {
|
||||
it("resumes a paused recording", () => {
|
||||
recorder.pause();
|
||||
|
||||
const result = resumeRecording(recorder, true, true);
|
||||
const result = resumeRecording(recorder, true, true, false);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(recorder.resume).toHaveBeenCalled();
|
||||
@@ -191,21 +271,108 @@ describe("useScreenRecorder state machine", () => {
|
||||
});
|
||||
|
||||
it("does nothing when not paused", () => {
|
||||
const result = resumeRecording(recorder, true, false);
|
||||
const result = resumeRecording(recorder, true, false, false);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(recorder.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing when not recording", () => {
|
||||
const result = resumeRecording(recorder, false, true);
|
||||
const result = resumeRecording(recorder, false, true, false);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("resumes webcam alongside browser recording", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
recorder.pause();
|
||||
webcam.pause();
|
||||
|
||||
resumeRecording(recorder, true, true, false, webcam);
|
||||
|
||||
expect(recorder.state).toBe("recording");
|
||||
expect(webcam.state).toBe("recording");
|
||||
});
|
||||
|
||||
it("resumes webcam during native recording resume", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
webcam.pause();
|
||||
|
||||
const result = resumeRecording(recorder, true, true, true, webcam);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(webcam.state).toBe("recording");
|
||||
});
|
||||
|
||||
it("skips webcam resume when webcam is not paused", () => {
|
||||
recorder.pause();
|
||||
const webcam = createMockMediaRecorder("inactive");
|
||||
|
||||
resumeRecording(recorder, true, true, false, webcam);
|
||||
|
||||
expect(webcam.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelRecording", () => {
|
||||
it("clears chunks and stops browser recording", () => {
|
||||
const chunks = { current: [new Blob(["data"])] };
|
||||
|
||||
const result = cancelRecording(recorder, false, chunks);
|
||||
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(result.wasNative).toBe(false);
|
||||
expect(chunks.current).toEqual([]);
|
||||
expect(recorder.stop).toHaveBeenCalled();
|
||||
expect(recorder.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("clears webcam chunks and stops webcam on cancel", () => {
|
||||
const chunks = { current: [new Blob(["data"])] };
|
||||
const webcamChunks = { current: [new Blob(["cam"])] };
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
cancelRecording(recorder, false, chunks, webcam, webcamChunks);
|
||||
|
||||
expect(webcamChunks.current).toEqual([]);
|
||||
expect(webcam.stop).toHaveBeenCalled();
|
||||
expect(webcam.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("stops webcam when cancelling native recording", () => {
|
||||
const chunks = { current: [] as Blob[] };
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
const result = cancelRecording(recorder, true, chunks, webcam);
|
||||
|
||||
expect(result.wasNative).toBe(true);
|
||||
expect(webcam.stop).toHaveBeenCalled();
|
||||
expect(recorder.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles cancel when recorder is already inactive", () => {
|
||||
const inactiveRecorder = createMockMediaRecorder("inactive");
|
||||
const chunks = { current: [new Blob(["data"])] };
|
||||
|
||||
const result = cancelRecording(inactiveRecorder, false, chunks);
|
||||
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(chunks.current).toEqual([]);
|
||||
expect(inactiveRecorder.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles cancel when webcam is already inactive", () => {
|
||||
const chunks = { current: [] as Blob[] };
|
||||
const webcam = createMockMediaRecorder("inactive");
|
||||
|
||||
cancelRecording(recorder, false, chunks, webcam);
|
||||
|
||||
expect(webcam.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pause → stop → editor flow", () => {
|
||||
it("full lifecycle: record → pause → stop completes cleanly", () => {
|
||||
it("record → pause → stop completes cleanly", () => {
|
||||
expect(recorder.state).toBe("recording");
|
||||
|
||||
pauseRecording(recorder, true, false, false);
|
||||
@@ -216,18 +383,59 @@ describe("useScreenRecorder state machine", () => {
|
||||
expect(recorder.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("full lifecycle: record → pause → resume → stop completes cleanly", () => {
|
||||
it("record → pause → resume → stop completes cleanly", () => {
|
||||
expect(recorder.state).toBe("recording");
|
||||
|
||||
pauseRecording(recorder, true, false, false);
|
||||
expect(recorder.state).toBe("paused");
|
||||
|
||||
resumeRecording(recorder, true, true);
|
||||
resumeRecording(recorder, true, true, false);
|
||||
expect(recorder.state).toBe("recording");
|
||||
|
||||
const result = stopRecording(recorder, false);
|
||||
expect(result.stopped).toBe(true);
|
||||
expect(recorder.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("webcam stays in sync through full pause/resume/stop cycle", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
pauseRecording(recorder, true, false, false, webcam);
|
||||
expect(recorder.state).toBe("paused");
|
||||
expect(webcam.state).toBe("paused");
|
||||
|
||||
resumeRecording(recorder, true, true, false, webcam);
|
||||
expect(recorder.state).toBe("recording");
|
||||
expect(webcam.state).toBe("recording");
|
||||
|
||||
stopRecording(recorder, false, webcam);
|
||||
expect(recorder.state).toBe("inactive");
|
||||
expect(webcam.state).toBe("inactive");
|
||||
});
|
||||
|
||||
it("native recording: webcam pauses/resumes while screen keeps capturing", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
|
||||
pauseRecording(recorder, true, false, true, webcam);
|
||||
expect(webcam.state).toBe("paused");
|
||||
expect(recorder.pause).not.toHaveBeenCalled();
|
||||
|
||||
resumeRecording(recorder, true, true, true, webcam);
|
||||
expect(webcam.state).toBe("recording");
|
||||
expect(recorder.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancel discards both screen and webcam recordings", () => {
|
||||
const webcam = createMockMediaRecorder("recording");
|
||||
const chunks = { current: [new Blob(["screen"])] };
|
||||
const webcamChunks = { current: [new Blob(["cam"])] };
|
||||
|
||||
cancelRecording(recorder, false, chunks, webcam, webcamChunks);
|
||||
|
||||
expect(chunks.current).toEqual([]);
|
||||
expect(webcamChunks.current).toEqual([]);
|
||||
expect(recorder.state).toBe("inactive");
|
||||
expect(webcam.state).toBe("inactive");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -750,18 +750,37 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
const pauseRecording = useCallback(() => {
|
||||
if (!recording || paused) return;
|
||||
// Native recordings (macOS SCK / Windows WGC) don't support pause yet
|
||||
if (nativeScreenRecording.current) return;
|
||||
if (nativeScreenRecording.current) {
|
||||
// Native captures cannot truly pause, but we pause the timer/UI and webcam
|
||||
if (webcamRecorder.current?.state === "recording") {
|
||||
webcamRecorder.current.pause();
|
||||
}
|
||||
setPaused(true);
|
||||
return;
|
||||
}
|
||||
if (mediaRecorder.current?.state === "recording") {
|
||||
mediaRecorder.current.pause();
|
||||
if (webcamRecorder.current?.state === "recording") {
|
||||
webcamRecorder.current.pause();
|
||||
}
|
||||
setPaused(true);
|
||||
}
|
||||
}, [recording, paused]);
|
||||
|
||||
const resumeRecording = useCallback(() => {
|
||||
if (!recording || !paused) return;
|
||||
if (nativeScreenRecording.current) {
|
||||
if (webcamRecorder.current?.state === "paused") {
|
||||
webcamRecorder.current.resume();
|
||||
}
|
||||
setPaused(false);
|
||||
return;
|
||||
}
|
||||
if (mediaRecorder.current?.state === "paused") {
|
||||
mediaRecorder.current.resume();
|
||||
if (webcamRecorder.current?.state === "paused") {
|
||||
webcamRecorder.current.resume();
|
||||
}
|
||||
setPaused(false);
|
||||
}
|
||||
}, [recording, paused]);
|
||||
@@ -770,12 +789,31 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (!recording) return;
|
||||
setPaused(false);
|
||||
|
||||
// Discard webcam recording regardless of recording mode
|
||||
webcamChunks.current = [];
|
||||
if (webcamRecorder.current && webcamRecorder.current.state !== "inactive") {
|
||||
webcamRecorder.current.stop();
|
||||
}
|
||||
webcamRecorder.current = null;
|
||||
webcamStream.current?.getTracks().forEach((t) => t.stop());
|
||||
webcamStream.current = null;
|
||||
pendingWebcamPathPromise.current = null;
|
||||
|
||||
if (nativeScreenRecording.current) {
|
||||
nativeScreenRecording.current = false;
|
||||
wgcRecording.current = false;
|
||||
setRecording(false);
|
||||
window.electronAPI?.setRecordingState(false);
|
||||
void window.electronAPI.stopNativeScreenRecording();
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.stopNativeScreenRecording();
|
||||
if (result?.path) {
|
||||
await window.electronAPI.deleteRecordingFile(result.path);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,27 @@
|
||||
"hideHudFromVideo": "Hide HUD from recording",
|
||||
"showHudInVideo": "Show HUD in recording",
|
||||
"hideHud": "Hide HUD",
|
||||
"closeApp": "Close App"
|
||||
"closeApp": "Close App",
|
||||
"screens": "Screens",
|
||||
"windows": "Windows",
|
||||
"noSourcesFound": "No sources found",
|
||||
"microphone": "Microphone",
|
||||
"turnOffMicrophone": "Turn Off Microphone",
|
||||
"selectMicToEnable": "Select a microphone to enable",
|
||||
"noMicrophonesFound": "No microphones found",
|
||||
"webcam": "Webcam",
|
||||
"turnOffWebcam": "Turn Off Webcam",
|
||||
"selectWebcamToEnable": "Select a webcam to enable",
|
||||
"noWebcamsFound": "No webcams found",
|
||||
"recordingsFolder": "Recordings Folder",
|
||||
"language": "Language",
|
||||
"paused": "PAUSED",
|
||||
"rec": "REC",
|
||||
"resume": "Resume",
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"cancel": "Cancel",
|
||||
"more": "More"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Loading sources...",
|
||||
|
||||
@@ -17,7 +17,27 @@
|
||||
"hideHudFromVideo": "Ocultar HUD en la grabación",
|
||||
"showHudInVideo": "Mostrar HUD en la grabación",
|
||||
"hideHud": "Ocultar HUD",
|
||||
"closeApp": "Cerrar aplicación"
|
||||
"closeApp": "Cerrar aplicación",
|
||||
"screens": "Pantallas",
|
||||
"windows": "Ventanas",
|
||||
"noSourcesFound": "No se encontraron fuentes",
|
||||
"microphone": "Micrófono",
|
||||
"turnOffMicrophone": "Desactivar micrófono",
|
||||
"selectMicToEnable": "Selecciona un micrófono para activar",
|
||||
"noMicrophonesFound": "No se encontraron micrófonos",
|
||||
"webcam": "Cámara",
|
||||
"turnOffWebcam": "Desactivar cámara",
|
||||
"selectWebcamToEnable": "Selecciona una cámara para activar",
|
||||
"noWebcamsFound": "No se encontraron cámaras",
|
||||
"recordingsFolder": "Carpeta de grabaciones",
|
||||
"language": "Idioma",
|
||||
"paused": "PAUSADO",
|
||||
"rec": "REC",
|
||||
"resume": "Reanudar",
|
||||
"pause": "Pausa",
|
||||
"stop": "Detener",
|
||||
"cancel": "Cancelar",
|
||||
"more": "Más"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Cargando fuentes...",
|
||||
|
||||
@@ -17,7 +17,27 @@
|
||||
"hideHudFromVideo": "在录制中隐藏 HUD",
|
||||
"showHudInVideo": "在录制中显示 HUD",
|
||||
"hideHud": "隐藏 HUD",
|
||||
"closeApp": "关闭应用"
|
||||
"closeApp": "关闭应用",
|
||||
"screens": "屏幕",
|
||||
"windows": "窗口",
|
||||
"noSourcesFound": "未找到源",
|
||||
"microphone": "麦克风",
|
||||
"turnOffMicrophone": "关闭麦克风",
|
||||
"selectMicToEnable": "选择一个麦克风以启用",
|
||||
"noMicrophonesFound": "未找到麦克风",
|
||||
"webcam": "摄像头",
|
||||
"turnOffWebcam": "关闭摄像头",
|
||||
"selectWebcamToEnable": "选择一个摄像头以启用",
|
||||
"noWebcamsFound": "未找到摄像头",
|
||||
"recordingsFolder": "录制文件夹",
|
||||
"language": "语言",
|
||||
"paused": "已暂停",
|
||||
"rec": "录制中",
|
||||
"resume": "恢复",
|
||||
"pause": "暂停",
|
||||
"stop": "停止",
|
||||
"cancel": "取消",
|
||||
"more": "更多"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "正在加载源...",
|
||||
|
||||
Reference in New Issue
Block a user