Browse Source

修复ios电话结束 回复音乐

weicu
liwei1dao 1 year ago
parent
commit
02b4913910
  1. 60
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift
  2. 759
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

60
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift

@ -470,6 +470,13 @@ public class AudioSessionHub {
os_log("🚫 音频会话中断开始", log: logger, type: .info) os_log("🚫 音频会话中断开始", log: logger, type: .info)
os_log(" - 中断状态: %{public}@", log: logger, type: .info, isInterrupted ? "是" : "否") os_log(" - 中断状态: %{public}@", log: logger, type: .info, isInterrupted ? "是" : "否")
// 发送中断开始通知给其他服务
NotificationCenter.default.post(
name: NSNotification.Name("AudioSessionInterruptionBegan"),
object: nil,
userInfo: ["type": "began"]
)
case .ended: case .ended:
// 中断结束 // 中断结束
isInterrupted = false isInterrupted = false
@ -480,16 +487,43 @@ public class AudioSessionHub {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
os_log("中断选项: %d, 应该恢复: %{public}@", log: logger, type: .info, optionsValue, options.contains(.shouldResume) ? "是" : "否") os_log("中断选项: %d, 应该恢复: %{public}@", log: logger, type: .info, optionsValue, options.contains(.shouldResume) ? "是" : "否")
if options.contains(.shouldResume), let intent = currentIntent { // 发送中断结束通知给其他服务,包含恢复建议
os_log("恢复之前的音频意图: %{public}@", log: logger, type: .info, String(describing: intent)) NotificationCenter.default.post(
// 恢复之前的音频意图 name: NSNotification.Name("AudioSessionInterruptionEnded"),
//begin(intent) object: nil,
os_log("✓ 音频会话中断结束,已恢复", log: logger, type: .info) userInfo: [
"type": "ended",
"shouldResume": options.contains(.shouldResume),
"options": optionsValue
]
)
if options.contains(.shouldResume) {
os_log("系统建议恢复音频会话", log: logger, type: .info)
// 延迟一段时间后发送恢复通知,让MusicService处理音频会话设置
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
os_log("音频中断结束,发送恢复通知", log: self.logger, type: .info)
// 直接发送恢复播放通知,让MusicService自己处理音频会话
self.resumeMusicServiceIfNeeded()
}
} else { } else {
os_log("不需要恢复音频会话", log: logger, type: .info) os_log("不需要恢复音频会话", log: logger, type: .info)
} }
} else { } else {
os_log("⚠️ 中断通知中无选项信息", log: logger, type: .default) os_log("⚠️ 中断通知中无选项信息", log: logger, type: .default)
// 即使没有选项信息,也发送通知
NotificationCenter.default.post(
name: NSNotification.Name("AudioSessionInterruptionEnded"),
object: nil,
userInfo: [
"type": "ended",
"shouldResume": false,
"options": 0
]
)
} }
@unknown default: @unknown default:
@ -736,6 +770,22 @@ public class AudioSessionHub {
} }
} }
/// 尝试恢复MusicService播放
private func resumeMusicServiceIfNeeded() {
os_log("🎵 [AudioSessionHub] 开始恢复MusicService播放流程", log: logger, type: .info)
// 不在AudioSessionHub中设置音频会话类别,让MusicService自己处理
// 直接发送恢复播放的通知,让MusicService处理音频会话设置
DispatchQueue.main.async {
NotificationCenter.default.post(
name: NSNotification.Name("AudioSessionHub_ResumeMusicPlayback"),
object: nil,
userInfo: ["shouldResume": true]
)
os_log("🎵 [AudioSessionHub] 已发送恢复播放通知", log: self.logger, type: .info)
}
}
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }

759
local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

