iOS后台播放列表自动切歌问题:AVPlayer未触发切换逻辑
后台模式下自动切歌的最优实现方案
问题背景
实现的AudioPlayer类在前台(屏幕点亮)时功能正常,但进入后台(如设备休眠)后,当前曲目可播放完毕,但.AVPlayerItemDidPlayToEndTime通知无法触发songFinished方法切换下一曲。
当前代码
class AudioPlayer: ObservableObject { var nextTracks: [String]? var player: AVPlayer? { didSet { if let player = player { NotificationCenter.default.addObserver(self, selector: #selector(self.songFinished), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) } } } func play(songId: String, nextSongs: [String]? = nil) { let url = SubsonicClient.shared.stream(id: songId) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) self.nextTracks = nextSongs self.player = AVPlayer(playerItem: item) self.player?.play() } @objc func songFinished() { if let id = nextTracks?.first { let url = SubsonicClient.shared.stream(id: id) nextTracks?.remove(at: 0) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) self.player = AVPlayer(playerItem: item) self.player?.play() } } }
解决步骤
1. 开启后台音频权限
在Xcode项目中:
- 打开
Signing & Capabilities标签页,点击+ Capability添加Background Modes - 勾选
Audio, AirPlay, and Picture in Picture选项
这是后台音频功能生效的基础,让系统允许应用在后台处理音频相关逻辑。
2. 激活并配置音频会话
在播放音频前必须激活AVAudioSession,确保应用获得音频焦点,后台状态下也能正常触发音频事件。修改play方法添加音频会话配置:
func play(songId: String, nextSongs: [String]? = nil) { // 配置音频会话 do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) try AVAudioSession.sharedInstance().activate() } catch { print("音频会话配置失败: \(error.localizedDescription)") } let url = SubsonicClient.shared.stream(id: songId) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) self.nextTracks = nextSongs // 复用AVPlayer实例,避免重复创建 if let player = self.player { player.replaceCurrentItem(with: item) } else { self.player = AVPlayer(playerItem: item) // 注册通知(只注册一次) NotificationCenter.default.addObserver(self, selector: #selector(songFinished), name: .AVPlayerItemDidPlayToEndTime, object: nil) } self.player?.play() }
3. 优化通知注册逻辑
原代码每次创建新AVPlayer都会注册通知,容易导致重复注册或内存泄漏。改为只注册一次通知,监听所有AVPlayerItem的结束事件(将object设为nil),同时在deinit中移除通知避免内存泄漏:
deinit { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) }
4. 复用AVPlayer实例
原代码每次切歌都创建新的AVPlayer,会导致后台状态下系统难以维持音频上下文。改为复用同一个AVPlayer实例,通过replaceCurrentItem(with:)方法切换曲目,保持音频会话连续性,后台事件触发更稳定。
5. 优化切歌逻辑
修改songFinished方法,复用现有AVPlayer实例:
@objc func songFinished() { guard let id = nextTracks?.first, let player = self.player else { return } let url = SubsonicClient.shared.stream(id: id) nextTracks?.remove(at: 0) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) player.replaceCurrentItem(with: item) player.play() }
最终优化后的完整代码
import AVFoundation import Combine class AudioPlayer: ObservableObject { var nextTracks: [String]? private(set) var player: AVPlayer? init() { // 提前初始化AVPlayer player = AVPlayer() // 注册通知 NotificationCenter.default.addObserver(self, selector: #selector(songFinished), name: .AVPlayerItemDidPlayToEndTime, object: nil) } deinit { // 移除通知监听 NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) } func play(songId: String, nextSongs: [String]? = nil) { // 配置并激活音频会话 do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) try AVAudioSession.sharedInstance().activate() } catch { print("音频会话配置失败: \(error.localizedDescription)") } let url = SubsonicClient.shared.stream(id: songId) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) self.nextTracks = nextSongs player?.replaceCurrentItem(with: item) player?.play() } @objc func songFinished() { guard let id = nextTracks?.first, let player = self.player else { return } let url = SubsonicClient.shared.stream(id: id) nextTracks?.remove(at: 0) let asset = AVURLAsset(url: url) let item = AVPlayerItem(asset: asset) player.replaceCurrentItem(with: item) player.play() } }
内容的提问来源于stack exchange,提问作者Halpo
相关产品推荐
相关产品推荐

