From 177700e13cf22aae117fc168539e8c852d224d17 Mon Sep 17 00:00:00 2001 From: Leonardo Dino Date: Sat, 1 Apr 2023 03:28:17 +0100 Subject: [PATCH] :sparkles: add support for bluetooth devices --- Sources/AudioInput.swift | 30 +++++++++++++++++++++++- Sources/SFBAudioEngine/AudioDevice.swift | 17 ++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Sources/AudioInput.swift b/Sources/AudioInput.swift index 934c2c3..7978a28 100644 --- a/Sources/AudioInput.swift +++ b/Sources/AudioInput.swift @@ -8,6 +8,7 @@ public extension Notification.Name { final class AudioInput { private var callback: (Bool) -> Void + private var timer: Timer? private var devices: Set { didSet { let added = devices.subtracting(oldValue) @@ -18,10 +19,23 @@ final class AudioInput { } } } + private var hasBluetooth: Bool { + didSet { + guard hasBluetooth != oldValue else { return } + + timer?.invalidate() + guard hasBluetooth == true else { return } + + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + NotificationCenter.default.post(name: .deviceIsRunningSomewhereDidChange, object: nil) + } + } + } init(_ callback: @escaping (Bool) -> Void) { self.devices = Set() self.callback = callback + self.hasBluetooth = false } private func updateDeviceList() { @@ -30,11 +44,12 @@ final class AudioInput { guard devices != self.devices else { return } self.devices = devices + self.hasBluetooth = devices.contains(where: \.isBluetooh) } private lazy var listener = Debouncer(delay: 0.5) { [weak self] in guard let self else { return } - self.callback(self.devices.isRunningSomewhere() ?? false) + self.callback(self.isRunningSomewhere) } func startListener() { @@ -49,3 +64,16 @@ final class AudioInput { } } } + +extension AudioInput { + private func hasOrangeDot() -> Bool { + let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[CFString: Any]] + return (windows ?? []).contains { $0[kCGWindowName] as? String == "StatusIndicator" } + } + + private var isRunningSomewhere: Bool { + // FB12081267: bluetooth input devices always report that isRunningSomewhere == false + if hasBluetooth { return hasOrangeDot() } + return devices.isRunningSomewhere() ?? false + } +} diff --git a/Sources/SFBAudioEngine/AudioDevice.swift b/Sources/SFBAudioEngine/AudioDevice.swift index 894502f..39deb8f 100644 --- a/Sources/SFBAudioEngine/AudioDevice.swift +++ b/Sources/SFBAudioEngine/AudioDevice.swift @@ -48,6 +48,14 @@ public class AudioDevice: AudioObject { // MARK: - Audio Device Base Properties +extension AudioDevice { + /// Returns the transport type + /// - remark: This corresponds to the property `kAudioDevicePropertyTransportType` + public func transportType() throws -> UInt32 { + return try getProperty(PropertyAddress(kAudioDevicePropertyTransportType), type: UInt32.self) + } +} + // MARK: - Audio Device Properties extension AudioDevice { @@ -109,6 +117,15 @@ extension AudioDevice { guard let inputs else { return nil } return Set(inputs) } + + var isBluetooh: Bool { + guard let transport = try? transportType() else { return false } + switch(transport){ + case(kAudioDeviceTransportTypeBluetooth): return true + case(kAudioDeviceTransportTypeBluetoothLE): return true + default: return false + } + } } extension Set where Element == AudioDevice {