fix(windows): resolve browser mic deviceId to WASAPI endpoint ID#105
fix(windows): resolve browser mic deviceId to WASAPI endpoint ID#105Aibd wants to merge 1 commit intowebadderall:mainfrom
Conversation
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>
|
Warning
|
| 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
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
electron/ipc/handlers.tselectron/native/wgc-capture/src/main.cppelectron/native/wgc-capture/src/wasapi_loopback.cpp
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
🧩 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.
|
PR made redundant by Fix WGC display selection and webcam sync But thanks for contributing! ❤️ |
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), sofindCaptureDeviceById()always fails and silently falls back to the default device.
Motivation
Fix: Added
--list-devicesmode towgc-capture.exethat enumerates WASAPIdevices as JSON.
handlers.tsnow calls this before recording to map browser devicelabel → correct WASAPI endpoint ID via name matching.
initializeMic.Type of Change
Related Issue(s)
#103
Testing Guide
wgc-capture.exe --list-devices, verify JSON outputMIC: Matched device by WASAPI endpoint IDChecklist
Thank you for contributing!
Summary by CodeRabbit
New Features
Bug Fixes