Skip to content

Commit 22d3006

Browse files
committed
feat: microphone device picker
The webcam had a device picker but the mic was hardcoded to the system default. Settings' Audio section now offers a microphone picker (persisted like the camera choice) and the capture pipeline resolves the selected device by uniqueID, falling back to the system default when the device disappears.
1 parent b08d098 commit 22d3006

4 files changed

Lines changed: 39 additions & 2 deletions

File tree

CineScreen/App/AppState.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ final class AppState {
2424
var captureCamera: Bool = false
2525
/// AVCaptureDevice uniqueID for the selected webcam. nil = system default.
2626
var selectedCameraID: String? = nil
27+
/// AVCaptureDevice uniqueID for the selected microphone. nil = default.
28+
var selectedMicID: String? = nil
2729

2830
// Permissions snapshot (refreshed on a timer and after each request)
2931
var permissions: PermissionStatus
@@ -60,6 +62,7 @@ final class AppState {
6062
static let captureMic = "cs.captureMic"
6163
static let captureCamera = "cs.captureCamera"
6264
static let selectedCameraID = "cs.selectedCameraID"
65+
static let selectedMicID = "cs.selectedMicID"
6366
}
6467

6568
private func loadSettings() {
@@ -73,6 +76,7 @@ final class AppState {
7376
captureMic = d.bool(forKey: Keys.captureMic)
7477
captureCamera = d.bool(forKey: Keys.captureCamera)
7578
selectedCameraID = d.string(forKey: Keys.selectedCameraID)
79+
selectedMicID = d.string(forKey: Keys.selectedMicID)
7680
}
7781

7882
func saveProjectsDirectory(_ url: URL) {
@@ -115,6 +119,15 @@ final class AppState {
115119
}
116120
}
117121

122+
func saveSelectedMicID(_ id: String?) {
123+
selectedMicID = id
124+
if let id = id {
125+
UserDefaults.standard.set(id, forKey: Keys.selectedMicID)
126+
} else {
127+
UserDefaults.standard.removeObject(forKey: Keys.selectedMicID)
128+
}
129+
}
130+
118131
// MARK: - Permissions
119132

120133
func refreshPermissions() {

CineScreen/Capture/ScreenCaptureService.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ struct CaptureRequest {
1717
var quality: Quality = .medium
1818
var captureSystemAudio: Bool = false
1919
var captureMic: Bool = false
20+
/// AVCaptureDevice uniqueID for the microphone. `nil` = system default.
21+
var micDeviceID: String? = nil
2022
var captureCamera: Bool = false
2123
/// AVCaptureDevice uniqueID. `nil` = system default camera.
2224
var cameraDeviceID: String? = nil
@@ -343,7 +345,7 @@ final class ScreenCaptureService: NSObject {
343345
}
344346
writer.add(m)
345347
mInput = m
346-
(session, micDelegate) = try Self.makeMicSession(target: m, queue: micQueue, getSessionStart: { [weak self] in
348+
(session, micDelegate) = try Self.makeMicSession(target: m, queue: micQueue, deviceID: request.micDeviceID, getSessionStart: { [weak self] in
347349
self?.streamOutput?.sessionStartTime
348350
})
349351
}
@@ -575,14 +577,25 @@ final class ScreenCaptureService: NSObject {
575577
]
576578
}
577579

580+
/// Discoverable microphones for the Settings picker.
581+
static func availableMicrophones() -> [AVCaptureDevice] {
582+
AVCaptureDevice.DiscoverySession(
583+
deviceTypes: [.microphone],
584+
mediaType: .audio,
585+
position: .unspecified
586+
).devices
587+
}
588+
578589
private static func makeMicSession(
579590
target: AVAssetWriterInput,
580591
queue: DispatchQueue,
592+
deviceID: String?,
581593
getSessionStart: @escaping () -> CMTime?
582594
) throws -> (AVCaptureSession, MicOutputDelegate) {
583595
let session = AVCaptureSession()
584596
session.beginConfiguration()
585-
guard let device = AVCaptureDevice.default(for: .audio) else {
597+
let picked = deviceID.flatMap { AVCaptureDevice(uniqueID: $0) }
598+
guard let device = picked ?? AVCaptureDevice.default(for: .audio) else {
586599
throw CaptureError.missingMicDevice
587600
}
588601
let input: AVCaptureDeviceInput

CineScreen/ControlBar/ControlBarView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ struct ControlBarView: View {
277277
quality: state.quality,
278278
captureSystemAudio: state.captureSystemAudio,
279279
captureMic: state.captureMic && state.permissions.microphone == .granted,
280+
micDeviceID: state.selectedMicID,
280281
captureCamera: state.captureCamera && state.permissions.camera == .granted,
281282
cameraDeviceID: state.selectedCameraID,
282283
webcamURL: project.fallbackWebcamURL

CineScreen/MainWindow/SettingsView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ struct SettingsView: View {
108108
.onChange(of: state.captureMic) { _, new in
109109
state.saveCaptureMic(new)
110110
}
111+
Picker("Microphone", selection: Binding(
112+
get: { state.selectedMicID ?? "" },
113+
set: { state.saveSelectedMicID($0.isEmpty ? nil : $0) }
114+
)) {
115+
Text("System default").tag("")
116+
ForEach(ScreenCaptureService.availableMicrophones(), id: \.uniqueID) { device in
117+
Text(device.localizedName).tag(device.uniqueID)
118+
}
119+
}
120+
.disabled(state.permissions.microphone != .granted)
111121
}
112122
Section("Webcam") {
113123
Toggle("Record webcam by default", isOn: $state.captureCamera)

0 commit comments

Comments
 (0)