Remember recording device preferences

This commit is contained in:
webadderall
2026-09-02 20:56:07 +10:00
parent f0cdbca3fb
commit fdded88786
6 changed files with 134 additions and 36 deletions
+5 -1
View File
@@ -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 }>;
+20 -33
View File
@@ -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;
},
};
}
+2
View File
@@ -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),
+16 -2
View File
@@ -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,
};