Skip to content

fix(windows): resolve browser mic deviceId to WASAPI endpoint ID#105

Closed
Aibd wants to merge 1 commit intowebadderall:mainfrom
Aibd:fix/honor-selected-microphone
Closed

fix(windows): resolve browser mic deviceId to WASAPI endpoint ID#105
Aibd wants to merge 1 commit intowebadderall:mainfrom
Aibd:fix/honor-selected-microphone

Conversation

@Aibd
Copy link

@Aibd Aibd commented Mar 26, 2026

The browser's navigator.mediaDevices.enumerateDevices() returns short hash device IDs that never match Windows WASAPI endpoint IDs (which use the {0.0.1.00000000}.{guid} format). This caused the native capture to always silently fall back to the default microphone.

Add --list-devices mode to wgc-capture.exe that enumerates WASAPI audio input devices as JSON. Before starting native recording, handlers.ts now calls this to map the browser device label to the correct WASAPI endpoint ID via name matching.

Pull Request Template

Description

Problem: Native Windows capture always records from the default microphone,
ignoring user selection. Browser API returns short hash device IDs that never match
WASAPI endpoint IDs ({0.0.1.00000000}.{guid} format), so findCaptureDeviceById()
always fails and silently falls back to the default device.

Motivation

Fix: Added --list-devices mode to wgc-capture.exe that enumerates WASAPI
devices as JSON. handlers.ts now calls this before recording to map browser device
label → correct WASAPI endpoint ID via name matching.

  • Added diagnostic stderr logging in initializeMic.

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

#103

Testing Guide

  • Run wgc-capture.exe --list-devices, verify JSON output
  • Connect two different mics, select non-default one, record
  • Verify stderr shows MIC: Matched device by WASAPI endpoint ID
  • Verify recorded audio comes from the selected device

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Thank you for contributing!

Summary by CodeRabbit

  • New Features

    • Added Windows audio device enumeration support for improved microphone detection and device matching on Windows systems.
  • Bug Fixes

    • Enhanced microphone device selection on Windows by implementing improved matching against available WASAPI audio endpoints.
    • Added debug logging for microphone device selection to help troubleshoot microphone connectivity issues.

The browser's navigator.mediaDevices.enumerateDevices() returns short
hash device IDs that never match Windows WASAPI endpoint IDs (which use
the {0.0.1.00000000}.{guid} format). This caused the native capture to
always silently fall back to the default microphone.

Add --list-devices mode to wgc-capture.exe that enumerates WASAPI audio
input devices as JSON. Before starting native recording, handlers.ts now
calls this to map the browser device label to the correct WASAPI endpoint
ID via name matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "*" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

Windows WASAPI microphone device enumeration and resolution system has been implemented. A native executable command now lists available audio devices via COM/WASAPI as JSON, while the IPC handler resolves microphone labels to WASAPI endpoint IDs for recording configuration. Debug logging tracks device matching behavior.

Changes

Cohort / File(s) Summary
IPC Handler & Device Resolution
electron/ipc/handlers.ts
Added resolveWasapiDeviceId() function that executes wgc-capture.exe --list-devices, parses JSON device list, and matches human-readable microphone labels to WASAPI endpoint IDs via exact or substring matching. Updated Windows recording start flow to resolve microphone labels instead of passing device IDs directly.
Native Capture Enumeration
electron/native/wgc-capture/src/main.cpp
Added Windows audio device enumeration via COM/WASAPI. Implemented helper functions for UTF-8 string conversion, JSON escaping, and active capture endpoint enumeration. Updated main function to detect --list-devices CLI argument and output JSON array of available devices with {id, name} pairs to stdout.
Debug Logging
electron/native/wgc-capture/src/wasapi_loopback.cpp
Added diagnostic logging in WasapiCapture::initializeMic to report requested microphone device ID/name, device matching results (exact or friendly name match), and fallback behavior to default capture device.

Sequence Diagram

sequenceDiagram
    participant IPC as IPC Handler
    participant EXE as wgc-capture.exe
    participant WASAPI as Windows WASAPI
    participant Config as Recording Config

    IPC->>IPC: Receive microphoneLabel
    IPC->>EXE: Execute --list-devices (5s timeout)
    EXE->>WASAPI: Enumerate capture endpoints via COM
    WASAPI-->>EXE: Active device list
    EXE-->>IPC: JSON [{id, name}, ...] to stdout
    IPC->>IPC: Parse JSON & match label to id
    alt Exact match found
        IPC->>Config: Set micDeviceId to WASAPI id
    else Substring match found
        IPC->>Config: Set micDeviceId to WASAPI id
    else No match
        IPC->>Config: Leave micDeviceId unset
    end
    IPC->>Config: Set micDeviceName from browser label
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through Windows sound,
Listing devices all around,
Labels matched to WASAPI's call,
Debug logs whisper through it all,
Audio endpoints found at last! 🎙️

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: resolving browser microphone device IDs to Windows WASAPI endpoint IDs, which is the core fix implemented across all modified files.
Description check ✅ Passed The description comprehensively covers the problem, solution, related issue, and testing steps. All critical sections are completed, including a clear explanation of the bug, the implemented fix, and actionable testing guidance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 1668-1688: resolveWasapiDeviceId currently uses find() and
silently picks the first match, which breaks when multiple devices share the
same friendly name; change the logic to detect duplicate matches and treat them
as ambiguous: for the exact-match step (devices where d.name === browserLabel)
use filter and if it yields exactly one result return its id, otherwise return
undefined (do not fall back); if no exact matches, do the same for the
partial-match step (filter for d.name.includes(browserLabel) ||
browserLabel.includes(d.name)) and only return an id when there is exactly one
match, otherwise return undefined; keep the
execFileAsync/getWindowsCaptureExePath usage and the existing try/catch but
avoid selecting the first result when multiple matches exist.

