-
-
-
-
-
-
-
-
-
- {supportsHudCaptureProtection && (
-
diff --git a/src/components/video-editor/editorPreferences.test.ts b/src/components/video-editor/editorPreferences.test.ts
index 4d18569..23fa07e 100644
--- a/src/components/video-editor/editorPreferences.test.ts
+++ b/src/components/video-editor/editorPreferences.test.ts
@@ -94,6 +94,7 @@ describe("editorPreferences", () => {
gifFrameRate: 30,
gifLoop: false,
gifSizePreset: DEFAULT_EDITOR_PREFERENCES.gifSizePreset,
+ webcam: DEFAULT_EDITOR_PREFERENCES.webcam,
customAspectWidth: "21",
customAspectHeight: "9",
customWallpapers: ["data:image/jpeg;base64,abc"],
@@ -134,6 +135,7 @@ describe("editorPreferences", () => {
gifFrameRate: DEFAULT_EDITOR_PREFERENCES.gifFrameRate,
gifLoop: DEFAULT_EDITOR_PREFERENCES.gifLoop,
gifSizePreset: DEFAULT_EDITOR_PREFERENCES.gifSizePreset,
+ webcam: DEFAULT_EDITOR_PREFERENCES.webcam,
customAspectWidth: "21",
customAspectHeight: "9",
customWallpapers: DEFAULT_EDITOR_PREFERENCES.customWallpapers,
@@ -193,6 +195,7 @@ describe("editorPreferences", () => {
gifFrameRate: 20,
gifLoop: false,
gifSizePreset: "large",
+ webcam: DEFAULT_EDITOR_PREFERENCES.webcam,
customAspectWidth: "4",
customAspectHeight: "5",
customWallpapers: ["data:image/jpeg;base64,abc"],
diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts
new file mode 100644
index 0000000..e8c2b6a
--- /dev/null
+++ b/src/hooks/useScreenRecorder.test.ts
@@ -0,0 +1,441 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+type RecordingState = "inactive" | "recording" | "paused";
+
+function createMockMediaRecorder(initialState: RecordingState = "inactive") {
+ let _state: RecordingState = initialState;
+ return {
+ get state() {
+ return _state;
+ },
+ pause: vi.fn(() => {
+ if (_state === "recording") _state = "paused";
+ }),
+ resume: vi.fn(() => {
+ if (_state === "paused") _state = "recording";
+ }),
+ stop: vi.fn(() => {
+ _state = "inactive";
+ }),
+ start: vi.fn(() => {
+ _state = "recording";
+ }),
+ };
+}
+
+function stopRecording(
+ recorder: ReturnType
,
+ isNativeRecording: boolean,
+ webcamRecorder?: ReturnType | null,
+) {
+ if (isNativeRecording) {
+ if (webcamRecorder && webcamRecorder.state !== "inactive") {
+ webcamRecorder.stop();
+ }
+ return { stopped: true, wasNative: true };
+ }
+
+ const recorderState = recorder.state;
+ if (recorderState === "recording" || recorderState === "paused") {
+ if (recorderState === "paused") {
+ recorder.resume();
+ }
+ if (webcamRecorder && webcamRecorder.state !== "inactive") {
+ webcamRecorder.stop();
+ }
+ recorder.stop();
+ return { stopped: true, wasNative: false };
+ }
+ return { stopped: false, wasNative: false };
+}
+
+function pauseRecording(
+ recorder: ReturnType,
+ recording: boolean,
+ paused: boolean,
+ isNativeRecording: boolean,
+ webcamRecorder?: ReturnType | null,
+): boolean {
+ if (!recording || paused) 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;
+}
+
+function resumeRecording(
+ recorder: ReturnType,
+ recording: boolean,
+ paused: boolean,
+ isNativeRecording: boolean,
+ webcamRecorder?: ReturnType | 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,
+ isNativeRecording: boolean,
+ chunks: { current: Blob[] },
+ webcamRecorder?: ReturnType | 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;
+
+ beforeEach(() => {
+ recorder = createMockMediaRecorder("recording");
+ });
+
+ describe("stopRecording", () => {
+ it("stops from recording state", () => {
+ const result = stopRecording(recorder, false);
+
+ expect(result.stopped).toBe(true);
+ expect(recorder.stop).toHaveBeenCalled();
+ expect(recorder.resume).not.toHaveBeenCalled();
+ expect(recorder.state).toBe("inactive");
+ });
+
+ it("resumes then stops from paused state", () => {
+ recorder.pause();
+ expect(recorder.state).toBe("paused");
+
+ const result = stopRecording(recorder, false);
+
+ expect(result.stopped).toBe(true);
+ expect(recorder.resume).toHaveBeenCalled();
+ expect(recorder.stop).toHaveBeenCalled();
+ expect(recorder.state).toBe("inactive");
+ });
+
+ it("resume is called before stop when paused", () => {
+ recorder.pause();
+ const callOrder: string[] = [];
+ recorder.resume.mockImplementation(() => {
+ callOrder.push("resume");
+ });
+ recorder.stop.mockImplementation(() => {
+ callOrder.push("stop");
+ });
+
+ stopRecording(recorder, false);
+
+ expect(callOrder).toEqual(["resume", "stop"]);
+ });
+
+ it("does nothing when already inactive", () => {
+ const inactiveRecorder = createMockMediaRecorder("inactive");
+
+ const result = stopRecording(inactiveRecorder, false);
+
+ expect(result.stopped).toBe(false);
+ expect(inactiveRecorder.stop).not.toHaveBeenCalled();
+ });
+
+ it("delegates to native path for native recordings", () => {
+ const result = stopRecording(recorder, true);
+
+ expect(result.stopped).toBe(true);
+ 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", () => {
+ it("pauses an active recording", () => {
+ const result = pauseRecording(recorder, true, false, false);
+
+ expect(result).toBe(true);
+ expect(recorder.pause).toHaveBeenCalled();
+ expect(recorder.state).toBe("paused");
+ });
+
+ it("does nothing when already paused", () => {
+ recorder.pause();
+ recorder.pause.mockClear();
+
+ const result = pauseRecording(recorder, true, true, false);
+
+ expect(result).toBe(false);
+ expect(recorder.pause).not.toHaveBeenCalled();
+ });
+
+ it("does nothing when not recording", () => {
+ const result = pauseRecording(recorder, false, false, false);
+
+ expect(result).toBe(false);
+ expect(recorder.pause).not.toHaveBeenCalled();
+ });
+
+ it("allows pause for native recordings", () => {
+ const result = pauseRecording(recorder, true, false, true);
+
+ 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();
+ });
+ });
+
+ describe("resumeRecording", () => {
+ it("resumes a paused recording", () => {
+ recorder.pause();
+
+ const result = resumeRecording(recorder, true, true, false);
+
+ expect(result).toBe(true);
+ expect(recorder.resume).toHaveBeenCalled();
+ expect(recorder.state).toBe("recording");
+ });
+
+ it("does nothing when not paused", () => {
+ 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, 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("record → pause → stop completes cleanly", () => {
+ expect(recorder.state).toBe("recording");
+
+ pauseRecording(recorder, true, false, false);
+ expect(recorder.state).toBe("paused");
+
+ const result = stopRecording(recorder, false);
+ expect(result.stopped).toBe(true);
+ expect(recorder.state).toBe("inactive");
+ });
+
+ 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, 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");
+ });
+ });
+});
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index a49a23c..10905d1 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -34,8 +34,12 @@ const WEBCAM_SUFFIX = "-webcam";
type UseScreenRecorderReturn = {
recording: boolean;
+ paused: boolean;
countdownActive: boolean;
toggleRecording: () => void;
+ pauseRecording: () => void;
+ resumeRecording: () => void;
+ cancelRecording: () => void;
preparePermissions: (options?: { startup?: boolean }) => Promise;
isMacOS: boolean;
microphoneEnabled: boolean;
@@ -54,6 +58,7 @@ type UseScreenRecorderReturn = {
export function useScreenRecorder(): UseScreenRecorderReturn {
const [recording, setRecording] = useState(false);
+ const [paused, setPaused] = useState(false);
const [starting, setStarting] = useState(false);
const [countdownActive, setCountdownActive] = useState(false);
const [isMacOS, setIsMacOS] = useState(false);
@@ -299,6 +304,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}, [webcamDeviceId, webcamEnabled]);
const stopRecording = useRef(() => {
+ setPaused(false);
if (nativeScreenRecording.current) {
nativeScreenRecording.current = false;
setRecording(false);
@@ -328,10 +334,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return;
}
- if (mediaRecorder.current?.state === "recording") {
+ const recorder = mediaRecorder.current;
+ const recorderState = recorder?.state;
+ if (recorder && (recorderState === "recording" || recorderState === "paused")) {
+ if (recorderState === "paused") {
+ recorder.resume();
+ }
pendingWebcamPathPromise.current = stopWebcamRecorder();
cleanupCapturedMedia();
- mediaRecorder.current.stop();
+ recorder.stop();
setRecording(false);
window.electronAPI?.setRecordingState(false);
}
@@ -737,6 +748,86 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
};
+ const pauseRecording = useCallback(() => {
+ if (!recording || paused) 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]);
+
+ const cancelRecording = useCallback(() => {
+ 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 (async () => {
+ try {
+ const result = await window.electronAPI.stopNativeScreenRecording();
+ if (result?.path) {
+ await window.electronAPI.deleteRecordingFile(result.path);
+ }
+ } catch {
+ // Best-effort cleanup
+ }
+ })();
+ return;
+ }
+
+ if (mediaRecorder.current) {
+ chunks.current = [];
+ cleanupCapturedMedia();
+ if (mediaRecorder.current.state !== "inactive") {
+ mediaRecorder.current.stop();
+ }
+ setRecording(false);
+ window.electronAPI?.setRecordingState(false);
+ }
+ }, [recording, cleanupCapturedMedia]);
+
const toggleRecording = async () => {
if (starting || countdownActive) {
return;
@@ -765,8 +856,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return {
recording,
+ paused,
countdownActive,
toggleRecording,
+ pauseRecording,
+ resumeRecording,
+ cancelRecording,
preparePermissions,
isMacOS,
microphoneEnabled,
@@ -783,4 +878,3 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
setCountdownDelay,
};
}
-
diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json
index 9d3a9fa..1e7eced 100644
--- a/src/i18n/locales/en/launch.json
+++ b/src/i18n/locales/en/launch.json
@@ -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...",
diff --git a/src/i18n/locales/es/launch.json b/src/i18n/locales/es/launch.json
index d8a12ba..e4d50f3 100644
--- a/src/i18n/locales/es/launch.json
+++ b/src/i18n/locales/es/launch.json
@@ -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...",
diff --git a/src/i18n/locales/zh-CN/launch.json b/src/i18n/locales/zh-CN/launch.json
index 6c3d91d..72c9573 100644
--- a/src/i18n/locales/zh-CN/launch.json
+++ b/src/i18n/locales/zh-CN/launch.json
@@ -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": "正在加载源...",