Summary
While trying to make microphone permission optional during recording (so a user who denies the macOS mic prompt can still record screen-only), I hit a hard constraint in the videodb recorder binary, and along the way found that the recordSystemAudio UI toggle is dead code. This issue documents both findings and proposes fixes.
Finding 1 — videodb recorder binary forces OS mic permission on macOS
The native Rust recorder shipped at node_modules/videodb/bin/recorder (videodb@0.2.0 / @videodb/recorder@0.2.4) unconditionally validates OS-level microphone permission at startSession on macOS, regardless of which channels are passed.
Reproduction (with attempted JS-level fix)
src/main/services/capture.ts was modified to:
- Check
systemPreferences.getMediaAccessStatus('microphone') before asking the binary to request mic permission.
- Skip
client.requestPermission('microphone') entirely when OS status is denied or restricted.
- Skip adding the mic channel to
captureChannels whenever OS status is not granted.
Run with macOS mic denied + recordMic: true + recordScreen: true:
[CAPTURE] Skipping mic permission request — OS status is "denied"
[CAPTURE] spawn intercepted: ... recorder ...
[CAPTURE] Screen capture permission granted
[CAPTURE] Permissions complete, binary should be running
[CAPTURE] Microphone permission status: not granted (skipping mic channel)
[CAPTURE] Discovered channels: 1 mic(s), 1 display(s)
[CAPTURE] Mic recording requested but OS permission not granted — recording without microphone
[CAPTURE] Added display channel: display:1
[CAPTURE] Starting capture with 1 channel(s)...
[IPC] capture:start failed: {"code":"PERMISSION_DENIED","message":"Permission denied: Microphone permission denied. Grant in System Preferences > Security & Privacy > Microphone"}
at BinaryManager.handleMessage (node_modules/videodb/dist/recorder/binaryManager.js:104)
The binary errored at startSession even though:
- We never called
requestPermission('microphone') for this session
- The
channels array passed to startSession contained only the display channel (no mic)
- userData and prior recorder processes were cleared between runs
Confirmed via binary string analysis
The recorder binary contains two distinct error strings, and on macOS it takes the denied path whenever OS TCC has microphone denied:
"Microphone permission denied. Grant in System Preferences..." ← what we see
"Microphone permission not requested. Please try again." ← would suggest the request-tracking theory; not what we hit
What this means
- A focusd-level "soft-optional mic" cannot be implemented at the JS layer with the current SDK/binary.
- Every
startSession on macOS must have OS-level mic granted, even when the user only wants screen capture.
Finding 2 — recordSystemAudio toggle is dead code
The Settings UI exposes a "System Audio" toggle (src/renderer/src/components/SettingsView.tsx:173–177) that maps to settings.recordSystemAudio. The setting is persisted to SQLite and round-trips through IPC correctly.
But in src/main/services/capture.ts, the only reference to recordSystemAudio is a log statement at line 166:
$ grep -n -i "systemAudio\|system_audio\|recordSystemAudio" src/main/services/capture.ts
166: recordSystemAudio: settings.recordSystemAudio,
There is no if (settings.recordSystemAudio) { ... } block that adds a system_audio channel to captureChannels. The videodb SDK exposes channels.systemAudio as the supported way to capture system audio, but we never read it or pass a system_audio channel to startSession.
Net effect: system audio is never captured by the SDK regardless of the toggle state. The toggle is non-functional.
This explains a confusing empirical observation that started this investigation: with recordMic: false, recordSystemAudio: false, and macOS mic granted, recording works fine — because (a) the mic toggle correctly omits the mic channel and (b) the system audio toggle has no effect either way (system audio was never being captured).
Proposed fixes
Short term — within this repo
- Make microphone permission required in onboarding, not optional. Update
Onboarding.tsx's mic permission card to gate the "Continue" button on getMediaAccessStatus('microphone') === 'granted', mirroring how Screen Recording is treated. Document that mic permission is mandatory due to the binary constraint, not because focusd needs to record mic.
- Surface a clearer error to the UI when capture fails due to mic denial post-onboarding. The current error card says "Screen recording permission is required" because the renderer maps any
PERMISSION_DENIED to the screen card. Map code: 'PERMISSION_DENIED' + message containing "Microphone" to the mic-permission card with a "Grant in System Settings" CTA.
- Wire up the
recordSystemAudio toggle in capture.ts:
if (settings.recordSystemAudio) {
const sysAudio = channels.systemAudio?.default;
if (sysAudio) {
captureChannels.push({
channelId: (sysAudio as any).id,
type: 'audio',
record: true,
});
}
}
Or, if system audio is intentionally unsupported for now, remove the toggle from SettingsView.tsx so users aren't given a non-functional control.
Long term — upstream (videodb SDK / recorder binary)
- File an issue against the recorder binary asking it to only validate OS permission for permission types whose channels are present in the
startSession config, instead of unconditionally validating mic on macOS. This would let downstream apps (focusd and others) implement truly optional mic capture.
Branch + reset script (referenced)
Environment
- macOS (TCC-managed permissions)
- Electron via electron-vite (
npm run dev)
videodb@0.2.0, @videodb/recorder@0.2.4
Summary
While trying to make microphone permission optional during recording (so a user who denies the macOS mic prompt can still record screen-only), I hit a hard constraint in the videodb recorder binary, and along the way found that the
recordSystemAudioUI toggle is dead code. This issue documents both findings and proposes fixes.Finding 1 —
videodbrecorder binary forces OS mic permission on macOSThe native Rust recorder shipped at
node_modules/videodb/bin/recorder(videodb@0.2.0 / @videodb/recorder@0.2.4) unconditionally validates OS-level microphone permission atstartSessionon macOS, regardless of which channels are passed.Reproduction (with attempted JS-level fix)
src/main/services/capture.tswas modified to:systemPreferences.getMediaAccessStatus('microphone')before asking the binary to request mic permission.client.requestPermission('microphone')entirely when OS status isdeniedorrestricted.captureChannelswhenever OS status is notgranted.Run with macOS mic denied +
recordMic: true+recordScreen: true:The binary errored at
startSessioneven though:requestPermission('microphone')for this sessionchannelsarray passed tostartSessioncontained only the display channel (no mic)Confirmed via binary string analysis
The recorder binary contains two distinct error strings, and on macOS it takes the denied path whenever OS TCC has microphone denied:
"Microphone permission denied. Grant in System Preferences..."← what we see"Microphone permission not requested. Please try again."← would suggest the request-tracking theory; not what we hitWhat this means
startSessionon macOS must have OS-level mic granted, even when the user only wants screen capture.Finding 2 —
recordSystemAudiotoggle is dead codeThe Settings UI exposes a "System Audio" toggle (
src/renderer/src/components/SettingsView.tsx:173–177) that maps tosettings.recordSystemAudio. The setting is persisted to SQLite and round-trips through IPC correctly.But in
src/main/services/capture.ts, the only reference torecordSystemAudiois a log statement at line 166:$ grep -n -i "systemAudio\|system_audio\|recordSystemAudio" src/main/services/capture.ts 166: recordSystemAudio: settings.recordSystemAudio,There is no
if (settings.recordSystemAudio) { ... }block that adds asystem_audiochannel tocaptureChannels. The videodb SDK exposeschannels.systemAudioas the supported way to capture system audio, but we never read it or pass a system_audio channel tostartSession.Net effect: system audio is never captured by the SDK regardless of the toggle state. The toggle is non-functional.
This explains a confusing empirical observation that started this investigation: with
recordMic: false,recordSystemAudio: false, and macOS mic granted, recording works fine — because (a) the mic toggle correctly omits the mic channel and (b) the system audio toggle has no effect either way (system audio was never being captured).Proposed fixes
Short term — within this repo
Onboarding.tsx's mic permission card to gate the "Continue" button ongetMediaAccessStatus('microphone') === 'granted', mirroring how Screen Recording is treated. Document that mic permission is mandatory due to the binary constraint, not because focusd needs to record mic.PERMISSION_DENIEDto the screen card. Mapcode: 'PERMISSION_DENIED'+messagecontaining"Microphone"to the mic-permission card with a "Grant in System Settings" CTA.recordSystemAudiotoggle incapture.ts:SettingsView.tsxso users aren't given a non-functional control.Long term — upstream (videodb SDK / recorder binary)
startSessionconfig, instead of unconditionally validating mic on macOS. This would let downstream apps (focusd and others) implement truly optional mic capture.Branch + reset script (referenced)
fix/microphone-permission-optionalon the contributor's fork.Environment
npm run dev)videodb@0.2.0,@videodb/recorder@0.2.4