In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 226-243: Guard all COM calls and pointer dereferences in the
device enumeration loop: check HRESULT from collection->Item(...) and skip the
entry on failure; verify dev is non-null before calling dev->GetId(...) and only
call CoTaskMemFree(deviceId) if deviceId is non-null; check HRESULT from
dev->OpenPropertyStore(...) and skip property access if it fails (only call
store->GetValue(...) when store is valid); before reading pv.pwszVal validate
the PROPVARIANT type is VT_LPWSTR (or handle other string types) and only access
pv.pwszVal when that check succeeds; call PropVariantClear(&pv) and Release() on
store/dev only when they were successfully obtained to avoid dereferencing null
pointers, and consider logging HRESULT errors for diagnostics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0756fdea-fde0-4a15-b0a9-b5b06549cd21

📥 Commits

Reviewing files that changed from the base of the PR and between 485726f and ab964a3.

📒 Files selected for processing (3)
  • electron/ipc/handlers.ts
  • electron/native/wgc-capture/src/main.cpp
  • electron/native/wgc-capture/src/wasapi_loopback.cpp

Comment on lines +1668 to +1688
async function resolveWasapiDeviceId(browserLabel: string | undefined): Promise<string | undefined> {
if (!browserLabel) return undefined
try {
const exePath = getWindowsCaptureExePath()
const { stdout } = await execFileAsync(exePath, ['--list-devices'], { timeout: 5000 })
const devices: Array<{ id: string; name: string }> = JSON.parse(stdout.trim())

// Exact match first
const exact = devices.find((d) => d.name === browserLabel)
if (exact) return exact.id

// Partial / substring match (browser label often contains or is contained by WASAPI name)
const partial = devices.find(
(d) => d.name.includes(browserLabel) || browserLabel.includes(d.name),
)
if (partial) return partial.id
} catch (err) {
console.warn('Failed to resolve WASAPI device ID:', err)
}
return undefined
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle duplicate microphone names explicitly instead of picking the first match.

This resolver assumes friendly names are unique, but two active USB mics/headsets can expose the same label. In that case find() returns whichever endpoint enumerates first, and electron/native/wgc-capture/src/wasapi_loopback.cpp, Lines 131-142 will then lock capture onto that wrong micDeviceId. Please treat multi-match results as ambiguous and skip the downstream name fallback in that case, otherwise the native side just repeats the same first-match mistake.

Suggested fix
-async function resolveWasapiDeviceId(browserLabel: string | undefined): Promise<string | undefined> {
-  if (!browserLabel) return undefined
+async function resolveWasapiDeviceId(
+  browserLabel: string | undefined,
+): Promise<{ id?: string; ambiguous?: boolean }> {
+  if (!browserLabel) return {}
   try {
     const exePath = getWindowsCaptureExePath()
     const { stdout } = await execFileAsync(exePath, ['--list-devices'], { timeout: 5000 })
     const devices: Array<{ id: string; name: string }> = JSON.parse(stdout.trim())
+    const normalizedLabel = normalizeDesktopSourceName(browserLabel)
 
-    // Exact match first
-    const exact = devices.find((d) => d.name === browserLabel)
-    if (exact) return exact.id
+    const exactMatches = devices.filter(
+      (d) => normalizeDesktopSourceName(d.name) === normalizedLabel,
+    )
+    if (exactMatches.length === 1) return { id: exactMatches[0].id }
+    if (exactMatches.length > 1) {
+      console.warn('Ambiguous WASAPI device label; skipping endpoint pinning:', browserLabel)
+      return { ambiguous: true }
+    }
 
-    // Partial / substring match (browser label often contains or is contained by WASAPI name)
-    const partial = devices.find(
-      (d) => d.name.includes(browserLabel) || browserLabel.includes(d.name),
-    )
-    if (partial) return partial.id
+    const partialMatches = devices.filter((d) => {
+      const normalizedName = normalizeDesktopSourceName(d.name)
+      return normalizedName.includes(normalizedLabel) || normalizedLabel.includes(normalizedName)
+    })
+    if (partialMatches.length === 1) return { id: partialMatches[0].id }
+    if (partialMatches.length > 1) {
+      console.warn('Ambiguous WASAPI partial match; skipping endpoint pinning:', browserLabel)
+      return { ambiguous: true }
+    }
   } catch (err) {
     console.warn('Failed to resolve WASAPI device ID:', err)
   }
-  return undefined
+  return {}
 }
-          const wasapiId = await resolveWasapiDeviceId(options.microphoneLabel)
-          if (wasapiId) {
-            config.micDeviceId = wasapiId
+          const resolvedMic = await resolveWasapiDeviceId(options.microphoneLabel)
+          if (resolvedMic.id) {
+            config.micDeviceId = resolvedMic.id
           }
-          if (options.microphoneLabel) {
+          if (options.microphoneLabel && !resolvedMic.ambiguous) {
             config.micDeviceName = options.microphoneLabel
           }

Also applies to: 3071-3075

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 1668 - 1688, resolveWasapiDeviceId
currently uses find() and silently picks the first match, which breaks when
multiple devices share the same friendly name; change the logic to detect
duplicate matches and treat them as ambiguous: for the exact-match step (devices
where d.name === browserLabel) use filter and if it yields exactly one result
return its id, otherwise return undefined (do not fall back); if no exact
matches, do the same for the partial-match step (filter for
d.name.includes(browserLabel) || browserLabel.includes(d.name)) and only return
an id when there is exactly one match, otherwise return undefined; keep the
execFileAsync/getWindowsCaptureExePath usage and the existing try/catch but
avoid selecting the first result when multiple matches exist.

Comment on lines +226 to +243
for (UINT i = 0; i < count; i++) {
IMMDevice* dev = nullptr;
collection->Item(i, &dev);

LPWSTR deviceId = nullptr;
dev->GetId(&deviceId);
std::string id = deviceId ? wideToUtf8(deviceId) : "";
if (deviceId) CoTaskMemFree(deviceId);

IPropertyStore* store = nullptr;
dev->OpenPropertyStore(STGM_READ, &store);
PROPVARIANT pv;
PropVariantInit(&pv);
store->GetValue(PKEY_Device_FriendlyName, &pv);
std::string name = pv.pwszVal ? wideToUtf8(pv.pwszVal) : "";
PropVariantClear(&pv);
store->Release();
dev->Release();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the main.cpp file
fd "main.cpp" electron/native/wgc-capture/src/ --exec cat -n {} \;

Repository: webadderall/Recordly

Length of output: 17497


Guard failed COM calls before dereferencing device pointers.

collection->Item(), dev->GetId(), and dev->OpenPropertyStore() are used unchecked here. A single endpoint that fails enumeration can leave dev or store null and crash --list-devices, which sends the whole Windows mic-resolution path back to default-device fallback. Additionally, store->GetValue() doesn't verify the variant type before accessing pv.pwszVal.

Suggested fix
     for (UINT i = 0; i < count; i++) {
         IMMDevice* dev = nullptr;
-        collection->Item(i, &dev);
+        hr = collection->Item(i, &dev);
+        if (FAILED(hr) || !dev) {
+            continue;
+        }

         LPWSTR deviceId = nullptr;
-        dev->GetId(&deviceId);
+        hr = dev->GetId(&deviceId);
+        if (FAILED(hr)) {
+            dev->Release();
+            continue;
+        }
         std::string id = deviceId ? wideToUtf8(deviceId) : "";
         if (deviceId) CoTaskMemFree(deviceId);

         IPropertyStore* store = nullptr;
-        dev->OpenPropertyStore(STGM_READ, &store);
+        hr = dev->OpenPropertyStore(STGM_READ, &store);
+        if (FAILED(hr) || !store) {
+            dev->Release();
+            continue;
+        }
         PROPVARIANT pv;
         PropVariantInit(&pv);
-        store->GetValue(PKEY_Device_FriendlyName, &pv);
-        std::string name = pv.pwszVal ? wideToUtf8(pv.pwszVal) : "";
+        std::string name;
+        hr = store->GetValue(PKEY_Device_FriendlyName, &pv);
+        if (SUCCEEDED(hr) && pv.vt == VT_LPWSTR && pv.pwszVal) {
+            name = wideToUtf8(pv.pwszVal);
+        }
         PropVariantClear(&pv);
         store->Release();
         dev->Release();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/native/wgc-capture/src/main.cpp` around lines 226 - 243, Guard all
COM calls and pointer dereferences in the device enumeration loop: check HRESULT
from collection->Item(...) and skip the entry on failure; verify dev is non-null
before calling dev->GetId(...) and only call CoTaskMemFree(deviceId) if deviceId
is non-null; check HRESULT from dev->OpenPropertyStore(...) and skip property
access if it fails (only call store->GetValue(...) when store is valid); before
reading pv.pwszVal validate the PROPVARIANT type is VT_LPWSTR (or handle other
string types) and only access pv.pwszVal when that check succeeds; call
PropVariantClear(&pv) and Release() on store/dev only when they were
successfully obtained to avoid dereferencing null pointers, and consider logging
HRESULT errors for diagnostics.

@webadderall
Copy link
Owner

webadderall commented Mar 26, 2026

PR made redundant by Fix WGC display selection and webcam sync

But thanks for contributing! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants