Skip to content

Commit 13e3a38

Browse files
Crnobog9527EtienneLescot
authored andcommitted
fix(macos): resolve Chromium USB microphone labels
1 parent 6862a38 commit 13e3a38

4 files changed

Lines changed: 73 additions & 17 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
/// Chromium decorates USB labels with a vendor/product suffix; AVFoundation does not.
4+
/// Resolve IDs first and only accept unambiguous names, never a substring match.
5+
public func resolveMicrophoneDeviceID(
6+
deviceID: String?,
7+
deviceName: String?,
8+
devices: [(id: String, name: String)]
9+
) -> String? {
10+
if let id = deviceID?.trimmingCharacters(in: .whitespacesAndNewlines),
11+
devices.contains(where: { $0.id == id }) {
12+
return id
13+
}
14+
guard let name = deviceName?.trimmingCharacters(in: .whitespacesAndNewlines),
15+
!name.isEmpty else { return nil }
16+
let exact = devices.filter { $0.name == name }
17+
if exact.count == 1 { return exact[0].id }
18+
if !exact.isEmpty { return nil }
19+
let normalized = name.replacingOccurrences(
20+
of: #" \([0-9a-fA-F]{4}:[0-9a-fA-F]{4}\)$"#,
21+
with: "",
22+
options: .regularExpression
23+
)
24+
guard normalized != name else { return nil }
25+
let matches = devices.filter { $0.name == normalized }
26+
return matches.count == 1 ? matches[0].id : nil
27+
}

‎electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift‎

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -828,23 +828,13 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
828828
}
829829

830830
private func resolveMicrophoneCaptureDeviceID() -> String? {
831-
let devices = AVCaptureDevice.devices(for: .audio)
832-
833-
if let deviceName = request.audio.microphone.deviceName?.trimmingCharacters(in: .whitespacesAndNewlines),
834-
!deviceName.isEmpty,
835-
let device = devices.first(where: { $0.localizedName == deviceName })
836-
{
837-
return device.uniqueID
838-
}
839-
840-
if let deviceId = request.audio.microphone.deviceId?.trimmingCharacters(in: .whitespacesAndNewlines),
841-
!deviceId.isEmpty,
842-
devices.contains(where: { $0.uniqueID == deviceId })
843-
{
844-
return deviceId
845-
}
846-
847-
return nil
831+
resolveMicrophoneDeviceID(
832+
deviceID: request.audio.microphone.deviceId,
833+
deviceName: request.audio.microphone.deviceName,
834+
devices: AVCaptureDevice.devices(for: .audio).map {
835+
(id: $0.uniqueID, name: $0.localizedName)
836+
}
837+
)
848838
}
849839
}
850840

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import XCTest
2+
import OpenScreenCaptureCore
3+
4+
final class MicrophoneDeviceSelectionTests: XCTestCase {
5+
private let devices = [(id: "studio", name: "Studio Display"), (id: "mv7", name: "Shure MV7")]
6+
7+
func testChromiumUSBLabelResolvesToSelectedMicrophone() {
8+
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: "browser-hash", deviceName: "Shure MV7 (14ed:1012)", devices: devices), "mv7")
9+
}
10+
11+
func testNativeIDTakesPrecedenceOverStaleName() {
12+
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: "mv7", deviceName: "Studio Display", devices: devices), "mv7")
13+
}
14+
15+
func testExactNameAndWhitespace() {
16+
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: nil, deviceName: " Shure MV7 ", devices: devices), "mv7")
17+
}
18+
19+
func testDoesNotGuessMissingOrPartialNames() {
20+
for name in [nil, "", "Shure", "Shure MV7 (Virtual)", "Shure MV7 (14ed:1012) extra"] as [String?] {
21+
XCTAssertNil(resolveMicrophoneDeviceID(deviceID: "browser-hash", deviceName: name, devices: devices))
22+
}
23+
}
24+
25+
func testAmbiguousNamesDoNotPickFirstDevice() {
26+
let duplicates = devices + [(id: "other", name: "Shure MV7")]
27+
for name in ["Shure MV7", "Shure MV7 (14ED:1012)"] {
28+
XCTAssertNil(resolveMicrophoneDeviceID(deviceID: nil, deviceName: name, devices: duplicates))
29+
}
30+
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: "other", deviceName: "Shure MV7", devices: duplicates), "other")
31+
}
32+
33+
func testExactDecoratedDeviceNameIsPreserved() {
34+
let decorated = devices + [(id: "literal", name: "Shure MV7 (14ed:1012)")]
35+
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: nil, deviceName: "Shure MV7 (14ed:1012)", devices: decorated), "literal")
36+
}
37+
}

‎technical-documentation/testing/manual-e2e-checklist.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,3 +621,5 @@ The mask comes from the native compositor (ONNX Runtime + the vendored selfie-se
621621
| | | | | |
622622
| | | | | |
623623
| 2026-09-05 | Windows single-frame decoder ownership follow-up; source SHA-256 `FAF87561...87117`, bundled addon `94bc16d7...43a2c`, app.asar `8063a377...96f04` | Windows 11 build 26340; packaged Electron 41.2.1 | Scoped pass: nonzero AV1 seek and cross-clip playback | Fresh package, unique profile, no addon override; all 15 packaged native files hash-match staging (the existing packaging filter excludes standalone `ffmpeg.exe`). Native keyboard opened Studio after HUD pointer targeting failed; Studio seek/play/close used native input. AV1 at timeline 1.500 s/source 0.400 s rendered red `[255,24,0,255]` with a retained screenshot. During native playback from the first H.264 clip, read-only CDP samples observed blue followed by third-clip green `[0,216,0,255]` at 1.571 s; native logs confirm the malformed AV1 and following H.264 sources. PID 35472 exited 0 through window Close. An earlier sandboxed launch failed during Chromium GPU startup and is excluded from this pass. Limits: synthetic media, two native playback attempts in one successful session, no repeated stability or reporter-file claim; capture/webcam/tray/export not run. Exact-endpoint 4.540 s and one subsequent seek showed first-clip blue after native free-run wrapped: unchanged clip-identity synchronization paths, outside this resource/preflight follow-up; endpoint correctness is not claimed. Historical A/B rows remain separate. |
624+
625+
| 2026-09-13 | `6862a380` + microphone USB-label resolver patch; rebuilt release helpers | macOS 26.6.2, Apple Silicon, Shure MV7 | Partial | Real desktop HUD: selected explicit `Shure MV7 (14ed:1012)`, microphone on, system audio/camera off; started/stopped capture and reached saved editor (16.1 s). Logs confirm the rebuilt helper path and decorated label. Raw AAC is non-silent (mean -37.4 dBFS, peak -13.3 dBFS). Separately, the resolver matched the decorated label to the real AVFoundation MV7 ID. Default input was also MV7, so audio alone does not prove non-default routing. Listening, mixed/system audio, export, non-default-input recording and other platforms were not run; this is not full acceptance. |

0 commit comments

Comments
 (0)