You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
8.4 KiB
240 lines
8.4 KiB
import AVFoundation
|
|
import os.log
|
|
|
|
public enum AudioIntent {
|
|
case voice
|
|
case playback
|
|
}
|
|
|
|
public class AudioSessionHub {
|
|
|
|
public static let shared = AudioSessionHub()
|
|
|
|
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioSessionHub")
|
|
private var currentIntent: AudioIntent?
|
|
private let audioSession = AVAudioSession.sharedInstance()
|
|
private var isInterrupted = false
|
|
|
|
private init() {
|
|
setupNotifications()
|
|
}
|
|
|
|
public func begin(_ intent: AudioIntent) {
|
|
if intent == currentIntent && audioSession.isOtherAudioPlaying == false {
|
|
return
|
|
}
|
|
|
|
currentIntent = intent
|
|
|
|
do {
|
|
switch intent {
|
|
|
|
case .voice:
|
|
try audioSession.setCategory(.playAndRecord,
|
|
mode: .voiceChat,
|
|
options: [.allowBluetooth, .mixWithOthers])
|
|
|
|
// 如果有蓝牙HFP设备,强制设置为首选输入
|
|
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) {
|
|
try audioSession.setPreferredInput(bluetoothInput)
|
|
os_log("已设置蓝牙HFP为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName)
|
|
}
|
|
|
|
case .playback:
|
|
try audioSession.setCategory(.playback,
|
|
mode: .default,
|
|
options: [.allowBluetooth, .mixWithOthers])
|
|
|
|
}
|
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
|
|
|
|
os_log("音频会话已设置为:%{public}@", log: logger, type: .debug, String(describing: intent))
|
|
|
|
// 输出当前音频路由信息
|
|
let hasBluetoothDevice = hasBluetoothAudioDevice()
|
|
let hasBluetoothMic = hasBluetoothMicrophone()
|
|
let currentInput = getCurrentInputDevice()
|
|
|
|
os_log("蓝牙音频设备状态: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接")
|
|
os_log("蓝牙麦克风状态: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接")
|
|
os_log("当前输入设备: %{public}@", log: logger, type: .info, currentInput)
|
|
|
|
if hasBluetoothDevice {
|
|
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
|
|
}
|
|
} catch {
|
|
os_log("设置音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
public func appWillEnterForeground() {
|
|
if let intent = currentIntent {
|
|
begin(intent)
|
|
}
|
|
}
|
|
|
|
public func appDidEnterBackground() {
|
|
do {
|
|
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
|
|
os_log("音频会话已释放(应用进入后台)", log: logger, type: .debug)
|
|
} catch {
|
|
os_log("释放音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// 获取当前音频路由信息
|
|
public func getCurrentAudioRoute() -> String {
|
|
let currentRoute = audioSession.currentRoute
|
|
var routeInfo = "当前音频路由:\n"
|
|
|
|
// 输出设备
|
|
routeInfo += "输出设备:\n"
|
|
for output in currentRoute.outputs {
|
|
routeInfo += " - \(output.portName) (\(output.portType.rawValue))\n"
|
|
}
|
|
|
|
// 输入设备
|
|
routeInfo += "输入设备:\n"
|
|
for input in currentRoute.inputs {
|
|
routeInfo += " - \(input.portName) (\(input.portType.rawValue))\n"
|
|
}
|
|
|
|
return routeInfo
|
|
}
|
|
|
|
/// 检查是否有蓝牙音频设备连接
|
|
public func hasBluetoothAudioDevice() -> Bool {
|
|
let currentRoute = audioSession.currentRoute
|
|
|
|
// 检查输出设备
|
|
for output in currentRoute.outputs {
|
|
if output.portType == .bluetoothA2DP ||
|
|
output.portType == .bluetoothHFP ||
|
|
output.portType == .bluetoothLE {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// 检查输入设备(这是关键!)
|
|
for input in currentRoute.inputs {
|
|
if input.portType == .bluetoothHFP {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// 检查是否有蓝牙麦克风作为输入设备
|
|
public func hasBluetoothMicrophone() -> Bool {
|
|
let currentRoute = audioSession.currentRoute
|
|
|
|
for input in currentRoute.inputs {
|
|
if input.portType == .bluetoothHFP {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// 获取当前输入设备信息
|
|
public func getCurrentInputDevice() -> String {
|
|
let currentRoute = audioSession.currentRoute
|
|
|
|
if let input = currentRoute.inputs.first {
|
|
return "\(input.portName) (\(input.portType.rawValue))"
|
|
}
|
|
|
|
return "无输入设备"
|
|
}
|
|
|
|
private func setupNotifications() {
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleAudioSessionInterruption),
|
|
name: AVAudioSession.interruptionNotification,
|
|
object: nil
|
|
)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleAudioRouteChange),
|
|
name: AVAudioSession.routeChangeNotification,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
/// 处理音频中断
|
|
@objc private func handleAudioSessionInterruption(notification: Notification) {
|
|
guard let userInfo = notification.userInfo,
|
|
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
return
|
|
}
|
|
|
|
switch type {
|
|
case .began:
|
|
// 中断开始
|
|
isInterrupted = true
|
|
os_log("音频会话被中断", log: logger, type: .debug)
|
|
|
|
case .ended:
|
|
// 中断结束
|
|
isInterrupted = false
|
|
|
|
// 检查是否应该恢复
|
|
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
if options.contains(.shouldResume), let intent = currentIntent {
|
|
// 恢复之前的音频意图
|
|
begin(intent)
|
|
os_log("音频会话中断结束,已恢复", log: logger, type: .debug)
|
|
}
|
|
}
|
|
|
|
@unknown default:
|
|
os_log("未知的音频中断类型: %{public}@", log: logger, type: .error, "\(type)")
|
|
}
|
|
}
|
|
|
|
/// 处理音频路由变化
|
|
@objc private func handleAudioRouteChange(notification: Notification) {
|
|
guard let userInfo = notification.userInfo,
|
|
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
|
|
return
|
|
}
|
|
|
|
switch reason {
|
|
case .newDeviceAvailable:
|
|
os_log("新音频设备已连接", log: logger, type: .info)
|
|
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
|
|
|
|
// 重新应用当前意图以确保正确路由
|
|
if let intent = currentIntent {
|
|
begin(intent)
|
|
}
|
|
|
|
case .oldDeviceUnavailable:
|
|
os_log("音频设备已断开", log: logger, type: .info)
|
|
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
|
|
|
|
// 重新应用当前意图以确保正确路由
|
|
if let intent = currentIntent {
|
|
begin(intent)
|
|
}
|
|
|
|
case .categoryChange:
|
|
os_log("音频类别已变化", log: logger, type: .info)
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
}
|