iOS/Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift

202 lines
6.7 KiB
Swift

import Foundation
import PromiseKit
#if canImport(CoreMediaIO)
import AVFoundation
import CoreMediaIO
#endif
#if targetEnvironment(macCatalyst)
import CoreAudio
#endif
private class InputOutputDeviceUpdateSignaler: SensorProviderUpdateSignaler {
let signal: () -> Void
enum ObservedObjectType: Hashable {
// so iOS (which has neither CoreMediaIO nor the CoreAudio APIs we need) doesn't whine about empty enum
case invalid
#if targetEnvironment(macCatalyst)
case coreAudio(AudioObjectID)
#if canImport(CoreMediaIO)
case coreMedia(CMIOObjectID)
#endif
#endif
var id: UInt32 {
switch self {
case .invalid: return .max
#if targetEnvironment(macCatalyst)
case let .coreAudio(id): return id
#if canImport(CoreMediaIO)
case let .coreMedia(id): return id
#endif
#endif
}
}
}
private var observedObjects = Set<ObservedObjectType>()
required init(signal: @escaping () -> Void) {
self.signal = signal
#if targetEnvironment(macCatalyst)
#if canImport(CoreMediaIO)
addCoreMediaObserver(for: CMIOObjectID(kCMIOObjectSystemObject), property: .allDevices)
#endif
addCoreAudioObserver(for: AudioObjectID(kAudioObjectSystemObject), property: .allDevices)
#endif
}
private func addObserver(object: ObservedObjectType, property: some HACoreBlahProperty) {
guard !observedObjects.contains(object) else { return }
let observedStatus = property.addListener(objectID: object.id) { [weak self] in
Current.Log.info("info updated for \(object)")
self?.signal()
}
Current.Log.info("added observer for \(object): \(observedStatus)")
observedObjects.insert(object)
}
// object IDs both alias to UInt32 so we can't rely on the type system to know which method to call
#if targetEnvironment(macCatalyst)
func addCoreAudioObserver(
for id: AudioObjectID,
property: HACoreAudioProperty<some Any>
) {
addObserver(object: .coreAudio(id), property: property)
}
#if canImport(CoreMediaIO)
func addCoreMediaObserver(
for id: CMIOObjectID,
property: HACoreMediaProperty<some Any>
) {
addObserver(object: .coreMedia(id), property: property)
}
#endif
#endif
}
public class InputOutputDeviceSensor: SensorProvider {
public enum InputOutputDeviceError: Error, Equatable {
case noInputsOrOutputs
}
public let request: SensorProviderRequest
#if targetEnvironment(macCatalyst)
#if canImport(CoreMediaIO)
let cameraSystemObject: HACoreMediaObjectSystem
#endif
let audioSystemObject: HACoreAudioObjectSystem
#endif
public required init(request: SensorProviderRequest) {
self.request = request
#if targetEnvironment(macCatalyst)
#if canImport(CoreMediaIO)
self.cameraSystemObject = HACoreMediaObjectSystem()
#endif
self.audioSystemObject = HACoreAudioObjectSystem()
#endif
}
public func sensors() -> Promise<[WebhookSensor]> {
let updateSignaler: InputOutputDeviceUpdateSignaler = request.dependencies.updateSignaler(for: self)
let sensors: Promise<[WebhookSensor]>
#if canImport(CoreMediaIO) && targetEnvironment(macCatalyst)
let queue: DispatchQueue = .global(qos: .userInitiated)
sensors = firstly {
Promise<Void>.value(())
}.map(on: queue) { [cameraSystemObject, audioSystemObject] in
(cameraSystemObject.allCameras, audioSystemObject.allInputDevices, audioSystemObject.allOutputDevices)
}.get(on: queue) { cameras, audioInputs, audioOutputs in
cameras.forEach { updateSignaler.addCoreMediaObserver(for: $0.id, property: .isRunningSomewhere) }
audioInputs.forEach { updateSignaler.addCoreAudioObserver(for: $0.id, property: .isRunningSomewhere) }
audioOutputs.forEach { updateSignaler.addCoreAudioObserver(for: $0.id, property: .isRunningSomewhere) }
}.map(on: queue) { cameras, audioInputs, audioOutputs -> [WebhookSensor] in
Self.sensors(cameras: cameras, audioInputs: audioInputs, audioOutputs: audioOutputs)
}
#else
sensors = .init(error: InputOutputDeviceError.noInputsOrOutputs)
#endif
return sensors
}
#if canImport(CoreMediaIO) && targetEnvironment(macCatalyst)
private static func sensors(
cameras: [HACoreMediaObjectCamera],
audioInputs: [HACoreAudioObjectDevice],
audioOutputs: [HACoreAudioObjectDevice]
) -> [WebhookSensor] {
let cameraFallback = "Unknown Camera"
let audioInputFallback = "Unknown Audio Input"
let audioOutputFallback = "Unknown Audio Output"
return Self.sensors(
name: "Camera",
uniqueID: "camera",
iconOn: "mdi:camera",
iconOff: "mdi:camera-off",
all: cameras.map { $0.name ?? cameraFallback },
active: cameras.filter(\.isOn).map { $0.name ?? cameraFallback }
) + Self.sensors(
name: "Audio Input",
uniqueID: "microphone",
iconOn: "mdi:microphone",
iconOff: "mdi:microphone-off",
all: audioInputs.map { $0.name ?? audioInputFallback },
active: audioInputs.filter(\.isOn).map { $0.name ?? audioInputFallback }
) + Self.sensors(
name: "Audio Output",
uniqueID: "audio_output",
iconOn: "mdi:volume-high",
iconOff: "mdi:volume-low",
all: audioOutputs.map { $0.name ?? audioOutputFallback },
active: audioOutputs.filter(\.isOn).map { $0.name ?? audioOutputFallback }
)
}
private static func sensors(
name: String,
uniqueID: String,
iconOn: String,
iconOff: String,
all: [String],
active: [String]
) -> [WebhookSensor] {
let anyActive = active.isEmpty == false
return [
with(WebhookSensor(
name: "\(name) In Use",
uniqueID: "\(uniqueID)_in_use",
icon: anyActive ? iconOn : iconOff,
state: anyActive
)) {
$0.Type = "binary_sensor"
},
with(WebhookSensor(
name: "Active \(name)",
uniqueID: "active_\(uniqueID)",
icon: anyActive ? iconOn : iconOff,
state: active.first ?? "Inactive"
)) {
$0.Attributes = [
"All \(name)": all,
"Active \(name)": active,
]
},
]
}
#endif
}