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 (#1)
This commit is contained in:
6 files changed
+38
-11
No files matched your search
@@ -24,4 +24,5 @@ A native macOS app that shows video from a capture device (camera / capture card
|
||||
- Settings live in `UserDefaults` (keys: `video`, `audio`, `autostart`, `autostartName`, `keepAwake`, `startFull`, `onTop`, `gravity`, `format.<id>`).
|
||||
- Add new strings through `NSLocalizedString` and to both `Localizable.strings` files.
|
||||
- Run `make check` before committing. Version is in `Config/Info.plist`; build number = commit count.
|
||||
- Commits: small, one per feature, with a `Co-Authored-By` line.
|
||||
- Work on a feature branch (`feature/<topic>`, `fix/<topic>`) and open a PR with `gh pr create`; do not commit straight to `main`.
|
||||
- Commits follow [Conventional Commits](https://www.conventionalcommits.org), in English: `feat:`, `fix:`, `docs:`, `refactor:`, `build:`, `chore:` (optional scope, e.g. `feat(menu):`), imperative subject under ~72 chars. Small, one per change, with a `Co-Authored-By` line.
|
||||
@@ -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