You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 18:20:07