16 changed files with 1535 additions and 973 deletions
Binary file not shown.
@ -0,0 +1,499 @@ |
|||||
|
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) |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,330 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:io' show Platform; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import '../../core/utils/logger.dart'; |
||||
|
|
||||
|
/// 蓝牙媒体按钮事件类型 |
||||
|
enum MediaButtonType { |
||||
|
/// 播放按钮 |
||||
|
play, |
||||
|
|
||||
|
/// 暂停按钮 |
||||
|
pause, |
||||
|
|
||||
|
/// 下一曲按钮 |
||||
|
next, |
||||
|
|
||||
|
/// 上一曲按钮 |
||||
|
previous, |
||||
|
|
||||
|
/// 快进按钮 |
||||
|
seekForward, |
||||
|
|
||||
|
/// 快退按钮 |
||||
|
seekBackward, |
||||
|
|
||||
|
/// 播放/暂停切换按钮 |
||||
|
togglePlayPause, |
||||
|
|
||||
|
/// 未知按钮 |
||||
|
unknown |
||||
|
} |
||||
|
|
||||
|
/// 蓝牙媒体按钮事件 |
||||
|
class MediaButtonEvent { |
||||
|
/// 按钮类型 |
||||
|
final MediaButtonType buttonType; |
||||
|
|
||||
|
/// 事件发生时间戳 |
||||
|
final int timestamp; |
||||
|
|
||||
|
MediaButtonEvent({ |
||||
|
required this.buttonType, |
||||
|
required this.timestamp, |
||||
|
}); |
||||
|
|
||||
|
factory MediaButtonEvent.fromMap(Map<dynamic, dynamic> map) { |
||||
|
MediaButtonType buttonType; |
||||
|
|
||||
|
switch (map['buttonType']) { |
||||
|
case 'play': |
||||
|
buttonType = MediaButtonType.play; |
||||
|
break; |
||||
|
case 'pause': |
||||
|
buttonType = MediaButtonType.pause; |
||||
|
break; |
||||
|
case 'next': |
||||
|
buttonType = MediaButtonType.next; |
||||
|
break; |
||||
|
case 'previous': |
||||
|
buttonType = MediaButtonType.previous; |
||||
|
break; |
||||
|
case 'seekForward': |
||||
|
buttonType = MediaButtonType.seekForward; |
||||
|
break; |
||||
|
case 'seekBackward': |
||||
|
buttonType = MediaButtonType.seekBackward; |
||||
|
break; |
||||
|
case 'togglePlayPause': |
||||
|
buttonType = MediaButtonType.togglePlayPause; |
||||
|
break; |
||||
|
default: |
||||
|
buttonType = MediaButtonType.unknown; |
||||
|
} |
||||
|
|
||||
|
return MediaButtonEvent( |
||||
|
buttonType: buttonType, |
||||
|
timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
String toString() { |
||||
|
switch (buttonType) { |
||||
|
case MediaButtonType.play: |
||||
|
return '蓝牙媒体按钮: 播放'; |
||||
|
case MediaButtonType.pause: |
||||
|
return '蓝牙媒体按钮: 暂停'; |
||||
|
case MediaButtonType.next: |
||||
|
return '蓝牙媒体按钮: 下一曲'; |
||||
|
case MediaButtonType.previous: |
||||
|
return '蓝牙媒体按钮: 上一曲'; |
||||
|
case MediaButtonType.seekForward: |
||||
|
return '蓝牙媒体按钮: 快进'; |
||||
|
case MediaButtonType.seekBackward: |
||||
|
return '蓝牙媒体按钮: 快退'; |
||||
|
case MediaButtonType.togglePlayPause: |
||||
|
return '蓝牙媒体按钮: 播放/暂停'; |
||||
|
case MediaButtonType.unknown: |
||||
|
return '蓝牙媒体按钮: 未知'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 蓝牙媒体按钮服务 |
||||
|
class BluetoothMediaButtonService extends GetxService { |
||||
|
static BluetoothMediaButtonService get to => Get.find<BluetoothMediaButtonService>(); |
||||
|
|
||||
|
// 方法通道名称 |
||||
|
final MethodChannel _channel = const MethodChannel('com.deep_voice.bluetooth_media_button'); |
||||
|
|
||||
|
// 事件通道名称 |
||||
|
final EventChannel _eventChannel = const EventChannel('com.deep_voice.bluetooth_media_button_events'); |
||||
|
|
||||
|
/// 按钮事件流控制器 |
||||
|
final _buttonEventController = StreamController<MediaButtonEvent>.broadcast(); |
||||
|
|
||||
|
/// 按钮事件流 |
||||
|
Stream<MediaButtonEvent> get buttonEvents => _buttonEventController.stream; |
||||
|
|
||||
|
/// 是否已启用监听 |
||||
|
final _isListening = false.obs; |
||||
|
bool get isListening => _isListening.value; |
||||
|
|
||||
|
/// 是否正在加载 |
||||
|
final isLoading = false.obs; |
||||
|
|
||||
|
/// 错误信息 |
||||
|
final errorMessage = ''.obs; |
||||
|
|
||||
|
/// 按钮事件流订阅 |
||||
|
StreamSubscription? _buttonEventSubscription; |
||||
|
|
||||
|
/// 服务是否已初始化 |
||||
|
bool _isInitialized = false; |
||||
|
|
||||
|
/// 是否支持媒体按钮服务(iOS 13.0及以上) |
||||
|
bool get isSupported { |
||||
|
if (!Platform.isIOS) return true; // 非iOS平台默认支持 |
||||
|
|
||||
|
// 获取平台版本信息 |
||||
|
final String version = Platform.operatingSystemVersion; |
||||
|
|
||||
|
// 尝试提取iOS版本号 (格式如: "Version 14.5 (Build 18E182)") |
||||
|
try { |
||||
|
// 提取版本号 |
||||
|
final RegExp versionRegex = RegExp(r'Version\s+(\d+)\.(\d+)'); |
||||
|
final match = versionRegex.firstMatch(version); |
||||
|
|
||||
|
if (match != null) { |
||||
|
final int majorVersion = int.parse(match.group(1) ?? '0'); |
||||
|
return majorVersion >= 13; // iOS 13及以上支持 |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('解析iOS版本失败: $e'); |
||||
|
} |
||||
|
|
||||
|
// 无法确定版本时,假设不支持 |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
BluetoothMediaButtonService() { |
||||
|
Logger.info('创建 BluetoothMediaButtonService 实例'); |
||||
|
} |
||||
|
|
||||
|
/// 初始化媒体按钮服务 |
||||
|
Future<bool> initialize() async { |
||||
|
if (_isInitialized) { |
||||
|
Logger.info('BluetoothMediaButtonService 已经初始化'); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 检查系统版本是否支持 |
||||
|
if (!isSupported) { |
||||
|
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
Logger.info('正在初始化 BluetoothMediaButtonService...'); |
||||
|
|
||||
|
// 设置事件监听器 |
||||
|
_setupButtonEventListener(); |
||||
|
|
||||
|
_isInitialized = true; |
||||
|
Logger.info('BluetoothMediaButtonService 初始化成功'); |
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
Logger.error('初始化 BluetoothMediaButtonService 失败: $e'); |
||||
|
errorMessage.value = '初始化蓝牙媒体按钮服务失败'; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置按钮事件监听器 |
||||
|
void _setupButtonEventListener() { |
||||
|
_buttonEventSubscription?.cancel(); |
||||
|
|
||||
|
// 设置事件通道监听器 |
||||
|
_buttonEventSubscription = _eventChannel |
||||
|
.receiveBroadcastStream() |
||||
|
.listen(_handleButtonEvent, onError: _handleEventStreamError); |
||||
|
|
||||
|
Logger.info('已设置蓝牙媒体按钮事件监听器'); |
||||
|
} |
||||
|
|
||||
|
/// 处理按钮事件 |
||||
|
void _handleButtonEvent(dynamic event) { |
||||
|
Logger.info('BluetoothMediaButtonService 收到原始事件: $event'); |
||||
|
|
||||
|
if (event is! Map) { |
||||
|
Logger.error('收到无效的蓝牙媒体按钮事件 (非Map类型): $event'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
final Map<dynamic, dynamic> eventMap = event; |
||||
|
Logger.info('蓝牙媒体按钮事件Map: $eventMap'); |
||||
|
|
||||
|
try { |
||||
|
if (eventMap['type'] == 'mediaButtonEvent') { |
||||
|
final buttonEvent = MediaButtonEvent.fromMap(eventMap); |
||||
|
|
||||
|
// 广播事件 |
||||
|
_buttonEventController.add(buttonEvent); |
||||
|
|
||||
|
Logger.info('成功处理蓝牙媒体按钮事件: $buttonEvent'); |
||||
|
} else { |
||||
|
Logger.warning('收到未知类型的事件: ${eventMap['type']}'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('处理蓝牙媒体按钮事件时出错: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理事件流错误 |
||||
|
void _handleEventStreamError(dynamic error) { |
||||
|
Logger.error('蓝牙媒体按钮事件流错误: $error'); |
||||
|
errorMessage.value = '蓝牙媒体按钮监听出错'; |
||||
|
} |
||||
|
|
||||
|
/// 开始监听蓝牙媒体按钮事件 |
||||
|
Future<bool> startButtonListening() async { |
||||
|
// 检查系统版本是否支持 |
||||
|
if (!isSupported) { |
||||
|
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!_isInitialized && !(await initialize())) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (_isListening.value) { |
||||
|
Logger.info('已经在监听蓝牙媒体按钮事件'); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
isLoading.value = true; |
||||
|
|
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<bool>('startButtonListening') ?? false; |
||||
|
|
||||
|
if (result) { |
||||
|
_isListening.value = true; |
||||
|
Logger.info('开始监听蓝牙媒体按钮事件成功'); |
||||
|
} else { |
||||
|
errorMessage.value = '开始监听蓝牙媒体按钮事件失败'; |
||||
|
Logger.error('开始监听蓝牙媒体按钮事件失败'); |
||||
|
} |
||||
|
|
||||
|
isLoading.value = false; |
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
isLoading.value = false; |
||||
|
errorMessage.value = '开始监听蓝牙媒体按钮事件出错: $e'; |
||||
|
Logger.error('开始监听蓝牙媒体按钮事件出错: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停止监听蓝牙媒体按钮事件 |
||||
|
Future<bool> stopButtonListening() async { |
||||
|
if (!_isInitialized) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!_isListening.value) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
isLoading.value = true; |
||||
|
|
||||
|
try { |
||||
|
final result = await _channel.invokeMethod<bool>('stopButtonListening') ?? false; |
||||
|
|
||||
|
if (result) { |
||||
|
_isListening.value = false; |
||||
|
Logger.info('停止监听蓝牙媒体按钮事件成功'); |
||||
|
} else { |
||||
|
errorMessage.value = '停止监听蓝牙媒体按钮事件失败'; |
||||
|
Logger.error('停止监听蓝牙媒体按钮事件失败'); |
||||
|
} |
||||
|
|
||||
|
isLoading.value = false; |
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
isLoading.value = false; |
||||
|
errorMessage.value = '停止监听蓝牙媒体按钮事件出错: $e'; |
||||
|
Logger.error('停止监听蓝牙媒体按钮事件出错: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 释放资源 |
||||
|
void dispose() { |
||||
|
_buttonEventSubscription?.cancel(); |
||||
|
_buttonEventSubscription = null; |
||||
|
_buttonEventController.close(); |
||||
|
_isInitialized = false; |
||||
|
_isListening.value = false; |
||||
|
Logger.info('BluetoothMediaButtonService 已释放'); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void onClose() { |
||||
|
dispose(); |
||||
|
super.onClose(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue