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.
499 lines
17 KiB
499 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
|
|
|
|
// 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: false)
|
|
|
|
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
|
|
do {
|
|
try passivelySetupAudioSession()
|
|
// startSilentAudio()
|
|
// 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 = 1.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 audioSession.setCategory(.ambient, mode: .default)
|
|
|
|
// 注意:我们不设置活跃状态,让系统自己管理
|
|
|
|
NSLog("\(TAG): 被动音频会话配置完成")
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 检查是否有其他音频播放
|
|
// let audioSession = AVAudioSession.sharedInstance()
|
|
// let isOtherAudioPlaying = audioSession.isOtherAudioPlaying
|
|
|
|
// if isOtherAudioPlaying {
|
|
// NSLog("\(TAG): 检测到其他音频在播放")
|
|
// // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制
|
|
// }
|
|
|
|
// // 更新播放状态(切换)
|
|
// let isCurrentlyPlaying = (silentAudioPlayer?.isPlaying ?? false)
|
|
// if isCurrentlyPlaying {
|
|
// stopSilentAudio()
|
|
// setupNowPlaying(isPlaying: false)
|
|
// } else {
|
|
// startSilentAudio()
|
|
// setupNowPlaying(isPlaying: true)
|
|
// }
|
|
|
|
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)
|
|
|
|
// 检查是否有其他音频播放
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
if audioSession.isOtherAudioPlaying {
|
|
NSLog("\(TAG): 检测到其他音频在播放")
|
|
// 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制
|
|
}
|
|
|
|
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)
|
|
|
|
// 检查是否有其他音频播放
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
if audioSession.isOtherAudioPlaying {
|
|
NSLog("\(TAG): 检测到其他音频在播放")
|
|
// 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|