mirror of
https://github.com/mmacierzanka/second-display.git
synced 2026-11-05 04:31:46 +01:00
feat: handle denied camera and microphone permissions
Quit with an error dialog when camera access is denied. When microphone access is denied, grey out the audio menu with an explanatory note and do not restore the saved audio device. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ece587fa2d
commit
d5f0f4b849
5 files changed
+36
-10
No files matched your search
@@ -23,3 +23,6 @@
|
||||
"Autostart" = "Autostart";
|
||||
"(not connected)" = "(not connected)";
|
||||
"About Second Display" = "About Second Display";
|
||||
"Camera access denied" = "Camera access denied";
|
||||
"Allow camera access for Second Display in System Settings > Privacy & Security > Camera, then launch it again." = "Allow camera access for Second Display in System Settings > Privacy & Security > Camera, then launch it again.";
|
||||
"Microphone access denied. Enable it in System Settings." = "Microphone access denied. Enable it in System Settings.";
|
||||
@@ -23,3 +23,6 @@
|
||||
"Autostart" = "Autostart";
|
||||
"(not connected)" = "(niepodłączone)";
|
||||
"About Second Display" = "O programie Second Display";
|
||||
"Camera access denied" = "Brak dostępu do kamery";
|
||||
"Allow camera access for Second Display in System Settings > Privacy & Security > Camera, then launch it again." = "Zezwól Second Display na dostęp do kamery w Ustawieniach systemowych > Prywatność i ochrona > Kamera i uruchom aplikację ponownie.";
|
||||
"Microphone access denied. Enable it in System Settings." = "Brak dostępu do mikrofonu. Włącz go w Ustawieniach systemowych.";
|
||||
+9
-4
@@ -21,6 +21,13 @@ final class App: NSObject, NSApplicationDelegate {
|
||||
other.activate(); return NSApp.terminate(nil)
|
||||
}
|
||||
guard !videoDevices().isEmpty else { return quitNoDevices() }
|
||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||
DispatchQueue.main.async { granted ? self.start() : self.quitCameraDenied() }
|
||||
}
|
||||
}
|
||||
|
||||
// runs once camera access is granted
|
||||
private func start() {
|
||||
window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 960, height: 540),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered, defer: false)
|
||||
@@ -50,10 +57,8 @@ final class App: NSObject, NSApplicationDelegate {
|
||||
|
||||
observeDevices()
|
||||
|
||||
AVCaptureDevice.requestAccess(for: .video) { _ in
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in
|
||||
DispatchQueue.main.async { self.restore() }
|
||||
}
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in
|
||||
DispatchQueue.main.async { self.restore() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -16,16 +16,26 @@ extension App {
|
||||
}
|
||||
}
|
||||
|
||||
func quitNoDevices() {
|
||||
func quitWithError(_ title: String, _ info: String) {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("No capture devices found", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Connect a camera or capture card and launch Second Display again.", comment: "")
|
||||
alert.messageText = title
|
||||
alert.informativeText = info
|
||||
alert.addButton(withTitle: "OK")
|
||||
NSApp.activate()
|
||||
alert.runModal()
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
func quitNoDevices() {
|
||||
quitWithError(NSLocalizedString("No capture devices found", comment: ""),
|
||||
NSLocalizedString("Connect a camera or capture card and launch Second Display again.", comment: ""))
|
||||
}
|
||||
func quitCameraDenied() {
|
||||
quitWithError(NSLocalizedString("Camera access denied", comment: ""),
|
||||
NSLocalizedString("Allow camera access for Second Display in System Settings > Privacy & Security > Camera, then launch it again.", comment: ""))
|
||||
}
|
||||
// .notDetermined is not blocked: the prompt is still to come
|
||||
var micBlocked: Bool { [.denied, .restricted].contains(AVCaptureDevice.authorizationStatus(for: .audio)) }
|
||||
|
||||
// MARK: devices
|
||||
func videoDevices() -> [AVCaptureDevice] {
|
||||
@@ -43,7 +53,7 @@ extension App {
|
||||
let preferred = [autostartID, defaults.string(forKey: "video")].compactMap { id in devices.first { $0.uniqueID == id } }
|
||||
if let d = preferred.first ?? devices.first { setVideo(d) }
|
||||
}
|
||||
if audioInput == nil, let id = defaults.string(forKey: "audio"), let d = audioDevices().first(where: { $0.uniqueID == id }) {
|
||||
if audioInput == nil, !micBlocked, let id = defaults.string(forKey: "audio"), let d = audioDevices().first(where: { $0.uniqueID == id }) {
|
||||
setAudio(d)
|
||||
} // default: audio off until chosen
|
||||
if !session.isRunning { session.startRunning() }
|
||||
|
||||
+7
-2
@@ -48,12 +48,17 @@ extension App: NSMenuDelegate {
|
||||
}
|
||||
} else if menu === audioMenu {
|
||||
menu.removeAllItems()
|
||||
let off = NSMenuItem(title: NSLocalizedString("Off", comment: ""), action: #selector(pickAudio(_:)), keyEquivalent: "")
|
||||
let pick = micBlocked ? nil : #selector(pickAudio(_:)) // no action = greyed out
|
||||
if micBlocked {
|
||||
menu.addItem(header(NSLocalizedString("Microphone access denied. Enable it in System Settings.", comment: "")))
|
||||
menu.addItem(.separator())
|
||||
}
|
||||
let off = NSMenuItem(title: NSLocalizedString("Off", comment: ""), action: pick, keyEquivalent: "")
|
||||
off.target = self; off.state = audioInput == nil ? .on : .off
|
||||
menu.addItem(off)
|
||||
menu.addItem(.separator())
|
||||
for d in audioDevices() {
|
||||
let it = NSMenuItem(title: d.localizedName, action: #selector(pickAudio(_:)), keyEquivalent: "")
|
||||
let it = NSMenuItem(title: d.localizedName, action: pick, keyEquivalent: "")
|
||||
it.target = self; it.representedObject = d
|
||||
it.state = d.uniqueID == audioInput?.device.uniqueID ? .on : .off
|
||||
menu.addItem(it)
|
||||
|
||||
Reference in new issue
Block a user