Skip to content

Microphone cannot be made optional at JS layer (binary forces OS mic permission); recordSystemAudio toggle is dead code #5

@skalkii

Description

@skalkii

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:

  1. Check systemPreferences.getMediaAccessStatus('microphone') before asking the binary to request mic permission.
  2. Skip client.requestPermission('microphone') entirely when OS status is denied or restricted.
  3. 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

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions