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.

498 lines
17 KiB

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: - 属性
/// 日志标签
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 passivelySetupAudioSession()
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(_:)))
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.nextTrackCommand.isEnabled = false
remoteCommandCenter.nextTrackCommand.removeTarget(nil)
remoteCommandCenter.previousTrackCommand.isEnabled = false
remoteCommandCenter.previousTrackCommand.removeTarget(nil)
remoteCommandCenter.seekForwardCommand.isEnabled = false
remoteCommandCenter.seekForwardCommand.removeTarget(nil)
remoteCommandCenter.seekBackwardCommand.isEnabled = false
remoteCommandCenter.seekBackwardCommand.removeTarget(nil)
}
/// 被动设置音频会话 - 只读取现有配置,不做侵入性修改
private func passivelySetupAudioSession() throws {
let audioSession = AVAudioSession.sharedInstance()
// 如果会话已经活跃,我们完全不修改它
if audioSession.isOtherAudioPlaying || audioSession.isInputAvailable {
NSLog("\(TAG): 检测到活跃的音频会话,不进行任何修改")
return
}
// 检查应用是否已经拥有音频会话
if audioSession.category != .ambient && audioSession.category != .soloAmbient {
// 应用已经有一个有意义的类别,不要修改它
NSLog("\(TAG): 检测到应用已设置音频会话类别: \(audioSession.category)")
return
}
NSLog("\(TAG): 当前没有活跃的音频,设置最小化会话配置")
// 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configure(category: .ambient, mode: .default, options: [])
NSLog("\(TAG): 被动音频会话配置完成")
}
/// 检查是否有其他应用正在播放音频
private func isOtherAudioPlaying() -> Bool {
return AVAudioSession.sharedInstance().isOtherAudioPlaying
}
// MARK: - 命令处理方法
/// 处理播放/暂停命令
@objc func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到播放/暂停命令")
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": "playPause",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
return .success
}
/// 处理下一曲命令
@objc func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到下一曲命令")
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": "next",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
return .success
}
/// 处理上一曲命令
@objc func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到上一曲命令")
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": "previous",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
return .success
}
/// 处理快进命令
@objc func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快进命令")
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": "seekForward",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
// 模拟快进
currentPlaybackTime += 10.0
if currentPlaybackTime > totalDuration {
currentPlaybackTime = totalDuration
}
setupNowPlaying(isPlaying: true)
return .success
}
/// 处理快退命令
@objc func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快退命令")
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": "seekBackward",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
// 模拟快退
currentPlaybackTime -= 10.0
if currentPlaybackTime < 0 {
currentPlaybackTime = 0
}
setupNowPlaying(isPlaying: true)
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<UInt32>.size)
}
}
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}