import Foundation import MediaPlayer import AVFoundation import UIKit /// 媒体按钮事件处理协议 @available(iOS 13.0, *) @objc protocol MediaButtonEventHandler: AnyObject { func sendEvent(_ event: [String: Any]) } /// 蓝牙媒体按键事件处理类 /// 该类仅在iOS 13.0及以上版本可用 @available(iOS 13.0, *) class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // MARK: - 属性 @objc static let shared = BluetoothMediaButtonHelper() /// 日志标签 private let TAG = "BluetoothMediaButtonHelper" /// 远程控制中心 private var remoteCommandCenter: MPRemoteCommandCenter /// 事件处理器 - 用于发送事件到Flutter private weak var eventHandler: MediaButtonEventHandler? /// 是否已启用监听 private var isListening = false /// 静音音频播放器 - 用于保持音频会话活跃 private var silentAudioPlayer: AVAudioPlayer? /// 播放计时器 - 用于定期更新播放信息 private var playbackTimer: Timer? /// 当前播放时间 - 模拟播放进度 private var currentPlaybackTime: TimeInterval = 0 /// 总时长 - 模拟 private let totalDuration: TimeInterval = 300.0 /// 音频会话管理器 private let audioSessionManager = AudioSessionManager.shared // MARK: - 初始化 override init() { // 获取远程控制中心单例 self.remoteCommandCenter = MPRemoteCommandCenter.shared() super.init() NSLog("\(TAG): 初始化媒体按钮处理器") // 初始化静音音频播放器 initSilentAudioPlayer() } deinit { stopButtonListening() NSLog("\(TAG): 媒体按钮处理器已释放") } // MARK: - 公共方法 /// 设置事件处理器 @objc func setEventHandler(_ handler: MediaButtonEventHandler) { eventHandler = handler NSLog("\(TAG): 已设置媒体按钮事件处理器") } /// 开始监听蓝牙媒体按钮事件 @objc func startButtonListening() -> Bool { if isListening { NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件") return true } NSLog("\(TAG): 开始监听蓝牙媒体按钮事件") // 立即注册远程命令 - 这不需要音频会话 registerRemoteCommands() // 标记为已在监听 isListening = true // 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话 setupNowPlaying(isPlaying: true) // 尝试启用静音音频播放(但即使失败,远程命令仍然可用) do { // try audioSessionManager.configureForBluetoothMediaButtons() startSilentAudio() // 延时1秒 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.setupNowPlaying(isPlaying: false) self.stopSilentAudio() } // startUpdatingPlaybackInfo() } catch { NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)") // 继续执行,因为远程命令在某些设备上不需要播放音频也能工作 } NSLog("\(TAG): 蓝牙媒体按钮监听已启动") return true } /// 停止监听蓝牙媒体按钮事件 @objc func stopButtonListening() -> Bool { if !isListening { return true } NSLog("\(TAG): 停止监听蓝牙媒体按钮事件") // 停止播放静音音频 stopSilentAudio() // 停止更新播放信息 stopUpdatingPlaybackInfo() // 取消注册远程命令 unregisterRemoteCommands() // 清除NowPlaying信息 DispatchQueue.main.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } isListening = false NSLog("\(TAG): 蓝牙媒体按钮监听已停止") return true } // MARK: - 私有方法 /// 初始化静音音频播放器 private func initSilentAudioPlayer() { do { // 尝试加载sample.mp3文件 if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") { let audioUrl = URL(fileURLWithPath: audioPath) silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl) silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放 silentAudioPlayer?.volume = 0.0 // 设置为全音量 silentAudioPlayer?.prepareToPlay() // 预加载 silentAudioPlayer?.delegate = self NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功") return } NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据") } catch { NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)") } } /// 开始播放静音音频 private func startSilentAudio() { guard let player = silentAudioPlayer else { NSLog("\(TAG): 无法开始播放,播放器未初始化") return } if !player.isPlaying { let success = player.play() if success { NSLog("\(TAG): 开始播放静音音频") } else { NSLog("\(TAG): 播放启动失败") } } else { NSLog("\(TAG): 静音音频已在播放中") } } /// 停止播放静音音频 private func stopSilentAudio() { silentAudioPlayer?.stop() NSLog("\(TAG): 停止播放静音音频") } /// 开始更新播放信息 private func startUpdatingPlaybackInfo() { // 初始设置 setupNowPlaying(isPlaying: true) // 定期更新播放信息 - 恢复定时器,确保持续更新播放状态 playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in guard let self = self else { return } // 更新播放时间(模拟) self.currentPlaybackTime += 1.0 if self.currentPlaybackTime >= self.totalDuration { self.currentPlaybackTime = 0.0 } // 更新NowPlaying信息 self.setupNowPlaying(isPlaying: true) } NSLog("\(TAG): 开始更新播放信息") } /// 停止更新播放信息 private func stopUpdatingPlaybackInfo() { playbackTimer?.invalidate() playbackTimer = nil // 清空NowPlaying信息 DispatchQueue.main.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } NSLog("\(TAG): 停止更新播放信息") } /// 设置NowPlaying信息 private func setupNowPlaying(isPlaying: Bool) { NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)") // 创建音频专辑图片 let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片 var nowPlayingInfo: [String: Any] = [ MPMediaItemPropertyTitle: "DeepVoice 语音助手", MPMediaItemPropertyArtist: "DeepVoice", MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放 MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime, MPMediaItemPropertyPlaybackDuration: totalDuration ] // 如果有专辑图片,添加到NowPlaying信息中 if let albumArt = albumArt { let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in return albumArt } nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } // 设置NowPlaying信息 (在主线程执行) DispatchQueue.main.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo NSLog("\(self.TAG): NowPlaying信息设置完成") } } /// 注册远程命令 private func registerRemoteCommands() { let commandCenter = MPRemoteCommandCenter.shared() // 播放/暂停命令 commandCenter.togglePlayPauseCommand.isEnabled = true commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) commandCenter.playCommand.isEnabled = true commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) commandCenter.pauseCommand.isEnabled = true commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) // 下一曲/上一曲命令 commandCenter.nextTrackCommand.isEnabled = true commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:))) commandCenter.previousTrackCommand.isEnabled = true commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:))) // 快进/快退命令 commandCenter.seekForwardCommand.isEnabled = true commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:))) commandCenter.seekBackwardCommand.isEnabled = true commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:))) // 停止命令 commandCenter.stopCommand.isEnabled = true commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:))) NSLog("\(TAG): 远程命令已注册") } /// 取消注册远程命令 private func unregisterRemoteCommands() { NSLog("\(TAG): 取消注册远程命令") // 基本控制命令 remoteCommandCenter.playCommand.isEnabled = false remoteCommandCenter.playCommand.removeTarget(nil) remoteCommandCenter.pauseCommand.isEnabled = false remoteCommandCenter.pauseCommand.removeTarget(nil) remoteCommandCenter.togglePlayPauseCommand.isEnabled = false remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil) remoteCommandCenter.stopCommand.isEnabled = false remoteCommandCenter.stopCommand.removeTarget(nil) } /// 发送按钮事件到Flutter private func sendButtonEvent(type: String) { // 创建事件数据 let eventData: [String: Any] = [ "type": "mediaButtonEvent", "buttonType": type, "timestamp": Int(Date().timeIntervalSince1970 * 1000) ] // 发送事件到处理器 eventHandler?.sendEvent(eventData) } // MARK: - 命令处理方法 /// 处理播放/暂停命令 @objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到播放/暂停命令") // 发送事件到Flutter sendButtonEvent(type: "play_pause") // 调用VoiceInteractionService处理媒体按钮事件 if VoiceInteractionService.shared.isServiceRunning() { NSLog("\(TAG): 转发到VoiceInteractionService处理") VoiceInteractionService.shared.handleMediaButtonAction() } return .success } /// 处理下一曲命令 @objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到下一曲命令") // 发送事件到Flutter sendButtonEvent(type: "next_track") // 也可以转发到VoiceInteractionService if VoiceInteractionService.shared.isServiceRunning() { VoiceInteractionService.shared.handleMediaButtonAction() } return .success } /// 处理上一曲命令 @objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到上一曲命令") // 发送事件到Flutter sendButtonEvent(type: "previous_track") // 也可以转发到VoiceInteractionService if VoiceInteractionService.shared.isServiceRunning() { VoiceInteractionService.shared.handleMediaButtonAction() } return .success } /// 处理快进命令 @objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到快进命令") // 发送事件到Flutter sendButtonEvent(type: "seek_forward") return .success } /// 处理快退命令 @objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到快退命令") // 发送事件到Flutter sendButtonEvent(type: "seek_backward") return .success } /// 处理停止命令 @objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { NSLog("\(TAG): 收到停止命令") // 发送事件到Flutter sendButtonEvent(type: "stop") // 调用VoiceInteractionService处理媒体按钮事件 if VoiceInteractionService.shared.isServiceRunning() { VoiceInteractionService.shared.handleMediaButtonAction() } return .success } // MARK: - AVAudioPlayerDelegate方法 /// 音频播放结束回调 func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { NSLog("\(TAG): 音频播放结束, 成功: \(flag)") // 如果仍在监听状态,重新开始播放 if isListening && player == silentAudioPlayer { player.play() } } /// 音频播放错误回调 func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { if let error = error { NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)") } else { NSLog("\(TAG): 音频解码发生未知错误") } // 重新初始化播放器 if player == silentAudioPlayer && isListening { initSilentAudioPlayer() startSilentAudio() } } /// 清理和释放资源 func dispose() { NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源") // 停止监听 stopButtonListening() // 释放播放器和计时器资源 silentAudioPlayer = nil if let timer = playbackTimer { timer.invalidate() playbackTimer = nil } // 清除引用 eventHandler = nil NSLog("\(TAG): 资源已释放") } } extension UInt32 { var data: Data { var int = self return Data(bytes: &int, count: MemoryLayout.size) } } extension UInt16 { var data: Data { var int = self return Data(bytes: &int, count: MemoryLayout.size) } }