@ -32,10 +32,19 @@ public class MusicService: NSObject {
// 当前播放的歌曲ID // 当前播放的歌曲ID
private var currentIdInternal: Int64? = nil private var currentIdInternal: Int64? = nil
// 音频中断处理相关
private var interruptedDuringPlayback: Bool = false
private override init() { private override init() {
super.init() super.init()
// 确保应用能接收远程控制事件
UIApplication.shared.beginReceivingRemoteControlEvents()
setupRemoteCommandCenter() setupRemoteCommandCenter()
setupAudioSession() setupAudioSession()
print("🎵 [MusicService] 🚀 MusicService初始化完成,远程控制已激活")
} }
public func initialize(serviceUrl: String, userToken: String) { public func initialize(serviceUrl: String, userToken: String) {
@ -47,17 +56,80 @@ public class MusicService: NSObject {
// MARK: - 音频会话设置 // MARK: - 音频会话设置
private func setupAudioSession() { private func setupAudioSession() {
do { print("🎵 [MusicService] 🚀 开始设置音频会话和通知监听器")
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP]) // 先注册通知监听器,不依赖音频会话设置成功
try audioSession.setActive(true) setupNotificationObservers()
// 然后尝试设置音频会话
setupAudioSessionConfiguration()
// 确保媒体控制中心可以接收到我们的应用 // 确保媒体控制中心可以接收到我们的应用
setupRemoteCommandCenter() setupRemoteCommandCenter()
print("音频会话和媒体控制设置成功") print("🎵 [MusicService] 📋 所有通知观察者注册完成")
print("🎵 [MusicService] 🔍 当前MusicService实例: \(Unmanaged.passUnretained(self).toOpaque())")
}
private func setupNotificationObservers() {
// 监听系统音频中断(来电/通话/Siri等)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
print("🎵 [MusicService] ✅ 已注册系统音频中断通知")
// 注册AudioSessionHub的自定义中断通知
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionHubInterruption(_:)),
name: NSNotification.Name("AudioSessionInterruptionBegan"),
object: nil
)
print("🎵 [MusicService] ✅ 已注册AudioSessionInterruptionBegan通知")
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionHubInterruptionEnded(_:)),
name: NSNotification.Name("AudioSessionInterruptionEnded"),
object: nil
)
print("🎵 [MusicService] ✅ 已注册AudioSessionInterruptionEnded通知")
// 注册AudioSessionHub的恢复播放通知
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionHubResumeMusicPlayback(_:)),
name: NSNotification.Name("AudioSessionHub_ResumeMusicPlayback"),
object: nil
)
print("🎵 [MusicService] ✅ 已注册AudioSessionHub_ResumeMusicPlayback通知")
}
private func setupAudioSessionConfiguration() {
do {
let audioSession = AVAudioSession.sharedInstance()
// 使用更简单的配置,避免参数冲突
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
print("🎵 [MusicService] ✅ 音频会话类别设置成功")
// 延迟激活音频会话,避免与其他服务冲突
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
do {
try audioSession.setActive(true)
print("🎵 [MusicService] ✅ 音频会话激活成功")
} catch {
print("🎵 [MusicService] ⚠️ 音频会话激活失败: \(error)")
// 激活失败不影响通知系统工作
}
}
} catch { } catch {
print("设置音频会话失败: \(error)") print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)")
print("🎵 [MusicService] ℹ️ 音频会话设置失败不影响通知系统工作")
} }
} }
@ -357,6 +429,16 @@ public class MusicService: NSObject {
print("开始播放: \(url), ID: \(currentIdInternal ?? 0)") print("开始播放: \(url), ID: \(currentIdInternal ?? 0)")
// 强制设置音频会话为活跃状态
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: [])
print("🎵 [StartPlayback] ✅ 音频会话强制激活成功")
} catch {
print("🎵 [StartPlayback] ⚠️ 音频会话设置失败: \(error)")
}
// 检查是否已在播放列表中 // 检查是否已在播放列表中
let existingIndex = playlist.firstIndex { existingSong in let existingIndex = playlist.firstIndex { existingSong in
if let existingId = existingSong["id"] as? NSNumber, if let existingId = existingSong["id"] as? NSNumber,
@ -389,9 +471,12 @@ public class MusicService: NSObject {
// 立即设置媒体信息,即使播放器还未准备就绪 // 立即设置媒体信息,即使播放器还未准备就绪
updateNowPlayingInfo(song: song) updateNowPlayingInfo(song: song)
// 确保远程控制中心已设置 // 强制重新设置远程控制中心
setupRemoteCommandCenter() setupRemoteCommandCenter()
// 强制激活远程控制
UIApplication.shared.beginReceivingRemoteControlEvents()
print("播放器已创建,等待加载完成...") print("播放器已创建,等待加载完成...")
} }
@ -401,6 +486,12 @@ public class MusicService: NSObject {
isPlaying = false isPlaying = false
updatePlaybackState() updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: false) updateMediaSessionPlaybackState(isPlaying: false)
// 更新媒体信息,保持通知栏显示但状态为暂停
if currentIndex < playlist.count {
updateNowPlayingInfo(song: playlist[currentIndex])
}
stopProgressTimer() stopProgressTimer()
print("已暂停") print("已暂停")
} }
@ -410,6 +501,12 @@ public class MusicService: NSObject {
isPlaying = true isPlaying = true
updatePlaybackState() updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true) updateMediaSessionPlaybackState(isPlaying: true)
// 重新设置媒体信息,确保通知栏显示
if currentIndex < playlist.count {
updateNowPlayingInfo(song: playlist[currentIndex])
}
startProgressTimer() startProgressTimer()
print("继续播放") print("继续播放")
} }
@ -527,204 +624,221 @@ public class MusicService: NSObject {
if completed { if completed {
print("跳转到位置: \(positionMs)ms") print("跳转到位置: \(positionMs)ms")
// 如果之前是播放状态,确保继续播放 // 如果之前在播放,跳转后继续播放
if wasPlaying { if wasPlaying {
self?.player?.play() self?.resume()
self?.isPlaying = true
self?.updateMediaSessionPlaybackState(isPlaying: true)
self?.startProgressTimer()
} }
} else {
// 立即更新进度信息到系统媒体控制中心 print("跳转失败")
self?.updateProgressInfoImmediately()
// 通知Flutter层
self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs])
} }
} }
} }
// MARK: - 媒体会话控制 // MARK: - 远程控制中心设置
private func setupRemoteCommandCenter() { private func setupRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { [unowned self] _ in // 清除之前的目标,避免重复注册
self.resume() commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.nextTrackCommand.removeTarget(nil)
commandCenter.previousTrackCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
// 播放命令
commandCenter.playCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 播放命令")
self?.resume()
return .success return .success
} }
commandCenter.pauseCommand.addTarget { [unowned self] _ in // 暂停命令
self.pause() commandCenter.pauseCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 暂停命令")
self?.pause()
return .success return .success
} }
commandCenter.nextTrackCommand.addTarget { [unowned self] _ in // 下一首命令
self.next() commandCenter.nextTrackCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 下一首命令")
self?.playNext()
return .success return .success
} }
commandCenter.previousTrackCommand.addTarget { [unowned self] _ in // 上一首命令
self.previous() commandCenter.previousTrackCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 上一首命令")
self?.playPrevious()
return .success return .success
} }
commandCenter.stopCommand.addTarget { [unowned self] _ in // 进度调整命令(iOS 9.1+)
self.stop() if #available(iOS 9.1, *) {
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent {
let positionMs = Int(positionEvent.positionTime * 1000)
print("🎵 [RemoteControl] 跳转到 \(positionMs)ms")
self?.seekTo(positionMs: positionMs)
return .success return .success
} }
return .commandFailed
}
commandCenter.changePlaybackPositionCommand.isEnabled = true
} }
private func updateNowPlayingInfo(song: [String: Any]) { // 启用远程控制命令
let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" commandCenter.playCommand.isEnabled = true
let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手" commandCenter.pauseCommand.isEnabled = true
let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" commandCenter.nextTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.isEnabled = true
var nowPlayingInfo: [String: Any] = [ // 确保音频会话支持远程控制
MPMediaItemPropertyTitle: title, do {
MPMediaItemPropertyArtist: artist let audioSession = AVAudioSession.sharedInstance()
] try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
print("🎵 [RemoteControl] 音频会话配置成功")
} catch {
print("🎵 [RemoteControl] ⚠️ 音频会话配置失败: \(error)")
}
// 异步加载封面图片 print("🎵 [RemoteControl] ✅ 远程控制中心设置完成")
if !coverUrl.isEmpty, let url = URL(string: coverUrl) { }
print("开始加载封面图片: \(coverUrl)")
DispatchQueue.global(qos: .background).async { // MARK: - 媒体信息更新
// 创建带超时的URLRequest private func updateNowPlayingInfo(song: [String: Any]) {
var request = URLRequest(url: url) var nowPlayingInfo = [String: Any]()
request.timeoutInterval = 10.0
URLSession.shared.dataTask(with: request) { data, response, error in // 设置歌曲信息
if let error = error { nowPlayingInfo[MPMediaItemPropertyTitle] = song["name"] as? String ?? "未知歌曲"
print("图片加载失败: \(error.localizedDescription)") nowPlayingInfo[MPMediaItemPropertyArtist] = song["singer"] as? String ?? "未知歌手"
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo // 设置专辑信息(如果有的话)
if let album = song["album"] as? String, !album.isEmpty {
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
} }
return
// 设置播放时间信息
if let player = player, let currentItem = player.currentItem {
let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration)
if !currentTime.isNaN && !currentTime.isInfinite {
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
} }
guard let data = data, let image = UIImage(data: data) else { if !duration.isNaN && !duration.isInfinite && duration > 0 {
print("图片数据无效或格式不支持") nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
return
} }
print("图片加载成功") // 设置播放速率
DispatchQueue.main.async { nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
// 设置媒体类型为音频
nowPlayingInfo[MPMediaItemPropertyMediaType] = MPMediaType.music.rawValue
// 强制更新媒体信息
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}.resume() // 确保在主线程上再次设置,解决某些iOS版本的兼容性问题
} DispatchQueue.main.async {
} else {
print("封面图片URL为空")
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
print("🎵 [MediaInfo] ✅ 已更新媒体信息: \(song["name"] as? String ?? "未知")")
print("🎵 [MediaInfo] 📱 播放状态: \(isPlaying ? "播放中" : "暂停")")
} }
private func updatePlaybackState() { private func updatePlaybackState() {
// iOS manages playback state automatically, but you can update NowPlayingInfo if needed guard let flutterChannel = flutterChannel else { return }
}
// MARK: - 新增媒体会话功能 let progressInfo = getProgress()
/// 更新媒体会话进度信息 flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: [
public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { "isPlaying": isPlaying,
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] "currentPosition": progressInfo["currentPosition"] ?? 0,
"duration": progressInfo["duration"] ?? 0,
"progress": progressInfo["progress"] ?? 0.0
])
}
// 更新播放时长和当前位置 public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0 guard let flutterChannel = flutterChannel else { return }
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo flutterChannel.invokeMethod("onProgressChanged", arguments: [
"currentPosition": currentPosition,
"duration": duration,
"progress": progress
])
} }
/// 启用媒体会话回调
public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) {
flutterChannel = channel self.flutterChannel = channel
setupEnhancedRemoteCommandCenter()
startProgressTimer()
print("媒体会话回调已启用") print("媒体会话回调已启用")
} }
/// 更新媒体会话元数据
public func updateMediaSessionMetadata(song: [String: Any]) { public func updateMediaSessionMetadata(song: [String: Any]) {
let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" guard let flutterChannel = flutterChannel else { return }
let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手"
let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? ""
// 更新当前歌曲ID // 构建媒体元数据
if let id = song["id"] as? NSNumber { var metadata: [String: Any] = [:]
currentIdInternal = id.int64Value
// 基本信息
if let id = song["id"] {
metadata["id"] = id
}
if let name = song["name"] as? String {
metadata["title"] = name
}
if let singer = song["singer"] as? String {
metadata["artist"] = singer
}
if let image = song["image"] as? String {
metadata["artUri"] = image
} }
print("updateMediaSessionMetadata currentIdInternal: \(currentIdInternal ?? 0)") // 播放状态
metadata["isPlaying"] = isPlaying
var nowPlayingInfo: [String: Any] = [ // 进度信息
MPMediaItemPropertyTitle: title, let progressInfo = getProgress()
MPMediaItemPropertyArtist: artist metadata["currentPosition"] = progressInfo["currentPosition"] ?? 0
] metadata["duration"] = progressInfo["duration"] ?? 0
metadata["progress"] = progressInfo["progress"] ?? 0.0
// 异步加载封面图片 // 播放列表信息
if !coverUrl.isEmpty, let url = URL(string: coverUrl) { metadata["playlistSize"] = playlist.count
print("开始加载封面图片: \(coverUrl)") metadata["currentIndex"] = currentIndex
DispatchQueue.global(qos: .background).async { // 能力标志
// 创建带超时的URLRequest metadata["canSkipToNext"] = currentIndex < playlist.count - 1
var request = URLRequest(url: url) metadata["canSkipToPrevious"] = currentIndex > 0
request.timeoutInterval = 10.0 metadata["canSeek"] = true
URLSession.shared.dataTask(with: request) { data, response, error in flutterChannel.invokeMethod("onMetadataChanged", arguments: metadata)
if let error = error {
print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
guard let data = data, let image = UIImage(data: data) else { print("已更新媒体元数据: \(metadata)")
print("图片数据无效或格式不支持")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
} }
print("图片加载成功") public func updateMediaSessionPlaybackState(isPlaying: Bool) {
DispatchQueue.main.async { guard let flutterChannel = flutterChannel else { return }
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}.resume()
}
} else {
print("封面图片URL为空")
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
print("媒体会话元数据已更新: \(title) - \(artist)") let progressInfo = getProgress()
}
/// 更新媒体会话播放状态 let playbackState: [String: Any] = [
public func updateMediaSessionPlaybackState(isPlaying: Bool) { "isPlaying": isPlaying,
self.isPlaying = isPlaying "currentPosition": progressInfo["currentPosition"] ?? 0,
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] "duration": progressInfo["duration"] ?? 0,
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 "progress": progressInfo["progress"] ?? 0.0,
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo "playbackSpeed": isPlaying ? 1.0 : 0.0
]
if isPlaying { flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: playbackState)
startProgressTimer()
} else {
stopProgressTimer()
}
print("媒体会话播放状态已更新: \(isPlaying)") print("已更新播放状态: isPlaying=\(isPlaying)")
} }
/// 设置增强的远程控制中心
private func setupEnhancedRemoteCommandCenter() { private func setupEnhancedRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
@ -737,54 +851,54 @@ public class MusicService: NSObject {
// 播放命令 // 播放命令
commandCenter.playCommand.addTarget { [weak self] _ in commandCenter.playCommand.addTarget { [weak self] _ in
print("远程控制: 播放")
self?.resume() self?.resume()
self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil)
return .success return .success
} }
// 暂停命令 // 暂停命令
commandCenter.pauseCommand.addTarget { [weak self] _ in commandCenter.pauseCommand.addTarget { [weak self] _ in
print("远程控制: 暂停")
self?.pause() self?.pause()
self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil)
return .success return .success
} }
// 下一曲命令 // 下一首命令
commandCenter.nextTrackCommand.addTarget { [weak self] _ in commandCenter.nextTrackCommand.addTarget { [weak self] _ in
self?.next() print("远程控制: 下一首")
self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil) self?.playNext()
return .success return .success
} }
// 上一曲命令 // 上一首命令
commandCenter.previousTrackCommand.addTarget { [weak self] _ in commandCenter.previousTrackCommand.addTarget { [weak self] _ in
self?.previous() print("远程控制: 上一首")
self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil) self?.playPrevious()
return .success return .success
} }
// 跳转命令 - 关键升级点 // 进度调整命令
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
if let event = event as? MPChangePlaybackPositionCommandEvent { if let positionEvent = event as? MPChangePlaybackPositionCommandEvent {
let positionMs = Int(event.positionTime * 1000) let positionMs = Int(positionEvent.positionTime * 1000)
print("远程控制: 跳转到 \(positionMs)ms")
self?.seekTo(positionMs: positionMs) self?.seekTo(positionMs: positionMs)
return .success return .success
} }
return .commandFailed return .commandFailed
} }
// 停止命令 // 启用命令
commandCenter.stopCommand.addTarget { [weak self] _ in commandCenter.playCommand.isEnabled = true
self?.stop() commandCenter.pauseCommand.isEnabled = true
self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) commandCenter.nextTrackCommand.isEnabled = playlist.count > 1
return .success commandCenter.previousTrackCommand.isEnabled = playlist.count > 1
} commandCenter.changePlaybackPositionCommand.isEnabled = true
print("增强的远程控制中心已设置,支持进度条滑动") print("增强远程控制中心设置完成")
} }
/// 启动进度定时器 // MARK: - 进度定时器
private func startProgressTimer() { private func startProgressTimer() {
stopProgressTimer() stopProgressTimer()
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
@ -792,162 +906,333 @@ public class MusicService: NSObject {
} }
} }
/// 停止进度定时器
private func stopProgressTimer() { private func stopProgressTimer() {
progressTimer?.invalidate() progressTimer?.invalidate()
progressTimer = nil progressTimer = nil
} }
/// 更新进度信息
private func updateProgressInfo() { private func updateProgressInfo() {
guard let player = player, let currentItem = player.currentItem, isPlaying else { return } guard isPlaying, let player = player, let currentItem = player.currentItem else {
return
}
let currentTime = CMTimeGetSeconds(player.currentTime()) let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration) let duration = CMTimeGetSeconds(currentItem.duration)
if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { if !currentTime.isNaN && !currentTime.isInfinite &&
// 更新系统媒体信息 - 关键升级点 !duration.isNaN && !duration.isInfinite && duration > 0 {
updateSystemMediaInfo(currentTime: currentTime, duration: duration)
// 通知Flutter层
let currentPosition = Int(currentTime * 1000) let currentPosition = Int(currentTime * 1000)
let totalDuration = Int(duration * 1000) let totalDuration = Int(duration * 1000)
let progress = totalDuration > 0 ? Float(currentTime / duration) : 0.0 let progress = Float(currentTime / duration)
flutterChannel?.invokeMethod("onMediaSessionProgress", arguments: [ // 更新媒体会话进度
"currentIdInternal": currentIdInternal ?? 0, updateMediaSessionProgress(currentPosition: currentPosition,
"currentPosition": currentPosition, duration: totalDuration,
"duration": totalDuration, progress: progress)
"progress": progress
]) // 更新系统媒体信息
updateSystemMediaInfo(currentTime: currentTime, duration: duration)
} }
} }
/// 立即更新进度信息(用于seekTo后)
private func updateProgressInfoImmediately() { private func updateProgressInfoImmediately() {
guard let player = player, let currentItem = player.currentItem else { return } guard let player = player, let currentItem = player.currentItem else {
return
}
let currentTime = CMTimeGetSeconds(player.currentTime()) let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration) let duration = CMTimeGetSeconds(currentItem.duration)
if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite {
updateSystemMediaInfo(currentTime: currentTime, duration: duration) updateSystemMediaInfo(currentTime: currentTime, duration: duration)
} }
}
/// 更新系统媒体信息
private func updateSystemMediaInfo(currentTime: Double, duration: Double) { private func updateSystemMediaInfo(currentTime: Double, duration: Double) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
if !currentTime.isNaN && !currentTime.isInfinite {
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
}
if !duration.isNaN && !duration.isInfinite && duration > 0 {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
}
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
// MARK: - 播放完成处理 // MARK: - 播放器观察者
private func setupPlayerObservers() { private func setupPlayerObservers() {
guard let player = player else { return } guard let player = player else { return }
// 监听播放器状态
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
// 监听播放结束通知
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime, self,
object: player.currentItem, selector: #selector(playerDidFinishPlaying),
queue: .main name: .AVPlayerItemDidPlayToEndTime,
) { [weak self] _ in object: player.currentItem
self?.stopProgressTimer() )
self?.playNextOrStop()
}
} }
// MARK: - 播放项观察者设置
private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) { private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) {
// 移除之前的观察者
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
// 监听播放项状态 // 监听播放项状态
playerItem.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil) playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
playerItem.addObserver(self, forKeyPath: "duration", options: [.new], context: nil)
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil)
// 播放完成通知 // 监听缓冲状态
NotificationCenter.default.addObserver( playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil)
forName: .AVPlayerItemDidPlayToEndTime, playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil)
object: playerItem,
queue: .main
) { [weak self] _ in
print("歌曲播放完成")
self?.stopProgressTimer()
self?.playNextOrStop()
} }
// 播放失败通知 @objc private func playerDidFinishPlaying() {
NotificationCenter.default.addObserver( print("歌曲播放完成")
forName: .AVPlayerItemFailedToPlayToEndTime, isPlaying = false
object: playerItem, updatePlaybackState()
queue: .main updateMediaSessionPlaybackState(isPlaying: false)
) { [weak self] notification in stopProgressTimer()
print("播放失败: \(notification)")
self?.isPlaying = false
self?.updateMediaSessionPlaybackState(isPlaying: false)
}
// 存储歌曲信息,用于状态变化时更新 // 自动播放下一首或停止
objc_setAssociatedObject(playerItem, "songInfo", song, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) playNextOrStop()
} }
// MARK: - KVO 观察者 // MARK: - KVO 观察者
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let playerItem = object as? AVPlayerItem else { return } guard let keyPath = keyPath else { return }
switch keyPath { switch keyPath {
case "status": case "status":
if let playerItem = object as? AVPlayerItem {
switch playerItem.status { switch playerItem.status {
case .readyToPlay: case .readyToPlay:
print("播放项准备就绪,开始播放") print("🎵 [Player] 播放器准备就绪,开始播放")
// 获取关联的歌曲信息
if let song = objc_getAssociatedObject(playerItem, "songInfo") as? [String: Any] {
updateNowPlayingInfo(song: song)
}
// 开始播放
player?.play() player?.play()
isPlaying = true isPlaying = true
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true) updateMediaSessionPlaybackState(isPlaying: true)
startProgressTimer() startProgressTimer()
// 强制更新媒体信息,确保通知栏显示
if let currentSong = playlist.isEmpty ? nil : playlist[currentIndex] {
updateNowPlayingInfo(song: currentSong)
updateMediaSessionMetadata(song: currentSong)
print("🎵 [Player] ✅ 播放开始时已更新媒体信息")
}
// 再次确保远程控制激活
DispatchQueue.main.async {
UIApplication.shared.beginReceivingRemoteControlEvents()
print("🎵 [Player] ✅ 远程控制事件接收已重新激活")
}
case .failed: case .failed:
print("播放项加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")") print("播放器加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")")
isPlaying = false isPlaying = false
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: false) updateMediaSessionPlaybackState(isPlaying: false)
case .unknown: case .unknown:
print("播放项状态未知") print("播放器状态未知")
@unknown default: @unknown default:
break break
} }
}
case "duration": case "timeControlStatus":
let duration = CMTimeGetSeconds(playerItem.duration) if let player = object as? AVPlayer {
if !duration.isNaN && !duration.isInfinite && duration > 0 { switch player.timeControlStatus {
print("获取到音频时长: \(duration) 秒") case .playing:
// 更新系统媒体信息 print("播放器正在播放")
updateProgressInfoImmediately() if !isPlaying {
isPlaying = true
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true)
startProgressTimer()
} }
case "loadedTimeRanges": case .paused:
if let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue { print("播放器已暂停")
let loadedDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration)) if isPlaying {
let totalDuration = CMTimeGetSeconds(playerItem.duration) isPlaying = false
if !totalDuration.isNaN && !totalDuration.isInfinite && totalDuration > 0 { updatePlaybackState()
let loadedPercentage = loadedDuration / totalDuration * 100 updateMediaSessionPlaybackState(isPlaying: false)
print("缓冲进度: \(String(format: "%.1f", loadedPercentage))%") stopProgressTimer()
} }
case .waitingToPlayAtSpecifiedRate:
print("播放器等待缓冲")
@unknown default:
break
}
}
case "playbackBufferEmpty":
print("播放缓冲区为空")
case "playbackLikelyToKeepUp":
if let playerItem = object as? AVPlayerItem, playerItem.isPlaybackLikelyToKeepUp {
print("播放缓冲充足,可以继续播放")
} }
default: default:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
// MARK: - 音频中断处理
@objc private func handleAudioInterruption(_ notification: Notification) {
print("🎵 [MusicService] 收到系统音频中断通知")
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
print("🎵 [MusicService] 无法解析中断通知信息")
return
}
switch type {
case .began:
print("🎵 [MusicService] 系统音频中断开始 - 当前播放状态: \(isPlaying)")
// 记录中断发生时是否在播放,并暂停
interruptedDuringPlayback = isPlaying
if isPlaying {
pause()
print("🎵 [MusicService] 已暂停音乐播放")
}
case .ended:
print("🎵 [MusicService] 系统音频中断结束 - 等待AudioSessionHub处理")
// 系统中断结束时不立即处理,等待AudioSessionHub的通知
@unknown default:
print("🎵 [MusicService] 未知的中断类型: \(typeValue)")
break break
} }
} }
// 处理AudioSessionHub发送的中断开始通知
@objc private func handleAudioSessionHubInterruption(_ notification: Notification) {
print("🎵 [MusicService] 收到AudioSessionHub中断开始通知")
// AudioSessionHub已经处理了中断开始,我们这里可以做额外的处理
}
// 处理AudioSessionHub发送的中断结束通知
@objc private func handleAudioSessionHubInterruptionEnded(_ notification: Notification) {
print("🎵 [MusicService] 收到AudioSessionHub中断结束通知")
guard let userInfo = notification.userInfo else {
print("🎵 [MusicService] AudioSessionHub通知缺少userInfo")
return
}
let shouldResume = userInfo["shouldResume"] as? Bool ?? false
let options = userInfo["options"] as? UInt ?? 0
print("🎵 [MusicService] AudioSessionHub建议恢复: \(shouldResume), 选项: \(options)")
print("🎵 [MusicService] 中断前播放状态: \(interruptedDuringPlayback)")
// 只有在中断前正在播放且系统建议恢复时才恢复播放
if interruptedDuringPlayback && shouldResume {
print("🎵 [MusicService] 准备恢复音乐播放...")
// 延迟恢复,给AudioSessionHub时间完成音频会话重新激活
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
do {
// 设置音频会话为播放模式
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: [])
print("🎵 [MusicService] 音频会话已设置为播放模式")
// 恢复播放
self.resume()
print("🎵 [MusicService] 已恢复音乐播放")
self.interruptedDuringPlayback = false
} catch {
print("🎵 [MusicService] 设置音频会话失败: \(error)")
// 即使设置失败也尝试恢复播放
self.resume()
print("🎵 [MusicService] 已尝试恢复音乐播放(音频会话设置失败)")
self.interruptedDuringPlayback = false
}
}
} else if !shouldResume {
print("🎵 [MusicService] AudioSessionHub不建议恢复,保持暂停状态")
interruptedDuringPlayback = false
} else {
print("🎵 [MusicService] 中断前未在播放,不恢复")
interruptedDuringPlayback = false
}
}
@objc private func handleAudioSessionHubResumeMusicPlayback(_ notification: Notification) {
print("🎵 [MusicService] ✅ 收到AudioSessionHub恢复播放通知")
print("🎵 [MusicService] 当前状态 - interruptedDuringPlayback: \(interruptedDuringPlayback), isPlaying: \(isPlaying)")
// 简化逻辑:只要收到通知且之前被中断,就尝试恢复
if interruptedDuringPlayback {
print("🎵 [MusicService] 开始恢复音乐播放...")
// 延迟一点时间确保音频会话已经正确设置
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
do {
// 设置音频会话为播放模式
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: [])
print("🎵 [MusicService] 音频会话已设置为播放模式")
// 恢复播放
self.resume()
print("🎵 [MusicService] ✅ 已恢复音乐播放")
self.interruptedDuringPlayback = false
} catch {
print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)")
// 即使设置失败也尝试恢复播放
self.resume()
print("🎵 [MusicService] ⚠️ 已尝试恢复音乐播放(音频会话设置失败)")
self.interruptedDuringPlayback = false
}
}
} else {
print("🎵 [MusicService] ℹ️ 不需要恢复播放 - interruptedDuringPlayback: \(interruptedDuringPlayback)")
}
}
deinit {
NotificationCenter.default.removeObserver(
self,
name: AVAudioSession.interruptionNotification,
object: nil
)
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name("AudioSessionInterruptionBegan"),
object: nil
)
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name("AudioSessionInterruptionEnded"),
object: nil
)
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name("AudioSessionHub_ResumeMusicPlayback"),
object: nil
)
}
} }

Loading…
Cancel
Save