From 02b49139101072ed010e3e1b84c849ecc9375b27 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 24 Oct 2025 15:03:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dios=E7=94=B5=E8=AF=9D?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=20=E5=9B=9E=E5=A4=8D=E9=9F=B3=E4=B9=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AudioSessionHub.swift | 60 +- .../Sources/music_service/MusicService.swift | 887 ++++++++++++------ 2 files changed, 641 insertions(+), 306 deletions(-) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift index f9132ee1d..65cbf38af 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift +++ b/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(" - 中断状态: %{public}@", log: logger, type: .info, isInterrupted ? "是" : "否") + // 发送中断开始通知给其他服务 + NotificationCenter.default.post( + name: NSNotification.Name("AudioSessionInterruptionBegan"), + object: nil, + userInfo: ["type": "began"] + ) + case .ended: // 中断结束 isInterrupted = false @@ -480,16 +487,43 @@ public class AudioSessionHub { let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) 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)) - // 恢复之前的音频意图 - //begin(intent) - os_log("✓ 音频会话中断结束,已恢复", log: logger, type: .info) + // 发送中断结束通知给其他服务,包含恢复建议 + NotificationCenter.default.post( + name: NSNotification.Name("AudioSessionInterruptionEnded"), + object: nil, + 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 { os_log("不需要恢复音频会话", log: logger, type: .info) } } else { os_log("⚠️ 中断通知中无选项信息", log: logger, type: .default) + + // 即使没有选项信息,也发送通知 + NotificationCenter.default.post( + name: NSNotification.Name("AudioSessionInterruptionEnded"), + object: nil, + userInfo: [ + "type": "ended", + "shouldResume": false, + "options": 0 + ] + ) } @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 { NotificationCenter.default.removeObserver(self) } diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 4e6c098ea..1236b423a 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -32,10 +32,19 @@ public class MusicService: NSObject { // 当前播放的歌曲ID private var currentIdInternal: Int64? = nil + // 音频中断处理相关 + private var interruptedDuringPlayback: Bool = false + private override init() { super.init() + + // 确保应用能接收远程控制事件 + UIApplication.shared.beginReceivingRemoteControlEvents() + setupRemoteCommandCenter() setupAudioSession() + + print("🎵 [MusicService] 🚀 MusicService初始化完成,远程控制已激活") } public func initialize(serviceUrl: String, userToken: String) { @@ -44,22 +53,85 @@ public class MusicService: NSObject { print("MusicService initialized with url: \(serviceUrl)") } - + // MARK: - 音频会话设置 private func setupAudioSession() { - do { - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP]) - try audioSession.setActive(true) - - // 确保媒体控制中心可以接收到我们的应用 - setupRemoteCommandCenter() - - print("音频会话和媒体控制设置成功") - } catch { - print("设置音频会话失败: \(error)") - } - } + print("🎵 [MusicService] 🚀 开始设置音频会话和通知监听器") + + // 先注册通知监听器,不依赖音频会话设置成功 + setupNotificationObservers() + + // 然后尝试设置音频会话 + setupAudioSessionConfiguration() + + // 确保媒体控制中心可以接收到我们的应用 + setupRemoteCommandCenter() + + 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 { + print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)") + print("🎵 [MusicService] ℹ️ 音频会话设置失败不影响通知系统工作") + } + } // MARK: - 基础播放控制 func getCurrentUrlString() -> String? { @@ -357,6 +429,16 @@ public class MusicService: NSObject { 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 if let existingId = existingSong["id"] as? NSNumber, @@ -389,9 +471,12 @@ public class MusicService: NSObject { // 立即设置媒体信息,即使播放器还未准备就绪 updateNowPlayingInfo(song: song) - // 确保远程控制中心已设置 + // 强制重新设置远程控制中心 setupRemoteCommandCenter() + // 强制激活远程控制 + UIApplication.shared.beginReceivingRemoteControlEvents() + print("播放器已创建,等待加载完成...") } @@ -401,6 +486,12 @@ public class MusicService: NSObject { isPlaying = false updatePlaybackState() updateMediaSessionPlaybackState(isPlaying: false) + + // 更新媒体信息,保持通知栏显示但状态为暂停 + if currentIndex < playlist.count { + updateNowPlayingInfo(song: playlist[currentIndex]) + } + stopProgressTimer() print("已暂停") } @@ -410,6 +501,12 @@ public class MusicService: NSObject { isPlaying = true updatePlaybackState() updateMediaSessionPlaybackState(isPlaying: true) + + // 重新设置媒体信息,确保通知栏显示 + if currentIndex < playlist.count { + updateNowPlayingInfo(song: playlist[currentIndex]) + } + startProgressTimer() print("继续播放") } @@ -527,427 +624,615 @@ public class MusicService: NSObject { if completed { print("跳转到位置: \(positionMs)ms") - // 如果之前是播放状态,确保继续播放 + // 如果之前在播放,跳转后继续播放 if wasPlaying { - self?.player?.play() - self?.isPlaying = true - self?.updateMediaSessionPlaybackState(isPlaying: true) - self?.startProgressTimer() + self?.resume() } - - // 立即更新进度信息到系统媒体控制中心 - self?.updateProgressInfoImmediately() - - // 通知Flutter层 - self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) + } else { + print("跳转失败") } } } - // MARK: - 媒体会话控制 + // MARK: - 远程控制中心设置 private func setupRemoteCommandCenter() { 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 } - commandCenter.pauseCommand.addTarget { [unowned self] _ in - self.pause() + // 暂停命令 + commandCenter.pauseCommand.addTarget { [weak self] _ in + print("🎵 [RemoteControl] 暂停命令") + self?.pause() return .success } - commandCenter.nextTrackCommand.addTarget { [unowned self] _ in - self.next() + // 下一首命令 + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + print("🎵 [RemoteControl] 下一首命令") + self?.playNext() return .success } - commandCenter.previousTrackCommand.addTarget { [unowned self] _ in - self.previous() + // 上一首命令 + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + print("🎵 [RemoteControl] 上一首命令") + self?.playPrevious() return .success } - commandCenter.stopCommand.addTarget { [unowned self] _ in - self.stop() - return .success + // 进度调整命令(iOS 9.1+) + 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 .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] ✅ 远程控制中心设置完成") } + // MARK: - 媒体信息更新 private func updateNowPlayingInfo(song: [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]() - var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: title, - MPMediaItemPropertyArtist: artist - ] - - // 异步加载封面图片 - if !coverUrl.isEmpty, let url = URL(string: coverUrl) { - print("开始加载封面图片: \(coverUrl)") + // 设置歌曲信息 + nowPlayingInfo[MPMediaItemPropertyTitle] = song["name"] as? String ?? "未知歌曲" + nowPlayingInfo[MPMediaItemPropertyArtist] = song["singer"] as? String ?? "未知歌手" + + // 设置专辑信息(如果有的话) + 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) - DispatchQueue.global(qos: .background).async { - // 创建带超时的URLRequest - var request = URLRequest(url: url) - request.timeoutInterval = 10.0 - - URLSession.shared.dataTask(with: request) { data, response, error in - 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("图片数据无效或格式不支持") - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - } - return - } - - print("图片加载成功") - DispatchQueue.main.async { - nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - } - }.resume() + if !currentTime.isNaN && !currentTime.isInfinite { + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime } - } else { - print("封面图片URL为空") + + if !duration.isNaN && !duration.isInfinite && duration > 0 { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + } + } + + // 设置播放速率 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + + // 设置媒体类型为音频 + nowPlayingInfo[MPMediaItemPropertyMediaType] = MPMediaType.music.rawValue + + // 强制更新媒体信息 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + // 确保在主线程上再次设置,解决某些iOS版本的兼容性问题 + DispatchQueue.main.async { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } + + print("🎵 [MediaInfo] ✅ 已更新媒体信息: \(song["name"] as? String ?? "未知")") + print("🎵 [MediaInfo] 📱 播放状态: \(isPlaying ? "播放中" : "暂停")") } private func updatePlaybackState() { - // iOS manages playback state automatically, but you can update NowPlayingInfo if needed + guard let flutterChannel = flutterChannel else { return } + + 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) { - var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - - // 更新播放时长和当前位置 - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0 - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0 - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 - - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + guard let flutterChannel = flutterChannel else { return } + + flutterChannel.invokeMethod("onProgressChanged", arguments: [ + "currentPosition": currentPosition, + "duration": duration, + "progress": progress + ]) } - - /// 启用媒体会话回调 + public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { - flutterChannel = channel - setupEnhancedRemoteCommandCenter() - startProgressTimer() + self.flutterChannel = channel print("媒体会话回调已启用") } - - /// 更新媒体会话元数据 + public func updateMediaSessionMetadata(song: [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 ?? "" + guard let flutterChannel = flutterChannel else { return } - // 更新当前歌曲ID - if let id = song["id"] as? NSNumber { - currentIdInternal = id.int64Value - } + // 构建媒体元数据 + var metadata: [String: Any] = [:] - print("updateMediaSessionMetadata currentIdInternal: \(currentIdInternal ?? 0)") - - var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: title, - MPMediaItemPropertyArtist: artist - ] - - // 异步加载封面图片 - 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 - 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("图片数据无效或格式不支持") - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - } - return - } - - print("图片加载成功") - DispatchQueue.main.async { - nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - } - }.resume() - } - } else { - print("封面图片URL为空") - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + // 基本信息 + 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 + } + + // 播放状态 + metadata["isPlaying"] = isPlaying - print("媒体会话元数据已更新: \(title) - \(artist)") + // 进度信息 + let progressInfo = getProgress() + metadata["currentPosition"] = progressInfo["currentPosition"] ?? 0 + metadata["duration"] = progressInfo["duration"] ?? 0 + metadata["progress"] = progressInfo["progress"] ?? 0.0 + + // 播放列表信息 + metadata["playlistSize"] = playlist.count + metadata["currentIndex"] = currentIndex + + // 能力标志 + metadata["canSkipToNext"] = currentIndex < playlist.count - 1 + metadata["canSkipToPrevious"] = currentIndex > 0 + metadata["canSeek"] = true + + flutterChannel.invokeMethod("onMetadataChanged", arguments: metadata) + + print("已更新媒体元数据: \(metadata)") } - - /// 更新媒体会话播放状态 + public func updateMediaSessionPlaybackState(isPlaying: Bool) { - self.isPlaying = isPlaying - var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - - if isPlaying { - startProgressTimer() - } else { - stopProgressTimer() - } + guard let flutterChannel = flutterChannel else { return } + + let progressInfo = getProgress() - print("媒体会话播放状态已更新: \(isPlaying)") + let playbackState: [String: Any] = [ + "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) + + print("已更新播放状态: isPlaying=\(isPlaying)") } - - /// 设置增强的远程控制中心 + private func setupEnhancedRemoteCommandCenter() { let commandCenter = MPRemoteCommandCenter.shared() - + // 清除之前的目标 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("远程控制: 播放") self?.resume() - self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil) return .success } - + // 暂停命令 commandCenter.pauseCommand.addTarget { [weak self] _ in + print("远程控制: 暂停") self?.pause() - self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil) return .success } - - // 下一曲命令 + + // 下一首命令 commandCenter.nextTrackCommand.addTarget { [weak self] _ in - self?.next() - self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil) + print("远程控制: 下一首") + self?.playNext() return .success } - - // 上一曲命令 + + // 上一首命令 commandCenter.previousTrackCommand.addTarget { [weak self] _ in - self?.previous() - self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil) + print("远程控制: 上一首") + self?.playPrevious() return .success } - - // 跳转命令 - 关键升级点 - commandCenter.changePlaybackPositionCommand.isEnabled = true + + // 进度调整命令 commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in - if let event = event as? MPChangePlaybackPositionCommandEvent { - let positionMs = Int(event.positionTime * 1000) + if let positionEvent = event as? MPChangePlaybackPositionCommandEvent { + let positionMs = Int(positionEvent.positionTime * 1000) + print("远程控制: 跳转到 \(positionMs)ms") self?.seekTo(positionMs: positionMs) return .success } return .commandFailed } - - // 停止命令 - commandCenter.stopCommand.addTarget { [weak self] _ in - self?.stop() - self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) - return .success - } - print("增强的远程控制中心已设置,支持进度条滑动") + // 启用命令 + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.nextTrackCommand.isEnabled = playlist.count > 1 + commandCenter.previousTrackCommand.isEnabled = playlist.count > 1 + commandCenter.changePlaybackPositionCommand.isEnabled = true + + print("增强远程控制中心设置完成") } - - /// 启动进度定时器 + + // MARK: - 进度定时器 private func startProgressTimer() { stopProgressTimer() progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.updateProgressInfo() } } - - /// 停止进度定时器 + private func stopProgressTimer() { progressTimer?.invalidate() progressTimer = nil } - - /// 更新进度信息 + 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 duration = CMTimeGetSeconds(currentItem.duration) - - if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { - // 更新系统媒体信息 - 关键升级点 - updateSystemMediaInfo(currentTime: currentTime, duration: duration) + + if !currentTime.isNaN && !currentTime.isInfinite && + !duration.isNaN && !duration.isInfinite && duration > 0 { - // 通知Flutter层 let currentPosition = Int(currentTime * 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, - "currentPosition": currentPosition, - "duration": totalDuration, - "progress": progress - ]) + // 更新媒体会话进度 + updateMediaSessionProgress(currentPosition: currentPosition, + duration: totalDuration, + progress: progress) + + // 更新系统媒体信息 + updateSystemMediaInfo(currentTime: currentTime, duration: duration) } } - /// 立即更新进度信息(用于seekTo后) 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 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) { var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + + if !currentTime.isNaN && !currentTime.isInfinite { + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + } + + if !duration.isNaN && !duration.isInfinite && duration > 0 { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + } + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - - // MARK: - 播放完成处理 + + // MARK: - 播放器观察者 private func setupPlayerObservers() { guard let player = player else { return } + // 监听播放器状态 + player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil) + + // 监听播放结束通知 NotificationCenter.default.addObserver( - forName: .AVPlayerItemDidPlayToEndTime, - object: player.currentItem, - queue: .main - ) { [weak self] _ in - self?.stopProgressTimer() - self?.playNextOrStop() - } + self, + selector: #selector(playerDidFinishPlaying), + name: .AVPlayerItemDidPlayToEndTime, + object: player.currentItem + ) } - // MARK: - 播放项观察者设置 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: "duration", options: [.new], context: nil) - playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil) - - // 播放完成通知 - NotificationCenter.default.addObserver( - forName: .AVPlayerItemDidPlayToEndTime, - object: playerItem, - queue: .main - ) { [weak self] _ in - print("歌曲播放完成") - self?.stopProgressTimer() - self?.playNextOrStop() - } + playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: nil) - // 播放失败通知 - NotificationCenter.default.addObserver( - forName: .AVPlayerItemFailedToPlayToEndTime, - object: playerItem, - queue: .main - ) { [weak self] notification in - print("播放失败: \(notification)") - self?.isPlaying = false - self?.updateMediaSessionPlaybackState(isPlaying: false) - } + // 监听缓冲状态 + playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil) + playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil) + } + + @objc private func playerDidFinishPlaying() { + print("歌曲播放完成") + isPlaying = false + updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: false) + stopProgressTimer() - // 存储歌曲信息,用于状态变化时更新 - objc_setAssociatedObject(playerItem, "songInfo", song, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + // 自动播放下一首或停止 + playNextOrStop() } - -// MARK: - KVO 观察者 -override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - guard let playerItem = object as? AVPlayerItem else { return } - - switch keyPath { - case "status": - switch playerItem.status { - case .readyToPlay: - print("播放项准备就绪,开始播放") + + // MARK: - KVO 观察者 + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let keyPath = keyPath else { return } + + switch keyPath { + case "status": + if let playerItem = object as? AVPlayerItem { + switch playerItem.status { + case .readyToPlay: + 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("播放器状态未知") + + @unknown default: + break + } + } - // 获取关联的歌曲信息 - if let song = objc_getAssociatedObject(playerItem, "songInfo") as? [String: Any] { - updateNowPlayingInfo(song: song) + case "timeControlStatus": + if let player = object as? AVPlayer { + switch player.timeControlStatus { + case .playing: + print("播放器正在播放") + if !isPlaying { + isPlaying = true + updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: true) + startProgressTimer() + } + + case .paused: + print("播放器已暂停") + if isPlaying { + isPlaying = false + updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: false) + stopProgressTimer() + } + + case .waitingToPlayAtSpecifiedRate: + print("播放器等待缓冲") + + @unknown default: + break + } } - // 开始播放 - player?.play() - isPlaying = true - updateMediaSessionPlaybackState(isPlaying: true) - startProgressTimer() + case "playbackBufferEmpty": + print("播放缓冲区为空") + + case "playbackLikelyToKeepUp": + if let playerItem = object as? AVPlayerItem, playerItem.isPlaybackLikelyToKeepUp { + print("播放缓冲充足,可以继续播放") + } - case .failed: - print("播放项加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")") - isPlaying = false - updateMediaSessionPlaybackState(isPlaying: false) + 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 .unknown: - print("播放项状态未知") + case .ended: + print("🎵 [MusicService] 系统音频中断结束 - 等待AudioSessionHub处理") + // 系统中断结束时不立即处理,等待AudioSessionHub的通知 @unknown default: + print("🎵 [MusicService] 未知的中断类型: \(typeValue)") break } + } + + // 处理AudioSessionHub发送的中断开始通知 + @objc private func handleAudioSessionHubInterruption(_ notification: Notification) { + print("🎵 [MusicService] 收到AudioSessionHub中断开始通知") + // AudioSessionHub已经处理了中断开始,我们这里可以做额外的处理 + } + + // 处理AudioSessionHub发送的中断结束通知 + @objc private func handleAudioSessionHubInterruptionEnded(_ notification: Notification) { + print("🎵 [MusicService] 收到AudioSessionHub中断结束通知") - case "duration": - let duration = CMTimeGetSeconds(playerItem.duration) - if !duration.isNaN && !duration.isInfinite && duration > 0 { - print("获取到音频时长: \(duration) 秒") - // 更新系统媒体信息 - updateProgressInfoImmediately() + 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)") - case "loadedTimeRanges": - 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 { - let loadedPercentage = loadedDuration / totalDuration * 100 - print("缓冲进度: \(String(format: "%.1f", loadedPercentage))%") + // 简化逻辑:只要收到通知且之前被中断,就尝试恢复 + 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 + ) - default: - break + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name("AudioSessionInterruptionEnded"), + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name("AudioSessionHub_ResumeMusicPlayback"), + object: nil + ) } } -} From 1184db8b332037d636fa1473c7d33028b73bd079 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 24 Oct 2025 15:53:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=BF=98=E5=8E=9F=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ios/music_service/Sources/music_service/MusicService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 1236b423a..1e5f2260d 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -113,7 +113,7 @@ public class MusicService: NSObject { let audioSession = AVAudioSession.sharedInstance() // 使用更简单的配置,避免参数冲突 - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) + try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP]) print("🎵 [MusicService] ✅ 音频会话类别设置成功") // 延迟激活音频会话,避免与其他服务冲突