mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
Remember recording device preferences
This commit is contained in:
Vendored
+5
-1
@@ -877,12 +877,14 @@ interface Window {
|
||||
getAnnouncements: () => Promise<unknown | null>;
|
||||
/** Hide the OS cursor before browser capture starts. */
|
||||
hideOsCursor: () => Promise<{ success: boolean }>;
|
||||
/** Recording preferences (mic, system audio) */
|
||||
/** Recording preferences (mic, system audio, webcam) */
|
||||
getRecordingPreferences: () => Promise<{
|
||||
success: boolean;
|
||||
microphoneEnabled: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
systemAudioEnabled: boolean;
|
||||
webcamEnabled: boolean;
|
||||
webcamDeviceId?: string;
|
||||
}>;
|
||||
getRecordingAudioLabConfig: () => Promise<{
|
||||
browserMicrophoneProfile: string;
|
||||
@@ -892,6 +894,8 @@ interface Window {
|
||||
microphoneEnabled?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
systemAudioEnabled?: boolean;
|
||||
webcamEnabled?: boolean;
|
||||
webcamDeviceId?: string;
|
||||
}) => Promise<{ success: boolean; error?: string }>;
|
||||
/** Countdown timer before recording */
|
||||
getCountdownDelay: () => Promise<{ success: boolean; delay: number }>;
|
||||
|
||||
@@ -4,6 +4,10 @@ import { hasAppSetting, readAppSettingsStore, writeAppSettingsStore } from "../.
|
||||
import { hideCursor } from "../../cursorHider";
|
||||
import { closeCountdownWindow, createCountdownWindow, getCountdownWindow } from "../../windows";
|
||||
import { COUNTDOWN_SETTINGS_FILE, RECORDINGS_SETTINGS_FILE, SHORTCUTS_FILE } from "../constants";
|
||||
import {
|
||||
createRecordingPreferencesStore,
|
||||
type RecordingPreferencesPatch,
|
||||
} from "../settings/recordingPreferencesStore";
|
||||
import {
|
||||
countdownCancelled,
|
||||
countdownInProgress,
|
||||
@@ -18,6 +22,7 @@ import { parseJsonWithByteOrderMark } from "../utils";
|
||||
|
||||
const BROWSER_MICROPHONE_PROFILE_ENV = "RECORDLY_BROWSER_MIC_PROFILE";
|
||||
const DEFAULT_BROWSER_MICROPHONE_PROFILE = "processed";
|
||||
const recordingPreferencesStore = createRecordingPreferencesStore(RECORDINGS_SETTINGS_FILE);
|
||||
const BROWSER_MICROPHONE_PROFILES = new Set([
|
||||
"processed",
|
||||
"no-agc",
|
||||
@@ -117,8 +122,7 @@ export function registerSettingsHandlers() {
|
||||
// ---------------------------------------------------------------------------
|
||||
ipcMain.handle("get-recording-preferences", async () => {
|
||||
try {
|
||||
const content = await fs.readFile(RECORDINGS_SETTINGS_FILE, "utf-8");
|
||||
const parsed = parseJsonWithByteOrderMark<Record<string, unknown>>(content);
|
||||
const parsed = await recordingPreferencesStore.read();
|
||||
return {
|
||||
success: true,
|
||||
microphoneEnabled: parsed.microphoneEnabled === true,
|
||||
@@ -127,6 +131,9 @@ export function registerSettingsHandlers() {
|
||||
? parsed.microphoneDeviceId
|
||||
: undefined,
|
||||
systemAudioEnabled: parsed.systemAudioEnabled === true,
|
||||
webcamEnabled: parsed.webcamEnabled === true,
|
||||
webcamDeviceId:
|
||||
typeof parsed.webcamDeviceId === "string" ? parsed.webcamDeviceId : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
@@ -134,6 +141,8 @@ export function registerSettingsHandlers() {
|
||||
microphoneEnabled: false,
|
||||
microphoneDeviceId: undefined,
|
||||
systemAudioEnabled: false,
|
||||
webcamEnabled: false,
|
||||
webcamDeviceId: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -142,37 +151,15 @@ export function registerSettingsHandlers() {
|
||||
return getBrowserMicrophoneProfileFromEnv();
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"set-recording-preferences",
|
||||
async (
|
||||
_,
|
||||
prefs: {
|
||||
microphoneEnabled?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
systemAudioEnabled?: boolean;
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
let existing: Record<string, unknown> = {};
|
||||
try {
|
||||
const content = await fs.readFile(RECORDINGS_SETTINGS_FILE, "utf-8");
|
||||
existing = parseJsonWithByteOrderMark<Record<string, unknown>>(content);
|
||||
} catch {
|
||||
// file doesn't exist yet
|
||||
}
|
||||
const merged = { ...existing, ...prefs };
|
||||
await fs.writeFile(
|
||||
RECORDINGS_SETTINGS_FILE,
|
||||
JSON.stringify(merged, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to save recording preferences:", error);
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
},
|
||||
);
|
||||
ipcMain.handle("set-recording-preferences", async (_, prefs: RecordingPreferencesPatch) => {
|
||||
try {
|
||||
await recordingPreferencesStore.update(prefs);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to save recording preferences:", error);
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-countdown-delay", async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRecordingPreferencesStore } from "./recordingPreferencesStore";
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
app: {
|
||||
getPath: () => "",
|
||||
},
|
||||
}));
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
fs.rm(directory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("recording preferences store", () => {
|
||||
it("preserves concurrent microphone and webcam preference updates", async () => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-preferences-"));
|
||||
temporaryDirectories.push(directory);
|
||||
const store = createRecordingPreferencesStore(path.join(directory, "recording.json"));
|
||||
|
||||
await Promise.all([
|
||||
store.update({ microphoneEnabled: true }),
|
||||
store.update({ microphoneDeviceId: "preferred-mic" }),
|
||||
store.update({ webcamEnabled: true }),
|
||||
store.update({ webcamDeviceId: "preferred-camera" }),
|
||||
]);
|
||||
|
||||
await expect(store.read()).resolves.toEqual({
|
||||
microphoneEnabled: true,
|
||||
microphoneDeviceId: "preferred-mic",
|
||||
webcamEnabled: true,
|
||||
webcamDeviceId: "preferred-camera",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { parseJsonWithByteOrderMark } from "../utils";
|
||||
|
||||
export interface RecordingPreferencesPatch {
|
||||
microphoneEnabled?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
systemAudioEnabled?: boolean;
|
||||
webcamEnabled?: boolean;
|
||||
webcamDeviceId?: string;
|
||||
}
|
||||
|
||||
export function createRecordingPreferencesStore(filePath: string) {
|
||||
let operationQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
const readFile = async (): Promise<Record<string, unknown>> => {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const parsed = parseJsonWithByteOrderMark<unknown>(content);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
async read(): Promise<Record<string, unknown>> {
|
||||
await operationQueue;
|
||||
return readFile();
|
||||
},
|
||||
async update(patch: RecordingPreferencesPatch): Promise<void> {
|
||||
const operation = operationQueue.then(async () => {
|
||||
const existing = await readFile();
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
JSON.stringify({ ...existing, ...patch }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
});
|
||||
operationQueue = operation.catch(() => undefined);
|
||||
await operation;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -979,6 +979,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
microphoneEnabled?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
systemAudioEnabled?: boolean;
|
||||
webcamEnabled?: boolean;
|
||||
webcamDeviceId?: string;
|
||||
}) => ipcRenderer.invoke("set-recording-preferences", prefs),
|
||||
getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"),
|
||||
setCountdownDelay: (delay: number) => ipcRenderer.invoke("set-countdown-delay", delay),
|
||||
|
||||
@@ -1533,6 +1533,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
setMicrophoneDeviceId(result.microphoneDeviceId);
|
||||
}
|
||||
setSystemAudioEnabled(result.systemAudioEnabled);
|
||||
setWebcamEnabled(result.webcamEnabled);
|
||||
if (result.webcamDeviceId) {
|
||||
setWebcamDeviceId(result.webcamDeviceId);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
@@ -1552,6 +1556,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
void window.electronAPI.setRecordingPreferences({ systemAudioEnabled: enabled });
|
||||
}, []);
|
||||
|
||||
const persistWebcamEnabled = useCallback((enabled: boolean) => {
|
||||
setWebcamEnabled(enabled);
|
||||
void window.electronAPI.setRecordingPreferences({ webcamEnabled: enabled });
|
||||
}, []);
|
||||
|
||||
const persistWebcamDeviceId = useCallback((deviceId: string | undefined) => {
|
||||
setWebcamDeviceId(deviceId);
|
||||
void window.electronAPI.setRecordingPreferences({ webcamDeviceId: deviceId });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
@@ -2427,9 +2441,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
systemAudioEnabled,
|
||||
setSystemAudioEnabled: persistSystemAudioEnabled,
|
||||
webcamEnabled,
|
||||
setWebcamEnabled,
|
||||
setWebcamEnabled: persistWebcamEnabled,
|
||||
webcamDeviceId,
|
||||
setWebcamDeviceId,
|
||||
setWebcamDeviceId: persistWebcamDeviceId,
|
||||
countdownDelay,
|
||||
setCountdownDelay,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user