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