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