import AVFoundation import VoiceCatCore @MainActor final class AudioSessionManager { static let shared = AudioSessionManager() weak var client: VoiceCatClient? func configure() { let session = AVAudioSession.sharedInstance() do { try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker, .mixWithOthers]) } catch { print("[AudioSession] setCategory failed: \(error)") } NotificationCenter.default.addObserver( self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil) } func activateForStreaming() throws { try AVAudioSession.sharedInstance().setActive(true, options: []) } func deactivateAfterStreaming() { try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } @objc private func handleInterruption(_ notification: Notification) { guard let info = notification.userInfo, let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } switch type { case .began: client?.audioSuspend() case .ended: let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0 let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) if options.contains(.shouldResume) { try? AVAudioSession.sharedInstance().setActive(true) client?.audioResume() } @unknown default: break } } @objc private func handleRouteChange(_ notification: Notification) { NotificationCenter.default.post(name: .voiceCatDeviceListChanged, object: nil) } } extension Notification.Name { static let voiceCatDeviceListChanged = Notification.Name("cat.voice.deviceListChanged") }